redoing navigation logic to favor more local
This commit is contained in:
242
src/App.tsx
242
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 { 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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
27
src/context/NavigationContext.tsx
Normal file
27
src/context/NavigationContext.tsx
Normal 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,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) => (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user