diff --git a/src/App.tsx b/src/App.tsx index 8042780..fbb6da4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,7 +13,8 @@ import { useRenderer } from "@opentui/solid"; import type { AuthScreen } from "@/types/auth"; import type { Episode } from "@/types/episode"; 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; @@ -34,6 +35,7 @@ export function App() { const toast = useToast(); const renderer = useRenderer(); const { theme } = useTheme(); + const keybind = useKeybinds(); useMultimediaKeys({ playerFocused: () => activeTab() === TABS.PLAYER && layerDepth() > 0, @@ -62,66 +64,116 @@ export function App() { }); }); + // Handle keyboard input with dynamic keybinds useKeyboard( (keyEvent) => { - //handle intra layer navigation - if (keyEvent.name == "up" || keyEvent.name) { + const name = 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 ( - ( - - - Error: {err?.message ?? String(err)} - {"\n"} - Press a number key (1-6) to switch tabs. - - - )} - > - {DEBUG && ( - - - - - - - - - - - - - - - - - - - - - - - - - - - )} - - - - {LayerGraph[activeTab()]({ depth: activeDepth })} - {/**TODO: Contextual controls based on tab/depth**/} - - + + + ( + + + Error: {err?.message ?? String(err)} + {"\n"} + Press a number key (1-6) to switch tabs. + + + )} + > + {DEBUG && ( + + + + + + + + + + + + + + + + + + + + + + + + + + + )} + + + + {LayerGraph[activeTab()]({ depth: activeDepth })} + {/**TODO: Contextual controls based on tab/depth**/} + + + + ); } diff --git a/src/components/Selectable.tsx b/src/components/Selectable.tsx index 690ca89..69692f4 100644 --- a/src/components/Selectable.tsx +++ b/src/components/Selectable.tsx @@ -1,29 +1,28 @@ 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"; -export function SelectableBox({ - selected, - children, - ...props -}: { - selected: () => boolean; - - children: JSXElement; -} & BoxOptions) { +export const SelectableBox: ParentComponent< + { + selected: () => boolean; + } & BoxOptions +> = (props) => { const { theme } = useTheme(); + const child = solidChildren(() => props.children); + return ( - {children} + {child()} ); -} +}; enum ColorSet { PRIMARY, @@ -45,35 +44,31 @@ function getTextColor(set: ColorSet, selected: () => boolean) { } } -export function SelectableText({ - selected, - children, - primary, - secondary, - tertiary, - ...props -}: { - selected: () => boolean; - primary?: boolean; - secondary?: boolean; - tertiary?: boolean; - children: JSXElement; -} & TextOptions) { +export const SelectableText: ParentComponent< + { + selected: () => boolean; + primary?: boolean; + secondary?: boolean; + tertiary?: boolean; + } & TextOptions +> = (props) => { + const child = solidChildren(() => props.children); + return ( - {children} + {child()} ); -} +}; diff --git a/src/pages/Feed/FeedPage.tsx b/src/pages/Feed/FeedPage.tsx index db46213..4d3873e 100644 --- a/src/pages/Feed/FeedPage.tsx +++ b/src/pages/Feed/FeedPage.tsx @@ -5,6 +5,7 @@ import { createSignal, For, Show } from "solid-js"; import { useFeedStore } from "@/stores/feed"; +import { useKeyboard } from "@opentui/solid"; import { format } from "date-fns"; import type { Episode } from "@/types/episode"; import type { Feed } from "@/types/feed"; @@ -18,10 +19,14 @@ enum FeedPaneType { } export const FeedPaneCount = 1; +/** Episodes to load per batch */ +const ITEMS_PER_BATCH = 50; + export function FeedPage(props: PageProps) { const feedStore = useFeedStore(); const [selectedIndex, setSelectedIndex] = createSignal(0); const [isRefreshing, setIsRefreshing] = createSignal(false); + const [loadedEpisodesCount, setLoadedEpisodesCount] = createSignal(ITEMS_PER_BATCH); const allEpisodes = () => feedStore.getAllEpisodesChronological(); @@ -29,9 +34,14 @@ export function FeedPage(props: PageProps) { return format(date, "MMM d, yyyy"); }; + const paginatedEpisodes = () => { + const episodes = allEpisodes(); + return episodes.slice(0, loadedEpisodesCount()); + }; + const episodesByDate = () => { const groups: Record = {}; - const sortedEpisodes = allEpisodes(); + const sortedEpisodes = paginatedEpisodes(); for (const episode of sortedEpisodes) { const dateKey = formatDate(new Date(episode.episode.pubDate)); @@ -54,6 +64,11 @@ export function FeedPage(props: PageProps) { setIsRefreshing(false); }; + const handleScrollDown = async () => { + if (feedStore.isLoadingMore() || !feedStore.hasMoreEpisodes()) return; + await feedStore.loadMoreEpisodes(); + }; + const { theme } = useTheme(); return ( + {/* Loading indicator */} + + + Loading more episodes... + + diff --git a/src/utils/system-theme.ts b/src/utils/system-theme.ts index d9d4ee9..2fc4420 100644 --- a/src/utils/system-theme.ts +++ b/src/utils/system-theme.ts @@ -64,6 +64,19 @@ export function generateSystemTheme( const diffAddedLineNumberBg = tint(grays[3], ansi.green, 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 { theme: { primary: ansi.cyan, @@ -75,6 +88,12 @@ export function generateSystemTheme( info: ansi.cyan, text: fg, textMuted, + textPrimary: fg, + textSecondary: textMuted, + textTertiary: textMuted, + textSelectedPrimary: selectedPrimary, + textSelectedSecondary: selectedSecondary, + textSelectedTertiary: selectedTertiary, selectedListItemText: bg, background: transparent, backgroundPanel: grays[2],