From 8d350d9eb5ee8685b5221f4983ff85fb612e082c Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Fri, 20 Feb 2026 01:13:32 -0500 Subject: [PATCH] navigation controls + starting indicators --- src/App.tsx | 59 +++++++++------- src/components/TabNavigation.tsx | 46 ++++++------- src/config/keybind.jsonc | 4 +- src/context/KeybindContext.tsx | 17 ++++- src/pages/Feed/FeedPage.tsx | 107 ++++++++++++++---------------- src/utils/keybinds-persistence.ts | 2 +- 6 files changed, 128 insertions(+), 107 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index a5a24d9..cff5e81 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -28,7 +28,15 @@ export function App() { const audio = useAudio(); const toast = useToast(); const renderer = useRenderer(); - const { theme } = useTheme(); + const themeContext = useTheme(); + const theme = themeContext.theme; + + // Create a reactive expression for background color + const backgroundColor = () => { + return themeContext.selected === "system" + ? "transparent" + : themeContext.theme.surface; + }; const keybind = useKeybinds(); const audioNav = useAudioNavStore(); @@ -62,11 +70,11 @@ export function App() { useKeyboard( (keyEvent) => { + const isCycle = keybind.match("cycle", keyEvent); const isUp = keybind.match("up", keyEvent); const isDown = keybind.match("down", keyEvent); const isLeft = keybind.match("left", keyEvent); const isRight = keybind.match("right", keyEvent); - const isCycle = keybind.match("cycle", keyEvent); const isDive = keybind.match("dive", keyEvent); const isOut = keybind.match("out", keyEvent); const isToggle = keybind.match("audio-toggle", keyEvent); @@ -75,27 +83,31 @@ export function App() { const isSeekForward = keybind.match("audio-seek-forward", keyEvent); const isSeekBackward = keybind.match("audio-seek-backward", keyEvent); const isQuit = keybind.match("quit", keyEvent); - console.log({ - up: isUp, - down: isDown, - left: isLeft, - right: isRight, - cycle: isCycle, - dive: isDive, - out: isOut, - audioToggle: isToggle, - audioNext: isNext, - audioPrev: isPrev, - audioSeekForward: isSeekForward, - audioSeekBackward: isSeekBackward, - quit: isQuit, - }); + const isInverting = keybind.isInverting(keyEvent); // only handling top navigation here, cycle through tabs, just to high priority(player) all else to be handled in each tab if (nav.activeDepth == 0) { - if (isCycle) { + if ( + (isCycle && !isInverting) || + (isDown && !isInverting) || + (isUp && isInverting) + ) { nav.nextTab(); + return; } + if ( + (isCycle && isInverting) || + (isDown && isInverting) || + (isUp && !isInverting) + ) { + nav.prevTab(); + return; + } + if ((isRight && !isInverting) || (isLeft && isInverting)) { + nav.setActiveDepth(1); + } + } + if (nav.activeDepth == 1) { } }, { release: false }, @@ -117,7 +129,11 @@ export function App() { flexDirection="column" width="100%" height="100%" - backgroundColor={theme.surface} + backgroundColor={ + themeContext.selected === "system" + ? "transparent" + : themeContext.theme.surface + } > {DEBUG && ( @@ -149,10 +165,7 @@ export function App() { )} - + {LayerGraph[nav.activeTab]()} diff --git a/src/components/TabNavigation.tsx b/src/components/TabNavigation.tsx index f3266a2..7244b3b 100644 --- a/src/components/TabNavigation.tsx +++ b/src/components/TabNavigation.tsx @@ -1,12 +1,8 @@ import { useTheme } from "@/context/ThemeContext"; -import { TABS } from "@/utils/navigation"; +import { TABS, TabsCount } from "@/utils/navigation"; import { For } from "solid-js"; import { SelectableBox, SelectableText } from "@/components/Selectable"; - -interface TabNavigationProps { - activeTab: TABS; - onTabSelect: (tab: TABS) => void; -} +import { useNavigation } from "@/context/NavigationContext"; export const tabs: TabDefinition[] = [ { id: TABS.FEED, label: "Feed" }, @@ -17,32 +13,36 @@ export const tabs: TabDefinition[] = [ { id: TABS.SETTINGS, label: "Settings" }, ]; -export function TabNavigation(props: TabNavigationProps) { +export function TabNavigation() { const { theme } = useTheme(); + const { activeTab, setActiveTab, activeDepth } = useNavigation(); return ( {(tab) => ( - tab.id == props.activeTab} - onMouseDown={() => props.onTabSelect(tab.id)} - > - tab.id == props.activeTab} - primary - alignSelf="center" - > - {tab.label} - - + tab.id == activeTab} + onMouseDown={() => setActiveTab(tab.id)} + > + tab.id == activeTab} + primary + alignSelf="center" + > + {tab.label} + + )} diff --git a/src/config/keybind.jsonc b/src/config/keybind.jsonc index 6e85465..f607a97 100644 --- a/src/config/keybind.jsonc +++ b/src/config/keybind.jsonc @@ -6,10 +6,10 @@ "cycle": ["tab"], // this will cycle no matter the depth/orientation "dive": ["return"], "out": ["esc"], - "inverse": ["shift"], + "inverseModifier": ["shift"], "leader": ":", // will not trigger while focused on input "quit": ["q"], - "refresh": ["r"], + "refresh": ["r"], "audio-toggle": ["p"], "audio-pause": [], "audio-play": [], diff --git a/src/context/KeybindContext.tsx b/src/context/KeybindContext.tsx index 248645f..5f2c781 100644 --- a/src/context/KeybindContext.tsx +++ b/src/context/KeybindContext.tsx @@ -15,7 +15,7 @@ export type KeybindsResolved = { cycle: string[]; // this will cycle no matter the depth/orientation dive: string[]; out: string[]; - inverse: string[]; + inverseModifier: string; leader: string; // will not trigger while focused on input quit: string[]; "audio-toggle": string[]; @@ -57,7 +57,7 @@ export const { use: useKeybinds, provider: KeybindProvider } = cycle: [], dive: [], out: [], - inverse: [], + inverseModifier: "", leader: "", quit: [], refresh: [], @@ -100,6 +100,18 @@ export const { use: useKeybinds, provider: KeybindProvider } = return false; } + function isInverting(evt: { + name: string; + ctrl?: boolean; + meta?: boolean; + shift?: boolean; + }) { + if (store.inverseModifier === "ctrl" && evt.ctrl) return true; + if (store.inverseModifier === "meta" && evt.meta) return true; + if (store.inverseModifier === "shift" && evt.shift) return true; + return false; + } + // Load on mount onMount(() => { load().catch(() => {}); @@ -115,6 +127,7 @@ export const { use: useKeybinds, provider: KeybindProvider } = save, print, match, + isInverting, }; }, }); diff --git a/src/pages/Feed/FeedPage.tsx b/src/pages/Feed/FeedPage.tsx index a0182ec..98c2b76 100644 --- a/src/pages/Feed/FeedPage.tsx +++ b/src/pages/Feed/FeedPage.tsx @@ -12,6 +12,7 @@ import { useTheme } from "@/context/ThemeContext"; import { SelectableBox, SelectableText } from "@/components/Selectable"; import { useNavigation } from "@/context/NavigationContext"; import { LoadingIndicator } from "@/components/LoadingIndicator"; +import { TABS } from "@/utils/navigation"; enum FeedPaneType { FEED = 1, @@ -23,27 +24,21 @@ const ITEMS_PER_BATCH = 50; export function FeedPage() { const feedStore = useFeedStore(); - const [isRefreshing, setIsRefreshing] = createSignal(false); - const [loadedEpisodesCount, setLoadedEpisodesCount] = - createSignal(ITEMS_PER_BATCH); const nav = useNavigation(); - + const { theme } = useTheme(); + const [selectedEpisodeID, setSelectedEpisodeID] = createSignal< + string | undefined + >(); const allEpisodes = () => feedStore.getAllEpisodesChronological(); - const paginatedEpisodes = () => { - const episodes = allEpisodes(); - return episodes.slice(0, loadedEpisodesCount()); - }; - const formatDate = (date: Date): string => { return format(date, "MMM d, yyyy"); }; const groupEpisodesByDate = () => { const groups: Record> = {}; - const episodes = paginatedEpisodes(); - for (const item of episodes) { + for (const item of allEpisodes()) { const dateKey = formatDate(new Date(item.episode.pubDate)); if (!groups[dateKey]) { groups[dateKey] = []; @@ -51,14 +46,13 @@ export function FeedPage() { groups[dateKey].push(item); } - return Object.entries(groups) - .sort(([a, _aItems], [b, _bItems]) => { - // Convert date strings back to Date objects for proper chronological sorting - const dateA = new Date(a); - const dateB = new Date(b); - // Sort in descending order (newest first) - return dateB.getTime() - dateA.getTime(); - }); + return Object.entries(groups).sort(([a, _aItems], [b, _bItems]) => { + // Convert date strings back to Date objects for proper chronological sorting + const dateA = new Date(a); + const dateB = new Date(b); + // Sort in descending order (newest first) + return dateB.getTime() - dateA.getTime(); + }); }; const formatDuration = (seconds: number): string => { @@ -68,7 +62,6 @@ export function FeedPage() { return `${mins}m`; }; - const { theme } = useTheme(); return ( - {/* Status line */} - - Refreshing feeds... - - 0} fallback={ @@ -99,42 +87,49 @@ export function FeedPage() { {date} - {(item) => ( - false} - flexDirection="column" - gap={0} - paddingLeft={1} - paddingRight={1} - paddingTop={0} - paddingBottom={0} - onMouseDown={() => { - // Selection is handled by App's keyboard navigation - }} - > - false} primary> - {item.episode.title} - - - false} primary> - {item.feed.podcast.title} + {(item) => { + const isSelected = () => { + if ( + nav.activeTab == TABS.FEED && + nav.activeDepth == FeedPaneType.FEED && + selectedEpisodeID() && + selectedEpisodeID() === item.episode.id + ) { + return true; + } + return false; + }; + return ( + { + // Selection is handled by App's keyboard navigation + }} + > + + {item.episode.title} - false} tertiary> - {formatDuration(item.episode.duration)} - - - - )} + + + {item.feed.podcast.title} + + + {formatDuration(item.episode.duration)} + + + + ); + }} )} - {/* Loading indicator */} - - - - - diff --git a/src/utils/keybinds-persistence.ts b/src/utils/keybinds-persistence.ts index 378597b..f30f2f2 100644 --- a/src/utils/keybinds-persistence.ts +++ b/src/utils/keybinds-persistence.ts @@ -28,7 +28,7 @@ const DEFAULT_KEYBINDS: KeybindsResolved = { cycle: ["tab"], dive: ["return"], out: ["esc"], - inverse: ["shift"], + inverseModifier: "shift", leader: ":", quit: ["q"], "audio-toggle": ["p"],