This commit is contained in:
2026-02-07 19:05:45 -05:00
parent 5bd393c9cd
commit bcf248f7dd
8 changed files with 93 additions and 249 deletions

View File

@@ -1,15 +1,9 @@
import { createSignal, createMemo, ErrorBoundary, Accessor } from "solid-js";
import { useSelectionHandler } from "@opentui/solid";
import { TabNavigation } from "./components/TabNavigation";
import { FeedPage } from "@/tabs/Feed/FeedPage";
import { MyShowsPage } from "@/tabs/MyShows/MyShowsPage";
import { LoginScreen } from "@/tabs/Settings/LoginScreen";
import { CodeValidation } from "@/components/CodeValidation";
import { OAuthPlaceholder } from "@/tabs/Settings/OAuthPlaceholder";
import { SyncProfile } from "@/tabs/Settings/SyncProfile";
import { SearchPage } from "@/tabs/Search/SearchPage";
import { DiscoverPage } from "@/tabs/Discover/DiscoverPage";
import { SettingsScreen } from "@/tabs/Settings/SettingsScreen";
import { useAuthStore } from "@/stores/auth";
import { useFeedStore } from "@/stores/feed";
import { useAudio } from "@/hooks/useAudio";
@@ -21,8 +15,7 @@ import { useToast } from "@/ui/toast";
import { useRenderer } from "@opentui/solid";
import type { AuthScreen } from "@/types/auth";
import type { Episode } from "@/types/episode";
import { DIRECTION } from "./types/navigation";
import { LayerGraph, TABS } from "./utils/navigation";
import { DIRECTION, LayerGraph, TABS } from "./utils/navigation";
import { useTheme } from "./context/ThemeContext";
export interface PageProps {
@@ -119,6 +112,7 @@ export function App() {
>
<TabNavigation activeTab={activeTab()} onTabSelect={setActiveTab} />
{LayerGraph[activeTab()]({ depth: activeDepth })}
{/**TODO: Contextual controls based on tab/depth**/}
</box>
</ErrorBoundary>
);

View File

@@ -11,6 +11,9 @@ import type { Feed } from "@/types/feed";
import { useTheme } from "@/context/ThemeContext";
import { PageProps } from "@/App";
enum FeedPaneType {
FEED = 1,
}
export const FeedPaneCount = 1;
export function FeedPage(props: PageProps) {
@@ -60,7 +63,7 @@ export function FeedPage(props: PageProps) {
}
>
{/**TODO: figure out wtf to do here **/}
<scrollbox height="100%" focused={true}>
<scrollbox height="100%" focused={props.depth() == FeedPaneType.FEED}>
<For each={allEpisodes()}>
{(item, index) => (
<box

View File

@@ -15,16 +15,15 @@ import type { Feed } from "@/types/feed";
import { PageProps } from "@/App";
enum MyShowsPaneType {
SHOWS,
EPISODES,
SHOWS = 1,
EPISODES = 2,
}
export const MyShowsPaneCount = 2
export const MyShowsPaneCount = 2;
export function MyShowsPage(props: PageProps) {
const feedStore = useFeedStore();
const downloadStore = useDownloadStore();
const [focusPane, setFocusPane] = createSignal<>("shows");
const [showIndex, setShowIndex] = createSignal(0);
const [episodeIndex, setEpisodeIndex] = createSignal(0);
const [isRefreshing, setIsRefreshing] = createSignal(false);
@@ -128,8 +127,8 @@ export function MyShowsPage(props: PageProps) {
setEpisodeIndex(0);
};
return {
showsPanel: () => (
return (
<box flexDirection="row" flexGrow={1}>
<box flexDirection="column" height="100%">
<Show when={isRefreshing()}>
<text fg="yellow">Refreshing...</text>
@@ -144,7 +143,10 @@ export function MyShowsPage(props: PageProps) {
</box>
}
>
<scrollbox height="100%" focused={props.depth}>
<scrollbox
height="100%"
focused={props.depth() == MyShowsPaneType.SHOWS}
>
<For each={shows()}>
{(feed, index) => (
<box
@@ -171,9 +173,6 @@ export function MyShowsPage(props: PageProps) {
</scrollbox>
</Show>
</box>
),
episodesPanel: () => (
<box flexDirection="column" height="100%">
<Show
when={selectedShow()}
@@ -193,7 +192,7 @@ export function MyShowsPage(props: PageProps) {
>
<scrollbox
height="100%"
focused={props.focused && focusPane() === "episodes"}
focused={props.depth() == MyShowsPaneType.EPISODES}
>
<For each={episodes()}>
{(episode, index) => (
@@ -252,9 +251,6 @@ export function MyShowsPage(props: PageProps) {
</Show>
</Show>
</box>
),
focusPane,
selectedShow,
};
</box>
);
}

View File

@@ -1,9 +1,15 @@
import { PageProps } from "@/App";
import { PlaybackControls } from "./PlaybackControls";
import { RealtimeWaveform } from "./RealtimeWaveform";
import { useAudio } from "@/hooks/useAudio";
import { useAppStore } from "@/stores/app";
export function PlayerPage() {
enum PlayerPaneType {
PLAYER = 1,
}
export const PlayerPaneCount = 1;
export function PlayerPage(props: PageProps) {
const audio = useAudio();
const progressPercent = () => {
@@ -63,11 +69,6 @@ export function PlayerPage() {
onSpeedChange={(s: number) => audio.setSpeed(s)}
onVolumeChange={(v: number) => audio.setVolume(v)}
/>
<text fg="gray">
Space play/pause | Left/Right seek 10s | Up/Down volume | S speed | Esc
back
</text>
</box>
);
}

View File

@@ -8,35 +8,35 @@ 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";
type SearchPageProps = {
focused: boolean;
onSubscribe?: (result: SearchResult) => void;
onInputFocusChange?: (focused: boolean) => void;
onExit?: () => void;
};
enum SearchPaneType {
INPUT = 1,
RESULTS = 2,
HISTORY = 3,
}
export const SearchPaneCount = 3;
type FocusArea = "input" | "results" | "history";
export function SearchPage(props: SearchPageProps) {
export function SearchPage(props: PageProps) {
const searchStore = useSearchStore();
const [focusArea, setFocusArea] = createSignal<FocusArea>("input");
const [inputValue, setInputValue] = createSignal("");
const [resultIndex, setResultIndex] = createSignal(0);
const [historyIndex, setHistoryIndex] = createSignal(0);
// Keep parent informed about input focus state
createEffect(() => {
const isInputFocused = props.focused && focusArea() === "input";
props.onInputFocusChange?.(isInputFocused);
});
// TODO: have a global input focused prop in useKeyboard hook
//createEffect(() => {
//const isInputFocused = props.focused && focusArea() === "input";
//props.onInputFocusChange?.(isInputFocused);
//});
const handleSearch = async () => {
const query = inputValue().trim();
if (query) {
await searchStore.search(query);
if (searchStore.results().length > 0) {
setFocusArea("results");
//setFocusArea("results"); //TODO: move level
setResultIndex(0);
}
}
@@ -46,120 +46,16 @@ export function SearchPage(props: SearchPageProps) {
setInputValue(query);
await searchStore.search(query);
if (searchStore.results().length > 0) {
setFocusArea("results");
//setFocusArea("results"); //TODO: move level
setResultIndex(0);
}
};
const handleResultSelect = (result: SearchResult) => {
props.onSubscribe?.(result);
//props.onSubscribe?.(result);
searchStore.markSubscribed(result.podcast.id);
};
// Keyboard navigation
useKeyboard((key) => {
if (!props.focused) return;
const area = focusArea();
// Enter to search from input
if (key.name === "return" && area === "input") {
handleSearch();
return;
}
// Tab to cycle focus areas
if (key.name === "tab" && !key.shift) {
if (area === "input") {
if (searchStore.results().length > 0) {
setFocusArea("results");
} else if (searchStore.history().length > 0) {
setFocusArea("history");
}
} else if (area === "results") {
if (searchStore.history().length > 0) {
setFocusArea("history");
} else {
setFocusArea("input");
}
} else {
setFocusArea("input");
}
return;
}
if (key.name === "tab" && key.shift) {
if (area === "input") {
if (searchStore.history().length > 0) {
setFocusArea("history");
} else if (searchStore.results().length > 0) {
setFocusArea("results");
}
} else if (area === "history") {
if (searchStore.results().length > 0) {
setFocusArea("results");
} else {
setFocusArea("input");
}
} else {
setFocusArea("input");
}
return;
}
// Up/Down for results and history
if (area === "results") {
const results = searchStore.results();
if (key.name === "down" || key.name === "j") {
setResultIndex((i) => Math.min(i + 1, results.length - 1));
return;
}
if (key.name === "up" || key.name === "k") {
setResultIndex((i) => Math.max(i - 1, 0));
return;
}
if (key.name === "return" || key.name === "enter") {
const result = results[resultIndex()];
if (result) handleResultSelect(result);
return;
}
}
if (area === "history") {
const history = searchStore.history();
if (key.name === "down" || key.name === "j") {
setHistoryIndex((i) => Math.min(i + 1, history.length - 1));
return;
}
if (key.name === "up" || key.name === "k") {
setHistoryIndex((i) => Math.max(i - 1, 0));
return;
}
if (key.name === "return" || key.name === "enter") {
const query = history[historyIndex()];
if (query) handleHistorySelect(query);
return;
}
}
// Escape goes back to input or up one level
if (key.name === "escape") {
if (area === "input") {
props.onExit?.();
} else {
setFocusArea("input");
key.stopPropagation();
}
return;
}
// "/" focuses search input
if (key.name === "/" && area !== "input") {
setFocusArea("input");
return;
}
});
return (
<box flexDirection="column" height="100%" gap={1}>
{/* Search Header */}
@@ -177,7 +73,7 @@ export function SearchPage(props: SearchPageProps) {
setInputValue(value);
}}
placeholder="Enter podcast name, topic, or author..."
focused={props.focused && focusArea() === "input"}
focused={props.depth() === SearchPaneType.INPUT}
width={50}
/>
<box
@@ -205,7 +101,9 @@ export function SearchPage(props: SearchPageProps) {
{/* Results Panel */}
<box flexDirection="column" flexGrow={1} border>
<box padding={1}>
<text fg={focusArea() === "results" ? "cyan" : "gray"}>
<text
fg={props.depth() === SearchPaneType.RESULTS ? "cyan" : "gray"}
>
Results ({searchStore.results().length})
</text>
</box>
@@ -224,7 +122,7 @@ export function SearchPage(props: SearchPageProps) {
<SearchResults
results={searchStore.results()}
selectedIndex={resultIndex()}
focused={focusArea() === "results"}
focused={props.depth() === SearchPaneType.RESULTS}
onSelect={handleResultSelect}
onChange={setResultIndex}
isSearching={searchStore.isSearching()}
@@ -237,14 +135,16 @@ export function SearchPage(props: SearchPageProps) {
<box width={30} border>
<box padding={1} flexDirection="column">
<box paddingBottom={1}>
<text fg={focusArea() === "history" ? "cyan" : "gray"}>
<text
fg={props.depth() === SearchPaneType.HISTORY ? "cyan" : "gray"}
>
History
</text>
</box>
<SearchHistory
history={searchStore.history()}
selectedIndex={historyIndex()}
focused={focusArea() === "history"}
focused={props.depth() === SearchPaneType.HISTORY}
onSelect={handleHistorySelect}
onRemove={searchStore.removeFromHistory}
onClear={searchStore.clearHistory}
@@ -253,14 +153,6 @@ export function SearchPage(props: SearchPageProps) {
</box>
</box>
</box>
{/* Footer Hints */}
<box flexDirection="row" gap={2}>
<text fg="gray">[Tab] Switch focus</text>
<text fg="gray">[/] Focus search</text>
<text fg="gray">[Enter] Select</text>
<text fg="gray">[Esc] Up</text>
</box>
</box>
);
}

View File

@@ -5,65 +5,33 @@ import { useTheme } from "@/context/ThemeContext";
import { PreferencesPanel } from "./PreferencesPanel";
import { SyncPanel } from "./SyncPanel";
import { VisualizerSettings } from "./VisualizerSettings";
import { PageProps } from "@/App";
type SettingsScreenProps = {
accountLabel: string;
accountStatus: "signed-in" | "signed-out";
onOpenAccount?: () => void;
onExit?: () => void;
};
enum SettingsPaneType {
SYNC = 1,
SOURCES = 2,
PREFERENCES = 3,
VISUALIZER = 4,
ACCOUNT = 5,
}
export const SettingsPaneCount = 5;
type SectionId = "sync" | "sources" | "preferences" | "visualizer" | "account";
const SECTIONS: Array<{ id: SectionId; label: string }> = [
{ id: "sync", label: "Sync" },
{ id: "sources", label: "Sources" },
{ id: "preferences", label: "Preferences" },
{ id: "visualizer", label: "Visualizer" },
{ id: "account", label: "Account" },
const SECTIONS: Array<{ id: SettingsPaneType; label: string }> = [
{ id: SettingsPaneType.SYNC, label: "Sync" },
{ id: SettingsPaneType.SOURCES, label: "Sources" },
{ id: SettingsPaneType.PREFERENCES, label: "Preferences" },
{ id: SettingsPaneType.VISUALIZER, label: "Visualizer" },
{ id: SettingsPaneType.ACCOUNT, label: "Account" },
];
export function SettingsPage(props: SettingsScreenProps) {
export function SettingsPage(props: PageProps) {
const { theme } = useTheme();
const [activeSection, setActiveSection] = createSignal<SectionId>("sync");
useKeyboard((key) => {
if (key.name === "escape") {
props.onExit?.();
return;
}
if (key.name === "tab") {
const idx = SECTIONS.findIndex((s) => s.id === activeSection());
const next = key.shift
? (idx - 1 + SECTIONS.length) % SECTIONS.length
: (idx + 1) % SECTIONS.length;
setActiveSection(SECTIONS[next].id);
return;
}
if (key.name === "1") setActiveSection("sync");
if (key.name === "2") setActiveSection("sources");
if (key.name === "3") setActiveSection("preferences");
if (key.name === "4") setActiveSection("visualizer");
if (key.name === "5") setActiveSection("account");
});
const [activeSection, setActiveSection] = createSignal<SettingsPaneType>(
SettingsPaneType.SYNC,
);
return (
<box flexDirection="column" gap={1} height="100%">
<box
flexDirection="row"
justifyContent="space-between"
alignItems="center"
>
<text>
<strong>Settings</strong>
</text>
<text fg={theme.textMuted}>
[Tab] Switch section | 1-5 jump | Esc up
</text>
</box>
<box flexDirection="row" gap={1}>
<For each={SECTIONS}>
{(section, index) => (
@@ -88,33 +56,22 @@ export function SettingsPage(props: SettingsScreenProps) {
</box>
<box border flexGrow={1} padding={1} flexDirection="column" gap={1}>
{activeSection() === "sync" && <SyncPanel />}
{activeSection() === "sources" && <SourceManager focused />}
{activeSection() === "preferences" && <PreferencesPanel />}
{activeSection() === "visualizer" && <VisualizerSettings />}
{activeSection() === "account" && (
{activeSection() === SettingsPaneType.SYNC && <SyncPanel />}
{activeSection() === SettingsPaneType.SOURCES && (
<SourceManager focused />
)}
{activeSection() === SettingsPaneType.PREFERENCES && (
<PreferencesPanel />
)}
{activeSection() === SettingsPaneType.VISUALIZER && (
<VisualizerSettings />
)}
{activeSection() === SettingsPaneType.ACCOUNT && (
<box flexDirection="column" gap={1}>
<text fg={theme.textMuted}>Account</text>
<box flexDirection="row" gap={2} alignItems="center">
<text fg={theme.textMuted}>Status:</text>
<text
fg={
props.accountStatus === "signed-in"
? theme.success
: theme.warning
}
>
{props.accountLabel}
</text>
</box>
<box border padding={0} onMouseDown={() => props.onOpenAccount?.()}>
<text fg={theme.primary}>[A] Manage Account</text>
</box>
</box>
)}
</box>
<text fg={theme.textMuted}>Enter to dive | Esc up</text>
</box>
);
}

View File

@@ -1,4 +0,0 @@
export enum DIRECTION {
Increment,
Decrement,
}

View File

@@ -1,9 +1,14 @@
import { DiscoverPage } from "@/pages/Discover/DiscoverPage";
import { DiscoverPage, DiscoverPaneCount } from "@/pages/Discover/DiscoverPage";
import { FeedPage, FeedPaneCount } from "@/pages/Feed/FeedPage";
import { MyShowsPage, MyShowsPaneCount } from "@/pages/MyShows/MyShowsPage";
import { PlayerPage } from "@/pages/Player/PlayerPage";
import { SearchPage } from "@/pages/Search/SearchPage";
import { SettingsPage } from "@/pages/Settings/SettingsPage";
import { PlayerPage, PlayerPaneCount } from "@/pages/Player/PlayerPage";
import { SearchPage, SearchPaneCount } from "@/pages/Search/SearchPage";
import { SettingsPage, SettingsPaneCount } from "@/pages/Settings/SettingsPage";
export enum DIRECTION {
Increment,
Decrement,
}
export enum TABS {
FEED,
@@ -28,5 +33,5 @@ export const LayerDepths = {
[TABS.DISCOVER]: DiscoverPaneCount,
[TABS.SEARCH]: SearchPaneCount,
[TABS.PLAYER]: PlayerPaneCount,
[TABS.SETTINGS]: SettingPaneCount,
[TABS.SETTINGS]: SettingsPaneCount,
};