redoing navigation logic to favor more local

This commit is contained in:
2026-02-19 15:59:50 -05:00
parent 8e0f90f449
commit 1c65c85d02
9 changed files with 230 additions and 266 deletions

View File

@@ -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 { useKeyboard, useSelectionHandler } from "@opentui/solid";
import { TabNavigation } from "./components/TabNavigation"; import { TabNavigation } from "./components/TabNavigation";
import { CodeValidation } from "@/components/CodeValidation"; import { CodeValidation } from "@/components/CodeValidation";
@@ -15,25 +15,13 @@ import type { Episode } from "@/types/episode";
import { DIRECTION, LayerGraph, TABS, LayerDepths } from "./utils/navigation"; import { DIRECTION, LayerGraph, TABS, LayerDepths } from "./utils/navigation";
import { useTheme, ThemeProvider } from "./context/ThemeContext"; import { useTheme, ThemeProvider } from "./context/ThemeContext";
import { KeybindProvider, useKeybinds } from "./context/KeybindContext"; import { KeybindProvider, useKeybinds } from "./context/KeybindContext";
import { NavigationProvider, useNavigation } from "./context/NavigationContext";
import { useAudioNavStore, AudioSource } from "./stores/audio-nav"; import { useAudioNavStore, AudioSource } from "./stores/audio-nav";
const DEBUG = import.meta.env.DEBUG; const DEBUG = import.meta.env.DEBUG;
export interface PageProps {
depth: Accessor<number>;
focusedIndex?: Accessor<number> | number;
focusedIndexValue?: number;
}
export function App() { export function App() {
const [activeTab, setActiveTab] = createSignal<TABS>(TABS.FEED); const nav = useNavigation();
const [activeDepth, setActiveDepth] = createSignal(0); // not fixed matrix size
const [authScreen, setAuthScreen] = createSignal<AuthScreen>("login");
const [showAuthPanel, setShowAuthPanel] = createSignal(false);
const [inputFocused, setInputFocused] = createSignal(false);
const [layerDepth, setLayerDepth] = createSignal(0);
const [focusedIndex, setFocusedIndex] = createSignal(0);
const auth = useAuthStore(); const auth = useAuthStore();
const feedStore = useFeedStore(); const feedStore = useFeedStore();
const audio = useAudio(); const audio = useAudio();
@@ -44,15 +32,15 @@ export function App() {
const audioNav = useAudioNavStore(); const audioNav = useAudioNavStore();
useMultimediaKeys({ useMultimediaKeys({
playerFocused: () => activeTab() === TABS.PLAYER && layerDepth() > 0, playerFocused: () => nav.activeTab === TABS.PLAYER && nav.activeDepth > 0,
inputFocused: () => inputFocused(), inputFocused: () => nav.inputFocused,
hasEpisode: () => !!audio.currentEpisode(), hasEpisode: () => !!audio.currentEpisode(),
}); });
const handlePlayEpisode = (episode: Episode) => { const handlePlayEpisode = (episode: Episode) => {
audio.play(episode); audio.play(episode);
setActiveTab(TABS.PLAYER); nav.setActiveTab(TABS.PLAYER);
setLayerDepth(1); nav.setActiveDepth(1);
audioNav.setSource(AudioSource.FEED); audioNav.setSource(AudioSource.FEED);
}; };
@@ -86,159 +74,83 @@ export function App() {
const isSeekForward = keybind.match("audio-seek-forward", keyEvent); const isSeekForward = keybind.match("audio-seek-forward", keyEvent);
const isSeekBackward = keybind.match("audio-seek-backward", keyEvent); const isSeekBackward = keybind.match("audio-seek-backward", keyEvent);
const isQuit = keybind.match("quit", keyEvent); const isQuit = keybind.match("quit", keyEvent);
console.log({
if (DEBUG) { up: isUp,
console.log("KeyEvent:", keyEvent); down: isDown,
console.log("Keybinds loaded:", { left: isLeft,
up: keybind.keybinds.up, right: isRight,
down: keybind.keybinds.down, cycle: isCycle,
left: keybind.keybinds.left, dive: isDive,
right: keybind.keybinds.right, out: isOut,
}); audioToggle: isToggle,
} audioNext: isNext,
audioPrev: isPrev,
if (isUp || isDown) { audioSeekForward: isSeekForward,
const currentDepth = activeDepth(); audioSeekBackward: isSeekBackward,
const maxDepth = LayerDepths[activeTab()]; quit: isQuit,
});
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
if (isCycle) { if (isCycle) {
const currentDepth = activeDepth();
const maxDepth = LayerDepths[activeTab()];
if (currentDepth < maxDepth) {
const newIndex = (focusedIndex() + 1) % (maxDepth + 1);
setFocusedIndex(newIndex);
}
} }
// Increase depth // only handling top
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);
}
}, },
{ release: false }, { release: false },
); );
return ( return (
<KeybindProvider> <ErrorBoundary
<ThemeProvider mode="dark"> fallback={(err) => (
<ErrorBoundary <box border padding={2} borderColor={theme.error}>
fallback={(err) => ( <text fg={theme.error}>
<box border padding={2} borderColor={theme.error}> Error: {err?.message ?? String(err)}
<text fg={theme.error}> {"\n"}
Error: {err?.message ?? String(err)} Press a number key (1-6) to switch tabs.
{"\n"} </text>
Press a number key (1-6) to switch tabs. </box>
</text> )}
</box> >
)} {DEBUG && (
> <box flexDirection="row" width="100%" height={1}>
{DEBUG && ( <text fg={theme.primary}></text>
<box flexDirection="row" width="100%" height={1}> <text fg={theme.secondary}></text>
<text fg={theme.primary}></text> <text fg={theme.accent}></text>
<text fg={theme.secondary}></text> <text fg={theme.error}></text>
<text fg={theme.accent}></text> <text fg={theme.warning}></text>
<text fg={theme.error}></text> <text fg={theme.success}></text>
<text fg={theme.warning}></text> <text fg={theme.info}></text>
<text fg={theme.success}></text> <text fg={theme.text}></text>
<text fg={theme.info}></text> <text fg={theme.textMuted}></text>
<text fg={theme.text}></text> <text fg={theme.surface}></text>
<text fg={theme.textMuted}></text> <text fg={theme.background}></text>
<text fg={theme.surface}></text> <text fg={theme.border}></text>
<text fg={theme.background}></text> <text fg={theme.borderActive}></text>
<text fg={theme.border}></text> <text fg={theme.diffAdded}></text>
<text fg={theme.borderActive}></text> <text fg={theme.diffRemoved}></text>
<text fg={theme.diffAdded}></text> <text fg={theme.diffContext}></text>
<text fg={theme.diffRemoved}></text> <text fg={theme.markdownText}></text>
<text fg={theme.diffContext}></text> <text fg={theme.markdownHeading}></text>
<text fg={theme.markdownText}></text> <text fg={theme.markdownLink}></text>
<text fg={theme.markdownHeading}></text> <text fg={theme.markdownCode}></text>
<text fg={theme.markdownLink}></text> <text fg={theme.syntaxKeyword}></text>
<text fg={theme.markdownCode}></text> <text fg={theme.syntaxString}></text>
<text fg={theme.syntaxKeyword}></text> <text fg={theme.syntaxNumber}></text>
<text fg={theme.syntaxString}></text> <text fg={theme.syntaxFunction}></text>
<text fg={theme.syntaxNumber}></text> </box>
<text fg={theme.syntaxFunction}></text> )}
</box> <box flexDirection="row" width="100%" height={1} />
)} <box
<box flexDirection="row" width="100%" height={1} /> flexDirection="row"
<box width="100%"
flexDirection="row" height="100%"
width="100%" backgroundColor={theme.surface}
height="100%" >
backgroundColor={theme.surface} <TabNavigation
> activeTab={nav.activeTab}
<TabNavigation activeTab={activeTab()} onTabSelect={setActiveTab} /> onTabSelect={nav.setActiveTab}
{LayerGraph[activeTab()]({ />
depth: activeDepth, {LayerGraph[nav.activeTab]()}
focusedIndex: focusedIndex(), {/** TODO: Contextual controls based on tab/depth**/}
})} </box>
{/** TODO: Contextual controls based on tab/depth**/} </ErrorBoundary>
</box>
</ErrorBoundary>
</ThemeProvider>
</KeybindProvider>
); );
} }

View File

@@ -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>(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,
};
},
});

View File

@@ -9,6 +9,7 @@ import { App } from "./App";
import { ThemeProvider } from "./context/ThemeContext"; import { ThemeProvider } from "./context/ThemeContext";
import { ToastProvider, Toast } from "./ui/toast"; import { ToastProvider, Toast } from "./ui/toast";
import { KeybindProvider } from "./context/KeybindContext"; import { KeybindProvider } from "./context/KeybindContext";
import { NavigationProvider } from "./context/NavigationContext";
import { DialogProvider } from "./ui/dialog"; import { DialogProvider } from "./ui/dialog";
import { CommandProvider } from "./ui/command"; import { CommandProvider } from "./ui/command";
@@ -24,12 +25,14 @@ render(
<ToastProvider> <ToastProvider>
<ThemeProvider mode="dark"> <ThemeProvider mode="dark">
<KeybindProvider> <KeybindProvider>
<DialogProvider> <NavigationProvider>
<CommandProvider> <DialogProvider>
<App /> <CommandProvider>
<Toast /> <App />
</CommandProvider> <Toast />
</DialogProvider> </CommandProvider>
</DialogProvider>
</NavigationProvider>
</KeybindProvider> </KeybindProvider>
</ThemeProvider> </ThemeProvider>
</ToastProvider> </ToastProvider>

View File

@@ -7,8 +7,8 @@ import { useKeyboard } from "@opentui/solid";
import { useDiscoverStore, DISCOVER_CATEGORIES } from "@/stores/discover"; import { useDiscoverStore, DISCOVER_CATEGORIES } from "@/stores/discover";
import { useTheme } from "@/context/ThemeContext"; import { useTheme } from "@/context/ThemeContext";
import { PodcastCard } from "./PodcastCard"; import { PodcastCard } from "./PodcastCard";
import { PageProps } from "@/App";
import { SelectableBox, SelectableText } from "@/components/Selectable"; import { SelectableBox, SelectableText } from "@/components/Selectable";
import { useNavigation } from "@/context/NavigationContext";
enum DiscoverPagePaneType { enum DiscoverPagePaneType {
CATEGORIES = 1, CATEGORIES = 1,
@@ -16,10 +16,11 @@ enum DiscoverPagePaneType {
} }
export const DiscoverPaneCount = 2; export const DiscoverPaneCount = 2;
export function DiscoverPage(props: PageProps) { export function DiscoverPage() {
const discoverStore = useDiscoverStore(); const discoverStore = useDiscoverStore();
const [showIndex, setShowIndex] = createSignal(0); const [showIndex, setShowIndex] = createSignal(0);
const [categoryIndex, setCategoryIndex] = createSignal(0); const [categoryIndex, setCategoryIndex] = createSignal(0);
const nav = useNavigation();
const handleCategorySelect = (categoryId: string) => { const handleCategorySelect = (categoryId: string) => {
discoverStore.setSelectedCategory(categoryId); discoverStore.setSelectedCategory(categoryId);
@@ -48,35 +49,32 @@ export function DiscoverPage(props: PageProps) {
> >
<text <text
fg={ fg={
props.depth() == DiscoverPagePaneType.CATEGORIES nav.activeDepth == DiscoverPagePaneType.CATEGORIES
? theme.accent ? theme.accent
: theme.text : theme.text
} }
> >
Categories: Categories:
</text> </text>
<box flexDirection="column" gap={1}> <box flexDirection="column" gap={1}>
<For each={discoverStore.categories}> <For each={discoverStore.categories}>
{(category) => { {(category) => {
const isSelected = () => const isSelected = () =>
discoverStore.selectedCategory() === category.id; discoverStore.selectedCategory() === category.id;
return ( return (
<SelectableBox <SelectableBox
selected={isSelected} selected={isSelected}
onMouseDown={() => handleCategorySelect(category.id)} onMouseDown={() => handleCategorySelect(category.id)}
> >
<SelectableText <SelectableText selected={isSelected} primary>
selected={isSelected} {category.icon} {category.name}
primary </SelectableText>
> </SelectableBox>
{category.icon} {category.name} );
</SelectableText> }}
</SelectableBox> </For>
); </box>
}}
</For>
</box>
</box> </box>
<box <box
flexDirection="column" flexDirection="column"
@@ -85,10 +83,10 @@ export function DiscoverPage(props: PageProps) {
borderColor={theme.border} borderColor={theme.border}
> >
<box padding={1}> <box padding={1}>
<SelectableText <SelectableText
selected={() => false} selected={() => false}
primary={props.depth() == DiscoverPagePaneType.SHOWS} primary={nav.activeDepth == DiscoverPagePaneType.SHOWS}
> >
Trending in{" "} Trending in{" "}
{DISCOVER_CATEGORIES.find( {DISCOVER_CATEGORIES.find(
(c) => c.id === discoverStore.selectedCategory(), (c) => c.id === discoverStore.selectedCategory(),
@@ -102,7 +100,9 @@ export function DiscoverPage(props: PageProps) {
{discoverStore.filteredPodcasts().length !== 0 ? ( {discoverStore.filteredPodcasts().length !== 0 ? (
<text fg={theme.warning}>Loading trending shows...</text> <text fg={theme.warning}>Loading trending shows...</text>
) : ( ) : (
<text fg={theme.textMuted}>No podcasts found in this category.</text> <text fg={theme.textMuted}>
No podcasts found in this category.
</text>
)} )}
</box> </box>
} }
@@ -119,7 +119,7 @@ export function DiscoverPage(props: PageProps) {
podcast={podcast} podcast={podcast}
selected={ selected={
index() === showIndex() && index() === showIndex() &&
props.depth() == DiscoverPagePaneType.SHOWS nav.activeDepth == DiscoverPagePaneType.SHOWS
} }
onSelect={() => handleShowSelect(index())} onSelect={() => handleShowSelect(index())}
onSubscribe={() => handleSubscribe(podcast)} onSubscribe={() => handleSubscribe(podcast)}

View File

@@ -10,9 +10,9 @@ 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";
import { useTheme } from "@/context/ThemeContext"; import { useTheme } from "@/context/ThemeContext";
import { PageProps } from "@/App";
import { SelectableBox, SelectableText } from "@/components/Selectable"; import { SelectableBox, SelectableText } from "@/components/Selectable";
import { se } from "date-fns/locale"; import { se } from "date-fns/locale";
import { useNavigation } from "@/context/NavigationContext";
enum FeedPaneType { enum FeedPaneType {
FEED = 1, FEED = 1,
@@ -22,10 +22,12 @@ export const FeedPaneCount = 1;
/** Episodes to load per batch */ /** Episodes to load per batch */
const ITEMS_PER_BATCH = 50; const ITEMS_PER_BATCH = 50;
export function FeedPage(props: PageProps) { export function FeedPage() {
const feedStore = useFeedStore(); const feedStore = useFeedStore();
const [isRefreshing, setIsRefreshing] = createSignal(false); 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(); const allEpisodes = () => feedStore.getAllEpisodesChronological();
@@ -86,15 +88,14 @@ export function FeedPage(props: PageProps) {
</box> </box>
} }
> >
<scrollbox height="100%" focused={props.depth() == FeedPaneType.FEED}> <scrollbox height="100%" focused={nav.activeDepth == FeedPaneType.FEED}>
<For <For
each={Object.entries(episodesByDate()).sort(([a], [b]) => each={Object.entries(episodesByDate()).sort(([a], [b]) =>
b.localeCompare(a), b.localeCompare(a),
)} )}
> >
{([date, episode], groupIndex) => { {([date, episode], groupIndex) => {
const index = typeof props.focusedIndex === 'function' ? props.focusedIndex() : props.focusedIndex; const selected = () => groupIndex() === 1; // TODO: Manage selections locally
const selected = () => groupIndex() === index;
return ( return (
<> <>
<box <box

View File

@@ -9,9 +9,9 @@ import { useFeedStore } from "@/stores/feed";
import { useDownloadStore } from "@/stores/download"; import { useDownloadStore } from "@/stores/download";
import { DownloadStatus } from "@/types/episode"; import { DownloadStatus } from "@/types/episode";
import { format } from "date-fns"; import { format } from "date-fns";
import { PageProps } from "@/App";
import { useTheme } from "@/context/ThemeContext"; import { useTheme } from "@/context/ThemeContext";
import { useAudioNavStore, AudioSource } from "@/stores/audio-nav"; import { useAudioNavStore, AudioSource } from "@/stores/audio-nav";
import { useNavigation } from "@/context/NavigationContext";
enum MyShowsPaneType { enum MyShowsPaneType {
SHOWS = 1, SHOWS = 1,
@@ -20,13 +20,16 @@ enum MyShowsPaneType {
export const MyShowsPaneCount = 2; export const MyShowsPaneCount = 2;
export function MyShowsPage(props: PageProps) { export function MyShowsPage() {
const feedStore = useFeedStore(); const feedStore = useFeedStore();
const downloadStore = useDownloadStore(); const downloadStore = useDownloadStore();
const audioNav = useAudioNavStore(); const audioNav = useAudioNavStore();
const [isRefreshing, setIsRefreshing] = createSignal(false); const [isRefreshing, setIsRefreshing] = createSignal(false);
const [showIndex, setShowIndex] = createSignal(0);
const [episodeIndex, setEpisodeIndex] = createSignal(0);
const { theme } = useTheme(); const { theme } = useTheme();
const mutedColor = () => theme.muted || theme.text; const mutedColor = () => theme.muted || theme.text;
const nav = useNavigation();
/** Threshold: load more when within this many items of the end */ /** Threshold: load more when within this many items of the end */
const LOAD_MORE_THRESHOLD = 5; const LOAD_MORE_THRESHOLD = 5;
@@ -34,9 +37,7 @@ export function MyShowsPage(props: PageProps) {
const shows = () => feedStore.getFilteredFeeds(); const shows = () => feedStore.getFilteredFeeds();
const selectedShow = createMemo(() => { const selectedShow = createMemo(() => {
const s = shows(); return shows()[0]; //TODO: Integrate with locally handled keyboard navigation
const index = typeof props.focusedIndex === 'function' ? props.focusedIndex() : props.focusedIndex;
return index < s.length ? s[index] : undefined;
}); });
const episodes = createMemo(() => { const episodes = createMemo(() => {
@@ -128,7 +129,7 @@ export function MyShowsPage(props: PageProps) {
> >
<scrollbox <scrollbox
height="100%" height="100%"
focused={props.depth() == MyShowsPaneType.SHOWS} focused={nav.activeDepth == MyShowsPaneType.SHOWS}
> >
<For each={shows()}> <For each={shows()}>
{(feed, index) => ( {(feed, index) => (
@@ -143,7 +144,10 @@ export function MyShowsPage(props: PageProps) {
onMouseDown={() => { onMouseDown={() => {
setShowIndex(index()); setShowIndex(index());
setEpisodeIndex(0); setEpisodeIndex(0);
audioNav.setSource(AudioSource.MY_SHOWS, selectedShow()?.podcast.id); audioNav.setSource(
AudioSource.MY_SHOWS,
selectedShow()?.podcast.id,
);
}} }}
> >
<text <text
@@ -184,7 +188,7 @@ export function MyShowsPage(props: PageProps) {
> >
<scrollbox <scrollbox
height="100%" height="100%"
focused={props.depth() == MyShowsPaneType.EPISODES} focused={nav.activeDepth == MyShowsPaneType.EPISODES}
> >
<For each={episodes()}> <For each={episodes()}>
{(episode, index) => ( {(episode, index) => (

View File

@@ -1,4 +1,3 @@
import { PageProps } from "@/App";
import { PlaybackControls } from "./PlaybackControls"; import { PlaybackControls } from "./PlaybackControls";
import { RealtimeWaveform } from "./RealtimeWaveform"; import { RealtimeWaveform } from "./RealtimeWaveform";
import { useAudio } from "@/hooks/useAudio"; import { useAudio } from "@/hooks/useAudio";
@@ -10,7 +9,7 @@ enum PlayerPaneType {
} }
export const PlayerPaneCount = 1; export const PlayerPaneCount = 1;
export function PlayerPage(props: PageProps) { export function PlayerPage() {
const audio = useAudio(); const audio = useAudio();
const { theme } = useTheme(); const { theme } = useTheme();
@@ -40,7 +39,13 @@ export function PlayerPage(props: PageProps) {
{audio.error() && <text fg={theme.error}>{audio.error()}</text>} {audio.error() && <text fg={theme.error}>{audio.error()}</text>}
<box border borderColor={theme.border} padding={1} flexDirection="column" gap={1}> <box
border
borderColor={theme.border}
padding={1}
flexDirection="column"
gap={1}
>
<text fg={theme.text}> <text fg={theme.text}>
<strong>{audio.currentEpisode()?.title}</strong> <strong>{audio.currentEpisode()?.title}</strong>
</text> </text>

View File

@@ -8,9 +8,9 @@ import { useSearchStore } from "@/stores/search";
import { SearchResults } from "./SearchResults"; import { SearchResults } from "./SearchResults";
import { SearchHistory } from "./SearchHistory"; import { SearchHistory } from "./SearchHistory";
import type { SearchResult } from "@/types/source"; import type { SearchResult } from "@/types/source";
import { PageProps } from "@/App";
import { MyShowsPage } from "../MyShows/MyShowsPage"; import { MyShowsPage } from "../MyShows/MyShowsPage";
import { useTheme } from "@/context/ThemeContext"; import { useTheme } from "@/context/ThemeContext";
import { useNavigation } from "@/context/NavigationContext";
enum SearchPaneType { enum SearchPaneType {
INPUT = 1, INPUT = 1,
@@ -19,19 +19,13 @@ enum SearchPaneType {
} }
export const SearchPaneCount = 3; export const SearchPaneCount = 3;
export function SearchPage(props: PageProps) { export function SearchPage() {
const searchStore = useSearchStore(); const searchStore = useSearchStore();
const [inputValue, setInputValue] = createSignal(""); const [inputValue, setInputValue] = createSignal("");
const [resultIndex, setResultIndex] = createSignal(0); const [resultIndex, setResultIndex] = createSignal(0);
const [historyIndex, setHistoryIndex] = createSignal(0); const [historyIndex, setHistoryIndex] = createSignal(0);
const { theme } = useTheme(); const { theme } = useTheme();
const nav = useNavigation();
// 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 handleSearch = async () => { const handleSearch = async () => {
const query = inputValue().trim(); const query = inputValue().trim();
@@ -75,7 +69,7 @@ export function SearchPage(props: PageProps) {
setInputValue(value); setInputValue(value);
}} }}
placeholder="Enter podcast name, topic, or author..." placeholder="Enter podcast name, topic, or author..."
focused={props.depth() === SearchPaneType.INPUT} focused={nav.activeDepth === SearchPaneType.INPUT}
width={50} width={50}
/> />
<box <box
@@ -98,33 +92,42 @@ export function SearchPage(props: PageProps) {
</Show> </Show>
</box> </box>
{/* Main Content - Results or History */} {/* Main Content - Results or History */}
<box flexDirection="row" height="100%" gap={2}> <box flexDirection="row" height="100%" gap={2}>
{/* Results Panel */} {/* Results Panel */}
<box flexDirection="column" flexGrow={1} border borderColor={theme.border}> <box
<box padding={1}> flexDirection="column"
<text flexGrow={1}
fg={props.depth() === SearchPaneType.RESULTS ? theme.primary : theme.muted} border
> borderColor={theme.border}
Results ({searchStore.results().length}) >
</text> <box padding={1}>
</box> <text
<Show fg={
when={searchStore.results().length > 0} nav.activeDepth === SearchPaneType.RESULTS
fallback={ ? theme.primary
<box padding={2}> : theme.muted
<text fg={theme.muted}>
{searchStore.query()
? "No results found"
: "Enter a search term to find podcasts"}
</text>
</box>
} }
> >
Results ({searchStore.results().length})
</text>
</box>
<Show
when={searchStore.results().length > 0}
fallback={
<box padding={2}>
<text fg={theme.muted}>
{searchStore.query()
? "No results found"
: "Enter a search term to find podcasts"}
</text>
</box>
}
>
<SearchResults <SearchResults
results={searchStore.results()} results={searchStore.results()}
selectedIndex={resultIndex()} selectedIndex={resultIndex()}
focused={props.depth() === SearchPaneType.RESULTS} focused={nav.activeDepth === SearchPaneType.RESULTS}
onSelect={handleResultSelect} onSelect={handleResultSelect}
onChange={setResultIndex} onChange={setResultIndex}
isSearching={searchStore.isSearching()} isSearching={searchStore.isSearching()}
@@ -138,7 +141,11 @@ export function SearchPage(props: PageProps) {
<box padding={1} flexDirection="column"> <box padding={1} flexDirection="column">
<box paddingBottom={1}> <box paddingBottom={1}>
<text <text
fg={props.depth() === SearchPaneType.HISTORY ? theme.primary : theme.muted} fg={
nav.activeDepth === SearchPaneType.HISTORY
? theme.primary
: theme.muted
}
> >
History History
</text> </text>
@@ -146,7 +153,7 @@ export function SearchPage(props: PageProps) {
<SearchHistory <SearchHistory
history={searchStore.history()} history={searchStore.history()}
selectedIndex={historyIndex()} selectedIndex={historyIndex()}
focused={props.depth() === SearchPaneType.HISTORY} focused={nav.activeDepth === SearchPaneType.HISTORY}
onSelect={handleHistorySelect} onSelect={handleHistorySelect}
onRemove={searchStore.removeFromHistory} onRemove={searchStore.removeFromHistory}
onClear={searchStore.clearHistory} onClear={searchStore.clearHistory}

View File

@@ -5,7 +5,7 @@ import { useTheme } from "@/context/ThemeContext";
import { PreferencesPanel } from "./PreferencesPanel"; import { PreferencesPanel } from "./PreferencesPanel";
import { SyncPanel } from "./SyncPanel"; import { SyncPanel } from "./SyncPanel";
import { VisualizerSettings } from "./VisualizerSettings"; import { VisualizerSettings } from "./VisualizerSettings";
import { PageProps } from "@/App"; import { useNavigation } from "@/context/NavigationContext";
enum SettingsPaneType { enum SettingsPaneType {
SYNC = 1, SYNC = 1,
@@ -24,11 +24,9 @@ const SECTIONS: Array<{ id: SettingsPaneType; label: string }> = [
{ id: SettingsPaneType.ACCOUNT, label: "Account" }, { id: SettingsPaneType.ACCOUNT, label: "Account" },
]; ];
export function SettingsPage(props: PageProps) { export function SettingsPage() {
const { theme } = useTheme(); const { theme } = useTheme();
const [activeSection, setActiveSection] = createSignal<SettingsPaneType>( const nav = useNavigation();
SettingsPaneType.SYNC,
);
return ( return (
<box flexDirection="column" gap={1} height="100%" width="100%"> <box flexDirection="column" gap={1} height="100%" width="100%">
@@ -40,13 +38,13 @@ export function SettingsPage(props: PageProps) {
borderColor={theme.border} borderColor={theme.border}
padding={0} padding={0}
backgroundColor={ backgroundColor={
activeSection() === section.id ? theme.primary : undefined nav.activeDepth === section.id ? theme.primary : undefined
} }
onMouseDown={() => setActiveSection(section.id)} onMouseDown={() => nav.setActiveDepth(section.id)}
> >
<text <text
fg={ fg={
activeSection() === section.id ? theme.text : theme.textMuted nav.activeDepth === section.id ? theme.text : theme.textMuted
} }
> >
[{index() + 1}] {section.label} [{index() + 1}] {section.label}
@@ -56,18 +54,25 @@ export function SettingsPage(props: PageProps) {
</For> </For>
</box> </box>
<box border borderColor={theme.border} flexGrow={1} padding={1} flexDirection="column" gap={1}> <box
{activeSection() === SettingsPaneType.SYNC && <SyncPanel />} border
{activeSection() === SettingsPaneType.SOURCES && ( borderColor={theme.border}
flexGrow={1}
padding={1}
flexDirection="column"
gap={1}
>
{nav.activeDepth === SettingsPaneType.SYNC && <SyncPanel />}
{nav.activeDepth === SettingsPaneType.SOURCES && (
<SourceManager focused /> <SourceManager focused />
)} )}
{activeSection() === SettingsPaneType.PREFERENCES && ( {nav.activeDepth === SettingsPaneType.PREFERENCES && (
<PreferencesPanel /> <PreferencesPanel />
)} )}
{activeSection() === SettingsPaneType.VISUALIZER && ( {nav.activeDepth === SettingsPaneType.VISUALIZER && (
<VisualizerSettings /> <VisualizerSettings />
)} )}
{activeSection() === SettingsPaneType.ACCOUNT && ( {nav.activeDepth === SettingsPaneType.ACCOUNT && (
<box flexDirection="column" gap={1}> <box flexDirection="column" gap={1}>
<text fg={theme.textMuted}>Account</text> <text fg={theme.textMuted}>Account</text>
</box> </box>