diff --git a/src/App.tsx b/src/App.tsx index 07203ae..c9489f7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import { createSignal, createMemo, ErrorBoundary, Accessor } from "solid-js"; +import { createMemo, ErrorBoundary, Accessor } from "solid-js"; import { useKeyboard, useSelectionHandler } from "@opentui/solid"; import { TabNavigation } from "./components/TabNavigation"; import { CodeValidation } from "@/components/CodeValidation"; @@ -15,25 +15,13 @@ import type { Episode } from "@/types/episode"; import { DIRECTION, LayerGraph, TABS, LayerDepths } from "./utils/navigation"; import { useTheme, ThemeProvider } from "./context/ThemeContext"; import { KeybindProvider, useKeybinds } from "./context/KeybindContext"; +import { NavigationProvider, useNavigation } from "./context/NavigationContext"; import { useAudioNavStore, AudioSource } from "./stores/audio-nav"; const DEBUG = import.meta.env.DEBUG; -export interface PageProps { - depth: Accessor; - focusedIndex?: Accessor | number; - focusedIndexValue?: number; -} - export function App() { - const [activeTab, setActiveTab] = createSignal(TABS.FEED); - const [activeDepth, setActiveDepth] = createSignal(0); // not fixed matrix size - const [authScreen, setAuthScreen] = createSignal("login"); - const [showAuthPanel, setShowAuthPanel] = createSignal(false); - const [inputFocused, setInputFocused] = createSignal(false); - const [layerDepth, setLayerDepth] = createSignal(0); - const [focusedIndex, setFocusedIndex] = createSignal(0); - + const nav = useNavigation(); const auth = useAuthStore(); const feedStore = useFeedStore(); const audio = useAudio(); @@ -44,15 +32,15 @@ export function App() { const audioNav = useAudioNavStore(); useMultimediaKeys({ - playerFocused: () => activeTab() === TABS.PLAYER && layerDepth() > 0, - inputFocused: () => inputFocused(), + playerFocused: () => nav.activeTab === TABS.PLAYER && nav.activeDepth > 0, + inputFocused: () => nav.inputFocused, hasEpisode: () => !!audio.currentEpisode(), }); const handlePlayEpisode = (episode: Episode) => { audio.play(episode); - setActiveTab(TABS.PLAYER); - setLayerDepth(1); + nav.setActiveTab(TABS.PLAYER); + nav.setActiveDepth(1); audioNav.setSource(AudioSource.FEED); }; @@ -86,159 +74,83 @@ 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); - - if (DEBUG) { - console.log("KeyEvent:", keyEvent); - console.log("Keybinds loaded:", { - up: keybind.keybinds.up, - down: keybind.keybinds.down, - left: keybind.keybinds.left, - right: keybind.keybinds.right, - }); - } - - if (isUp || isDown) { - const currentDepth = activeDepth(); - const maxDepth = LayerDepths[activeTab()]; - - console.log("Navigation:", { isUp, isDown, currentDepth, maxDepth }); - - // Navigate within current depth layer - if (currentDepth < maxDepth) { - const newIndex = isUp ? focusedIndex() - 1 : focusedIndex() + 1; - setFocusedIndex(Math.max(0, Math.min(newIndex, maxDepth))); - } - } - - // Horizontal movement - move within current layer - if (isLeft || isRight) { - const currentDepth = activeDepth(); - const maxDepth = LayerDepths[activeTab()]; - - if (currentDepth < maxDepth) { - const newIndex = isLeft ? focusedIndex() - 1 : focusedIndex() + 1; - setFocusedIndex(Math.max(0, Math.min(newIndex, maxDepth))); - } - } - - // Cycle through current depth + 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, + }); if (isCycle) { - const currentDepth = activeDepth(); - const maxDepth = LayerDepths[activeTab()]; - - if (currentDepth < maxDepth) { - const newIndex = (focusedIndex() + 1) % (maxDepth + 1); - setFocusedIndex(newIndex); - } } - // Increase depth - if (isDive) { - const currentDepth = activeDepth(); - const maxDepth = LayerDepths[activeTab()]; - - if (currentDepth < maxDepth) { - setActiveDepth(currentDepth + 1); - setFocusedIndex(0); - } - } - - // Decrease depth - if (isOut) { - const currentDepth = activeDepth(); - - if (currentDepth > 0) { - setActiveDepth(currentDepth - 1); - setFocusedIndex(0); - } - } - - if (isToggle) { - audio.togglePlayback(); - } - - if (isNext) { - audio.next(); - } - - if (isPrev) { - audio.prev(); - } - - if (isSeekForward) { - audio.seekRelative(15); - } - - if (isSeekBackward) { - audio.seekRelative(-15); - } - - // Quit application - if (isQuit) { - process.exit(0); - } + // only handling top }, { release: false }, ); return ( - - - ( - - - Error: {err?.message ?? String(err)} - {"\n"} - Press a number key (1-6) to switch tabs. - - - )} - > - {DEBUG && ( - - - - - - - - - - - - - - - - - - - - - - - - - - - )} - - - - {LayerGraph[activeTab()]({ - depth: activeDepth, - focusedIndex: focusedIndex(), - })} - {/** TODO: Contextual controls based on tab/depth**/} - - - - + ( + + + Error: {err?.message ?? String(err)} + {"\n"} + Press a number key (1-6) to switch tabs. + + + )} + > + {DEBUG && ( + + + + + + + + + + + + + + + + + + + + + + + + + + + )} + + + + {LayerGraph[nav.activeTab]()} + {/** TODO: Contextual controls based on tab/depth**/} + + ); } diff --git a/src/context/NavigationContext.tsx b/src/context/NavigationContext.tsx new file mode 100644 index 0000000..ec96fe7 --- /dev/null +++ b/src/context/NavigationContext.tsx @@ -0,0 +1,27 @@ +import { createSignal } from "solid-js"; +import { createSimpleContext } from "./helper"; +import { TABS } from "../utils/navigation"; + +export const { use: useNavigation, provider: NavigationProvider } = createSimpleContext({ + name: "Navigation", + init: () => { + const [activeTab, setActiveTab] = createSignal(TABS.FEED); + const [activeDepth, setActiveDepth] = createSignal(0); + const [inputFocused, setInputFocused] = createSignal(false); + + return { + get activeTab() { + return activeTab(); + }, + get activeDepth() { + return activeDepth(); + }, + get inputFocused() { + return inputFocused(); + }, + setActiveTab, + setActiveDepth, + setInputFocused, + }; + }, +}); diff --git a/src/index.tsx b/src/index.tsx index 99c8c9e..f26fd15 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -9,6 +9,7 @@ import { App } from "./App"; import { ThemeProvider } from "./context/ThemeContext"; import { ToastProvider, Toast } from "./ui/toast"; import { KeybindProvider } from "./context/KeybindContext"; +import { NavigationProvider } from "./context/NavigationContext"; import { DialogProvider } from "./ui/dialog"; import { CommandProvider } from "./ui/command"; @@ -24,12 +25,14 @@ render( - - - - - - + + + + + + + + diff --git a/src/pages/Discover/DiscoverPage.tsx b/src/pages/Discover/DiscoverPage.tsx index ffa9b86..342daf3 100644 --- a/src/pages/Discover/DiscoverPage.tsx +++ b/src/pages/Discover/DiscoverPage.tsx @@ -7,8 +7,8 @@ import { useKeyboard } from "@opentui/solid"; import { useDiscoverStore, DISCOVER_CATEGORIES } from "@/stores/discover"; import { useTheme } from "@/context/ThemeContext"; import { PodcastCard } from "./PodcastCard"; -import { PageProps } from "@/App"; import { SelectableBox, SelectableText } from "@/components/Selectable"; +import { useNavigation } from "@/context/NavigationContext"; enum DiscoverPagePaneType { CATEGORIES = 1, @@ -16,10 +16,11 @@ enum DiscoverPagePaneType { } export const DiscoverPaneCount = 2; -export function DiscoverPage(props: PageProps) { +export function DiscoverPage() { const discoverStore = useDiscoverStore(); const [showIndex, setShowIndex] = createSignal(0); const [categoryIndex, setCategoryIndex] = createSignal(0); + const nav = useNavigation(); const handleCategorySelect = (categoryId: string) => { discoverStore.setSelectedCategory(categoryId); @@ -48,35 +49,32 @@ export function DiscoverPage(props: PageProps) { > Categories: - - - {(category) => { - const isSelected = () => - discoverStore.selectedCategory() === category.id; + + + {(category) => { + const isSelected = () => + discoverStore.selectedCategory() === category.id; - return ( - handleCategorySelect(category.id)} - > - - {category.icon} {category.name} - - - ); - }} - - + return ( + handleCategorySelect(category.id)} + > + + {category.icon} {category.name} + + + ); + }} + + - false} - primary={props.depth() == DiscoverPagePaneType.SHOWS} - > + false} + primary={nav.activeDepth == DiscoverPagePaneType.SHOWS} + > Trending in{" "} {DISCOVER_CATEGORIES.find( (c) => c.id === discoverStore.selectedCategory(), @@ -102,7 +100,9 @@ export function DiscoverPage(props: PageProps) { {discoverStore.filteredPodcasts().length !== 0 ? ( Loading trending shows... ) : ( - No podcasts found in this category. + + No podcasts found in this category. + )} } @@ -119,7 +119,7 @@ export function DiscoverPage(props: PageProps) { podcast={podcast} selected={ index() === showIndex() && - props.depth() == DiscoverPagePaneType.SHOWS + nav.activeDepth == DiscoverPagePaneType.SHOWS } onSelect={() => handleShowSelect(index())} onSubscribe={() => handleSubscribe(podcast)} diff --git a/src/pages/Feed/FeedPage.tsx b/src/pages/Feed/FeedPage.tsx index 3d3de33..f27203d 100644 --- a/src/pages/Feed/FeedPage.tsx +++ b/src/pages/Feed/FeedPage.tsx @@ -10,9 +10,9 @@ import { format } from "date-fns"; import type { Episode } from "@/types/episode"; import type { Feed } from "@/types/feed"; import { useTheme } from "@/context/ThemeContext"; -import { PageProps } from "@/App"; import { SelectableBox, SelectableText } from "@/components/Selectable"; import { se } from "date-fns/locale"; +import { useNavigation } from "@/context/NavigationContext"; enum FeedPaneType { FEED = 1, @@ -22,10 +22,12 @@ export const FeedPaneCount = 1; /** Episodes to load per batch */ const ITEMS_PER_BATCH = 50; -export function FeedPage(props: PageProps) { +export function FeedPage() { const feedStore = useFeedStore(); const [isRefreshing, setIsRefreshing] = createSignal(false); - const [loadedEpisodesCount, setLoadedEpisodesCount] = createSignal(ITEMS_PER_BATCH); + const [loadedEpisodesCount, setLoadedEpisodesCount] = + createSignal(ITEMS_PER_BATCH); + const nav = useNavigation(); const allEpisodes = () => feedStore.getAllEpisodesChronological(); @@ -86,15 +88,14 @@ export function FeedPage(props: PageProps) { } > - + b.localeCompare(a), )} > {([date, episode], groupIndex) => { - const index = typeof props.focusedIndex === 'function' ? props.focusedIndex() : props.focusedIndex; - const selected = () => groupIndex() === index; + const selected = () => groupIndex() === 1; // TODO: Manage selections locally return ( <> theme.muted || theme.text; + const nav = useNavigation(); /** Threshold: load more when within this many items of the end */ const LOAD_MORE_THRESHOLD = 5; @@ -34,9 +37,7 @@ export function MyShowsPage(props: PageProps) { const shows = () => feedStore.getFilteredFeeds(); const selectedShow = createMemo(() => { - const s = shows(); - const index = typeof props.focusedIndex === 'function' ? props.focusedIndex() : props.focusedIndex; - return index < s.length ? s[index] : undefined; + return shows()[0]; //TODO: Integrate with locally handled keyboard navigation }); const episodes = createMemo(() => { @@ -128,7 +129,7 @@ export function MyShowsPage(props: PageProps) { > {(feed, index) => ( @@ -143,7 +144,10 @@ export function MyShowsPage(props: PageProps) { onMouseDown={() => { setShowIndex(index()); setEpisodeIndex(0); - audioNav.setSource(AudioSource.MY_SHOWS, selectedShow()?.podcast.id); + audioNav.setSource( + AudioSource.MY_SHOWS, + selectedShow()?.podcast.id, + ); }} > {(episode, index) => ( diff --git a/src/pages/Player/PlayerPage.tsx b/src/pages/Player/PlayerPage.tsx index e4c54c7..273a975 100644 --- a/src/pages/Player/PlayerPage.tsx +++ b/src/pages/Player/PlayerPage.tsx @@ -1,4 +1,3 @@ -import { PageProps } from "@/App"; import { PlaybackControls } from "./PlaybackControls"; import { RealtimeWaveform } from "./RealtimeWaveform"; import { useAudio } from "@/hooks/useAudio"; @@ -10,7 +9,7 @@ enum PlayerPaneType { } export const PlayerPaneCount = 1; -export function PlayerPage(props: PageProps) { +export function PlayerPage() { const audio = useAudio(); const { theme } = useTheme(); @@ -40,7 +39,13 @@ export function PlayerPage(props: PageProps) { {audio.error() && {audio.error()}} - + {audio.currentEpisode()?.title} diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index da70575..fdab0e1 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -8,9 +8,9 @@ import { useSearchStore } from "@/stores/search"; import { SearchResults } from "./SearchResults"; import { SearchHistory } from "./SearchHistory"; import type { SearchResult } from "@/types/source"; -import { PageProps } from "@/App"; import { MyShowsPage } from "../MyShows/MyShowsPage"; import { useTheme } from "@/context/ThemeContext"; +import { useNavigation } from "@/context/NavigationContext"; enum SearchPaneType { INPUT = 1, @@ -19,19 +19,13 @@ enum SearchPaneType { } export const SearchPaneCount = 3; -export function SearchPage(props: PageProps) { +export function SearchPage() { const searchStore = useSearchStore(); const [inputValue, setInputValue] = createSignal(""); const [resultIndex, setResultIndex] = createSignal(0); const [historyIndex, setHistoryIndex] = createSignal(0); const { theme } = useTheme(); - - // Keep parent informed about input focus state - // TODO: have a global input focused prop in useKeyboard hook - //createEffect(() => { - //const isInputFocused = props.focused && focusArea() === "input"; - //props.onInputFocusChange?.(isInputFocused); - //}); + const nav = useNavigation(); const handleSearch = async () => { const query = inputValue().trim(); @@ -75,7 +69,7 @@ export function SearchPage(props: PageProps) { setInputValue(value); }} placeholder="Enter podcast name, topic, or author..." - focused={props.depth() === SearchPaneType.INPUT} + focused={nav.activeDepth === SearchPaneType.INPUT} width={50} /> - {/* Main Content - Results or History */} - - {/* Results Panel */} - - - - Results ({searchStore.results().length}) - - - 0} - fallback={ - - - {searchStore.query() - ? "No results found" - : "Enter a search term to find podcasts"} - - + {/* Main Content - Results or History */} + + {/* Results Panel */} + + + + Results ({searchStore.results().length}) + + + 0} + fallback={ + + + {searchStore.query() + ? "No results found" + : "Enter a search term to find podcasts"} + + + } + > History @@ -146,7 +153,7 @@ export function SearchPage(props: PageProps) { = [ { id: SettingsPaneType.ACCOUNT, label: "Account" }, ]; -export function SettingsPage(props: PageProps) { +export function SettingsPage() { const { theme } = useTheme(); - const [activeSection, setActiveSection] = createSignal( - SettingsPaneType.SYNC, - ); + const nav = useNavigation(); return ( @@ -40,13 +38,13 @@ export function SettingsPage(props: PageProps) { borderColor={theme.border} padding={0} backgroundColor={ - activeSection() === section.id ? theme.primary : undefined + nav.activeDepth === section.id ? theme.primary : undefined } - onMouseDown={() => setActiveSection(section.id)} + onMouseDown={() => nav.setActiveDepth(section.id)} > [{index() + 1}] {section.label} @@ -56,18 +54,25 @@ export function SettingsPage(props: PageProps) { - - {activeSection() === SettingsPaneType.SYNC && } - {activeSection() === SettingsPaneType.SOURCES && ( + + {nav.activeDepth === SettingsPaneType.SYNC && } + {nav.activeDepth === SettingsPaneType.SOURCES && ( )} - {activeSection() === SettingsPaneType.PREFERENCES && ( + {nav.activeDepth === SettingsPaneType.PREFERENCES && ( )} - {activeSection() === SettingsPaneType.VISUALIZER && ( + {nav.activeDepth === SettingsPaneType.VISUALIZER && ( )} - {activeSection() === SettingsPaneType.ACCOUNT && ( + {nav.activeDepth === SettingsPaneType.ACCOUNT && ( Account