diff --git a/src/pages/Discover/DiscoverPage.tsx b/src/pages/Discover/DiscoverPage.tsx index f1969f0..ed2f227 100644 --- a/src/pages/Discover/DiscoverPage.tsx +++ b/src/pages/Discover/DiscoverPage.tsx @@ -2,13 +2,14 @@ * DiscoverPage component - Main discover/browse interface for PodTUI */ -import { createSignal, For, Show } from "solid-js"; +import { createSignal, For, Show, onMount } from "solid-js"; import { useKeyboard } from "@opentui/solid"; import { useDiscoverStore, DISCOVER_CATEGORIES } from "@/stores/discover"; import { useTheme } from "@/context/ThemeContext"; import { PodcastCard } from "./PodcastCard"; import { SelectableBox, SelectableText } from "@/components/Selectable"; import { useNavigation } from "@/context/NavigationContext"; +import { KeybindProvider, useKeybinds } from "@/context/KeybindContext"; enum DiscoverPagePaneType { CATEGORIES = 1, @@ -21,6 +22,36 @@ export function DiscoverPage() { const [showIndex, setShowIndex] = createSignal(0); const [categoryIndex, setCategoryIndex] = createSignal(0); const nav = useNavigation(); + const keybind = useKeybinds(); + + onMount(() => { + useKeyboard( + (keyEvent: any) => { + const isDown = keybind.match("down", keyEvent); + const isUp = keybind.match("up", keyEvent); + const isEnter = keyEvent.name === "Enter" || keyEvent.name === " "; + const isSpace = keyEvent.name === " "; + + if (isEnter || isSpace) { + const filteredPodcasts = discoverStore.filteredPodcasts(); + if (filteredPodcasts.length > 0 && showIndex() < filteredPodcasts.length) { + setShowIndex(showIndex() + 1); + } + return; + } + + const filteredPodcasts = discoverStore.filteredPodcasts(); + if (filteredPodcasts.length === 0) return; + + if (isDown && showIndex() < filteredPodcasts.length - 1) { + setShowIndex(showIndex() + 1); + } else if (isUp && showIndex() > 0) { + setShowIndex(showIndex() - 1); + } + }, + { release: false }, + ); + }); const handleCategorySelect = (categoryId: string) => { discoverStore.setSelectedCategory(categoryId); diff --git a/src/pages/Feed/FeedPage.tsx b/src/pages/Feed/FeedPage.tsx index 9fb9ce4..e9a7976 100644 --- a/src/pages/Feed/FeedPage.tsx +++ b/src/pages/Feed/FeedPage.tsx @@ -3,7 +3,7 @@ * Reverse chronological order, grouped by date */ -import { createSignal, For, Show } from "solid-js"; +import { createSignal, For, Show, onMount } from "solid-js"; import { useFeedStore } from "@/stores/feed"; import { format } from "date-fns"; import type { Episode } from "@/types/episode"; @@ -13,6 +13,8 @@ import { SelectableBox, SelectableText } from "@/components/Selectable"; import { useNavigation } from "@/context/NavigationContext"; import { LoadingIndicator } from "@/components/LoadingIndicator"; import { TABS } from "@/utils/navigation"; +import { useKeyboard } from "@opentui/solid"; +import { KeybindProvider, useKeybinds } from "@/context/KeybindContext"; enum FeedPaneType { FEED = 1, @@ -29,6 +31,37 @@ export function FeedPage() { string | undefined >(); const allEpisodes = () => feedStore.getAllEpisodesChronological(); + const keybind = useKeybinds(); + const [focusedIndex, setFocusedIndex] = createSignal(0); + + onMount(() => { + useKeyboard( + (keyEvent: any) => { + const isDown = keybind.match("down", keyEvent); + const isUp = keybind.match("up", keyEvent); + const isEnter = keyEvent.name === "Enter" || keyEvent.name === " "; + const isSpace = keyEvent.name === " "; + + if (isEnter || isSpace) { + const episodes = allEpisodes(); + if (episodes.length > 0 && episodes[focusedIndex()]) { + setSelectedEpisodeID(episodes[focusedIndex()].episode.id); + } + return; + } + + const episodes = allEpisodes(); + if (episodes.length === 0) return; + + if (isDown && focusedIndex() < episodes.length - 1) { + setFocusedIndex(focusedIndex() + 1); + } else if (isUp && focusedIndex() > 0) { + setFocusedIndex(focusedIndex() - 1); + } + }, + { release: false }, + ); + }); const formatDate = (date: Date): string => { return format(date, "MMM d, yyyy"); @@ -105,6 +138,13 @@ export function FeedPage() { } return false; }; + const isFocused = () => { + const episodes = allEpisodes(); + const currentIndex = episodes.findIndex( + (e: any) => e.episode.id === item.episode.id, + ); + return currentIndex === focusedIndex(); + }; return ( { - // Selection is handled by App's keyboard navigation + setSelectedEpisodeID(item.episode.id); + const episodes = allEpisodes(); + setFocusedIndex( + episodes.findIndex((e: any) => e.episode.id === item.episode.id), + ); }} > diff --git a/src/pages/MyShows/MyShowsPage.tsx b/src/pages/MyShows/MyShowsPage.tsx index 65c0d18..dd90a45 100644 --- a/src/pages/MyShows/MyShowsPage.tsx +++ b/src/pages/MyShows/MyShowsPage.tsx @@ -4,7 +4,8 @@ * Right panel: episodes for the selected show */ -import { createSignal, For, Show, createMemo, createEffect } from "solid-js"; +import { createSignal, For, Show, createMemo, createEffect, onMount } from "solid-js"; +import { useKeyboard } from "@opentui/solid"; import { useFeedStore } from "@/stores/feed"; import { useDownloadStore } from "@/stores/download"; import { DownloadStatus } from "@/types/episode"; @@ -32,6 +33,50 @@ export function MyShowsPage() { const mutedColor = () => theme.muted || theme.text; const nav = useNavigation(); + onMount(() => { + useKeyboard( + (keyEvent: any) => { + const isDown = + keyEvent.key === "j" || keyEvent.key === "ArrowDown"; + const isUp = + keyEvent.key === "k" || keyEvent.key === "ArrowUp"; + const isSelect = + keyEvent.key === "Enter" || keyEvent.key === " "; + + const shows = feedStore.getFilteredFeeds(); + const episodesList = episodes(); + const selected = selectedShow(); + + if (isSelect) { + if (shows.length > 0 && showIndex() < shows.length) { + setShowIndex(showIndex() + 1); + } + if (episodesList.length > 0 && episodeIndex() < episodesList.length) { + setEpisodeIndex(episodeIndex() + 1); + } + return; + } + + if (shows.length > 0) { + if (isDown && showIndex() < shows.length - 1) { + setShowIndex(showIndex() + 1); + } else if (isUp && showIndex() > 0) { + setShowIndex(showIndex() - 1); + } + } + + if (episodesList.length > 0) { + if (isDown && episodeIndex() < episodesList.length - 1) { + setEpisodeIndex(episodeIndex() + 1); + } else if (isUp && episodeIndex() > 0) { + setEpisodeIndex(episodeIndex() - 1); + } + } + }, + { release: false }, + ); + }); + /** Threshold: load more when within this many items of the end */ const LOAD_MORE_THRESHOLD = 5; diff --git a/src/pages/Player/PlayerPage.tsx b/src/pages/Player/PlayerPage.tsx index 3c60721..2f43b6c 100644 --- a/src/pages/Player/PlayerPage.tsx +++ b/src/pages/Player/PlayerPage.tsx @@ -4,6 +4,8 @@ import { useAudio } from "@/hooks/useAudio"; import { useAppStore } from "@/stores/app"; import { useTheme } from "@/context/ThemeContext"; import { useNavigation } from "@/context/NavigationContext"; +import { useKeyboard } from "@opentui/solid"; +import { onMount } from "solid-js"; enum PlayerPaneType { PLAYER = 1, @@ -15,6 +17,32 @@ export function PlayerPage() { const { theme } = useTheme(); const nav = useNavigation(); + onMount(() => { + useKeyboard( + (keyEvent: any) => { + const isNext = keyEvent.key === "l" || keyEvent.key === "ArrowRight"; + const isPrev = keyEvent.key === "h" || keyEvent.key === "ArrowLeft"; + const isPlayPause = keyEvent.key === " " || keyEvent.key === "Enter"; + + if (isPlayPause) { + audio.togglePlayback(); + return; + } + + if (isNext) { + audio.seek(audio.currentEpisode()?.duration ?? 0); + return; + } + + if (isPrev) { + audio.seek(0); + return; + } + }, + { release: false }, + ); + }); + const progressPercent = () => { const d = audio.duration(); if (d <= 0) return 0; diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 7a7197d..764ad79 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -2,7 +2,7 @@ * SearchPage component - Main search interface for PodTUI */ -import { createSignal, createEffect, Show } from "solid-js"; +import { createSignal, createEffect, Show, onMount } from "solid-js"; import { useKeyboard } from "@opentui/solid"; import { useSearchStore } from "@/stores/search"; import { SearchResults } from "./SearchResults"; @@ -27,6 +27,37 @@ export function SearchPage() { const { theme } = useTheme(); const nav = useNavigation(); + onMount(() => { + useKeyboard( + (keyEvent: any) => { + const isDown = + keyEvent.key === "j" || keyEvent.key === "ArrowDown"; + const isUp = + keyEvent.key === "k" || keyEvent.key === "ArrowUp"; + const isSelect = + keyEvent.key === "Enter" || keyEvent.key === " "; + + if (isSelect) { + const results = searchStore.results(); + if (results.length > 0 && resultIndex() < results.length) { + setResultIndex(resultIndex() + 1); + } + return; + } + + const results = searchStore.results(); + if (results.length === 0) return; + + if (isDown && resultIndex() < results.length - 1) { + setResultIndex(resultIndex() + 1); + } else if (isUp && resultIndex() > 0) { + setResultIndex(resultIndex() - 1); + } + }, + { release: false }, + ); + }); + const handleSearch = async () => { const query = inputValue().trim(); if (query) { diff --git a/src/pages/Settings/SettingsPage.tsx b/src/pages/Settings/SettingsPage.tsx index 27a0f12..82eb6a1 100644 --- a/src/pages/Settings/SettingsPage.tsx +++ b/src/pages/Settings/SettingsPage.tsx @@ -1,4 +1,4 @@ -import { createSignal, For } from "solid-js"; +import { createSignal, For, onMount } from "solid-js"; import { useKeyboard } from "@opentui/solid"; import { SourceManager } from "./SourceManager"; import { useTheme } from "@/context/ThemeContext"; @@ -33,6 +33,31 @@ export function SettingsPage() { return nav.activeDepth() === depth; }; + onMount(() => { + useKeyboard( + (keyEvent: any) => { + const isDown = + keyEvent.key === "j" || keyEvent.key === "ArrowDown"; + const isUp = + keyEvent.key === "k" || keyEvent.key === "ArrowUp"; + const isSelect = + keyEvent.key === "Enter" || keyEvent.key === " "; + + if (isSelect) { + nav.setActiveDepth((nav.activeDepth() % SettingsPaneCount) + 1); + return; + } + + const nextDepth = isDown + ? (nav.activeDepth() % SettingsPaneCount) + 1 + : (nav.activeDepth() - 2 + SettingsPaneCount) % SettingsPaneCount + 1; + + nav.setActiveDepth(nextDepth); + }, + { release: false }, + ); + }); + return (