getting keybinds going
This commit is contained in:
60
src/App.tsx
60
src/App.tsx
@@ -13,7 +13,8 @@ import { useRenderer } from "@opentui/solid";
|
|||||||
import type { AuthScreen } from "@/types/auth";
|
import type { AuthScreen } from "@/types/auth";
|
||||||
import type { Episode } from "@/types/episode";
|
import type { Episode } from "@/types/episode";
|
||||||
import { DIRECTION, LayerGraph, TABS } from "./utils/navigation";
|
import { DIRECTION, LayerGraph, TABS } from "./utils/navigation";
|
||||||
import { useTheme } from "./context/ThemeContext";
|
import { useTheme, ThemeProvider } from "./context/ThemeContext";
|
||||||
|
import { KeybindProvider, useKeybinds } from "./context/KeybindContext";
|
||||||
|
|
||||||
const DEBUG = import.meta.env.DEBUG;
|
const DEBUG = import.meta.env.DEBUG;
|
||||||
|
|
||||||
@@ -34,6 +35,7 @@ export function App() {
|
|||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const renderer = useRenderer();
|
const renderer = useRenderer();
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
|
const keybind = useKeybinds();
|
||||||
|
|
||||||
useMultimediaKeys({
|
useMultimediaKeys({
|
||||||
playerFocused: () => activeTab() === TABS.PLAYER && layerDepth() > 0,
|
playerFocused: () => activeTab() === TABS.PLAYER && layerDepth() > 0,
|
||||||
@@ -62,16 +64,64 @@ export function App() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle keyboard input with dynamic keybinds
|
||||||
useKeyboard(
|
useKeyboard(
|
||||||
(keyEvent) => {
|
(keyEvent) => {
|
||||||
//handle intra layer navigation
|
const name = keyEvent.name;
|
||||||
if (keyEvent.name == "up" || keyEvent.name) {
|
|
||||||
|
// Navigation: up/down
|
||||||
|
if (keybind.match("up", keyEvent) || keybind.match("down", keyEvent)) {
|
||||||
|
// TODO: Implement navigation logic
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigation: left/right
|
||||||
|
if (keybind.match("left", keyEvent) || keybind.match("right", keyEvent)) {
|
||||||
|
// TODO: Implement navigation logic
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cycle through options
|
||||||
|
if (keybind.match("cycle", keyEvent)) {
|
||||||
|
// TODO: Implement cycle logic
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dive into content
|
||||||
|
if (keybind.match("dive", keyEvent)) {
|
||||||
|
// TODO: Implement dive logic
|
||||||
|
}
|
||||||
|
|
||||||
|
// Out of content
|
||||||
|
if (keybind.match("out", keyEvent)) {
|
||||||
|
setActiveDepth((prev) => Math.max(0, prev - 1));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Audio controls
|
||||||
|
if (keybind.match("audio-toggle", keyEvent)) {
|
||||||
|
audio.togglePlayback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keybind.match("audio-next", keyEvent)) {
|
||||||
|
audio.seekRelative(30); // Skip forward 30 seconds
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keybind.match("audio-prev", keyEvent)) {
|
||||||
|
audio.seekRelative(-30); // Skip back 30 seconds
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quit application
|
||||||
|
if (keybind.match("quit", keyEvent)) {
|
||||||
|
process.exit(0);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ release: false }, // Not strictly necessary
|
{ release: false },
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<KeybindProvider>
|
||||||
|
<ThemeProvider mode="dark">
|
||||||
<ErrorBoundary
|
<ErrorBoundary
|
||||||
fallback={(err) => (
|
fallback={(err) => (
|
||||||
<box border padding={2} borderColor={theme.error}>
|
<box border padding={2} borderColor={theme.error}>
|
||||||
@@ -123,5 +173,7 @@ export function App() {
|
|||||||
{/**TODO: Contextual controls based on tab/depth**/}
|
{/**TODO: Contextual controls based on tab/depth**/}
|
||||||
</box>
|
</box>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
</ThemeProvider>
|
||||||
|
</KeybindProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,28 @@
|
|||||||
import { useTheme } from "@/context/ThemeContext";
|
import { useTheme } from "@/context/ThemeContext";
|
||||||
import type { JSXElement } from "solid-js";
|
import { children as solidChildren } from "solid-js";
|
||||||
|
import type { ParentComponent } from "solid-js";
|
||||||
import type { BoxOptions, TextOptions } from "@opentui/core";
|
import type { BoxOptions, TextOptions } from "@opentui/core";
|
||||||
|
|
||||||
export function SelectableBox({
|
export const SelectableBox: ParentComponent<
|
||||||
selected,
|
{
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: {
|
|
||||||
selected: () => boolean;
|
selected: () => boolean;
|
||||||
|
} & BoxOptions
|
||||||
children: JSXElement;
|
> = (props) => {
|
||||||
} & BoxOptions) {
|
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
|
|
||||||
|
const child = solidChildren(() => props.children);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<box
|
<box
|
||||||
border={!!props.border}
|
border={!!props.border}
|
||||||
borderColor={selected() ? theme.surface : theme.border}
|
borderColor={props.selected() ? theme.surface : theme.border}
|
||||||
backgroundColor={selected() ? theme.primary : theme.surface}
|
backgroundColor={props.selected() ? theme.primary : theme.surface}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{child()}
|
||||||
</box>
|
</box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
enum ColorSet {
|
enum ColorSet {
|
||||||
PRIMARY,
|
PRIMARY,
|
||||||
@@ -45,35 +44,31 @@ function getTextColor(set: ColorSet, selected: () => boolean) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SelectableText({
|
export const SelectableText: ParentComponent<
|
||||||
selected,
|
{
|
||||||
children,
|
|
||||||
primary,
|
|
||||||
secondary,
|
|
||||||
tertiary,
|
|
||||||
...props
|
|
||||||
}: {
|
|
||||||
selected: () => boolean;
|
selected: () => boolean;
|
||||||
primary?: boolean;
|
primary?: boolean;
|
||||||
secondary?: boolean;
|
secondary?: boolean;
|
||||||
tertiary?: boolean;
|
tertiary?: boolean;
|
||||||
children: JSXElement;
|
} & TextOptions
|
||||||
} & TextOptions) {
|
> = (props) => {
|
||||||
|
const child = solidChildren(() => props.children);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<text
|
<text
|
||||||
fg={getTextColor(
|
fg={getTextColor(
|
||||||
primary
|
props.primary
|
||||||
? ColorSet.PRIMARY
|
? ColorSet.PRIMARY
|
||||||
: secondary
|
: props.secondary
|
||||||
? ColorSet.SECONDARY
|
? ColorSet.SECONDARY
|
||||||
: tertiary
|
: props.tertiary
|
||||||
? ColorSet.TERTIARY
|
? ColorSet.TERTIARY
|
||||||
: ColorSet.DEFAULT,
|
: ColorSet.DEFAULT,
|
||||||
selected,
|
props.selected,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{child()}
|
||||||
</text>
|
</text>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
import { createSignal, For, Show } from "solid-js";
|
import { createSignal, For, Show } from "solid-js";
|
||||||
import { useFeedStore } from "@/stores/feed";
|
import { useFeedStore } from "@/stores/feed";
|
||||||
|
import { useKeyboard } from "@opentui/solid";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import type { Episode } from "@/types/episode";
|
import type { Episode } from "@/types/episode";
|
||||||
import type { Feed } from "@/types/feed";
|
import type { Feed } from "@/types/feed";
|
||||||
@@ -18,10 +19,14 @@ enum FeedPaneType {
|
|||||||
}
|
}
|
||||||
export const FeedPaneCount = 1;
|
export const FeedPaneCount = 1;
|
||||||
|
|
||||||
|
/** Episodes to load per batch */
|
||||||
|
const ITEMS_PER_BATCH = 50;
|
||||||
|
|
||||||
export function FeedPage(props: PageProps) {
|
export function FeedPage(props: PageProps) {
|
||||||
const feedStore = useFeedStore();
|
const feedStore = useFeedStore();
|
||||||
const [selectedIndex, setSelectedIndex] = createSignal(0);
|
const [selectedIndex, setSelectedIndex] = createSignal(0);
|
||||||
const [isRefreshing, setIsRefreshing] = createSignal(false);
|
const [isRefreshing, setIsRefreshing] = createSignal(false);
|
||||||
|
const [loadedEpisodesCount, setLoadedEpisodesCount] = createSignal(ITEMS_PER_BATCH);
|
||||||
|
|
||||||
const allEpisodes = () => feedStore.getAllEpisodesChronological();
|
const allEpisodes = () => feedStore.getAllEpisodesChronological();
|
||||||
|
|
||||||
@@ -29,9 +34,14 @@ export function FeedPage(props: PageProps) {
|
|||||||
return format(date, "MMM d, yyyy");
|
return format(date, "MMM d, yyyy");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const paginatedEpisodes = () => {
|
||||||
|
const episodes = allEpisodes();
|
||||||
|
return episodes.slice(0, loadedEpisodesCount());
|
||||||
|
};
|
||||||
|
|
||||||
const episodesByDate = () => {
|
const episodesByDate = () => {
|
||||||
const groups: Record<string, { episode: Episode; feed: Feed }> = {};
|
const groups: Record<string, { episode: Episode; feed: Feed }> = {};
|
||||||
const sortedEpisodes = allEpisodes();
|
const sortedEpisodes = paginatedEpisodes();
|
||||||
|
|
||||||
for (const episode of sortedEpisodes) {
|
for (const episode of sortedEpisodes) {
|
||||||
const dateKey = formatDate(new Date(episode.episode.pubDate));
|
const dateKey = formatDate(new Date(episode.episode.pubDate));
|
||||||
@@ -54,6 +64,11 @@ export function FeedPage(props: PageProps) {
|
|||||||
setIsRefreshing(false);
|
setIsRefreshing(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleScrollDown = async () => {
|
||||||
|
if (feedStore.isLoadingMore() || !feedStore.hasMoreEpisodes()) return;
|
||||||
|
await feedStore.loadMoreEpisodes();
|
||||||
|
};
|
||||||
|
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
return (
|
return (
|
||||||
<box
|
<box
|
||||||
@@ -131,6 +146,12 @@ export function FeedPage(props: PageProps) {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</For>
|
</For>
|
||||||
|
{/* Loading indicator */}
|
||||||
|
<Show when={feedStore.isLoadingMore()}>
|
||||||
|
<box padding={1}>
|
||||||
|
<text fg={theme.textMuted}>Loading more episodes...</text>
|
||||||
|
</box>
|
||||||
|
</Show>
|
||||||
</scrollbox>
|
</scrollbox>
|
||||||
</Show>
|
</Show>
|
||||||
</box>
|
</box>
|
||||||
|
|||||||
@@ -64,6 +64,19 @@ export function generateSystemTheme(
|
|||||||
const diffAddedLineNumberBg = tint(grays[3], ansi.green, diffAlpha);
|
const diffAddedLineNumberBg = tint(grays[3], ansi.green, diffAlpha);
|
||||||
const diffRemovedLineNumberBg = tint(grays[3], ansi.red, diffAlpha);
|
const diffRemovedLineNumberBg = tint(grays[3], ansi.red, diffAlpha);
|
||||||
|
|
||||||
|
// Create darker shades for selected text colors to ensure contrast
|
||||||
|
const darken = (color: RGBA, factor: number = 0.6) => {
|
||||||
|
return RGBA.fromInts(
|
||||||
|
Math.round(color.r * 255 * factor),
|
||||||
|
Math.round(color.g * 255 * factor),
|
||||||
|
Math.round(color.b * 255 * factor)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedPrimary = darken(ansi.cyan, isDark ? 0.4 : 0.6);
|
||||||
|
const selectedSecondary = darken(ansi.magenta, isDark ? 0.4 : 0.6);
|
||||||
|
const selectedTertiary = darken(textMuted, isDark ? 0.5 : 0.5);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
theme: {
|
theme: {
|
||||||
primary: ansi.cyan,
|
primary: ansi.cyan,
|
||||||
@@ -75,6 +88,12 @@ export function generateSystemTheme(
|
|||||||
info: ansi.cyan,
|
info: ansi.cyan,
|
||||||
text: fg,
|
text: fg,
|
||||||
textMuted,
|
textMuted,
|
||||||
|
textPrimary: fg,
|
||||||
|
textSecondary: textMuted,
|
||||||
|
textTertiary: textMuted,
|
||||||
|
textSelectedPrimary: selectedPrimary,
|
||||||
|
textSelectedSecondary: selectedSecondary,
|
||||||
|
textSelectedTertiary: selectedTertiary,
|
||||||
selectedListItemText: bg,
|
selectedListItemText: bg,
|
||||||
background: transparent,
|
background: transparent,
|
||||||
backgroundPanel: grays[2],
|
backgroundPanel: grays[2],
|
||||||
|
|||||||
Reference in New Issue
Block a user