redoing navigation logic to favor more local
This commit is contained in:
@@ -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) {
|
||||
>
|
||||
<text
|
||||
fg={
|
||||
props.depth() == DiscoverPagePaneType.CATEGORIES
|
||||
nav.activeDepth == DiscoverPagePaneType.CATEGORIES
|
||||
? theme.accent
|
||||
: theme.text
|
||||
}
|
||||
>
|
||||
Categories:
|
||||
</text>
|
||||
<box flexDirection="column" gap={1}>
|
||||
<For each={discoverStore.categories}>
|
||||
{(category) => {
|
||||
const isSelected = () =>
|
||||
discoverStore.selectedCategory() === category.id;
|
||||
<box flexDirection="column" gap={1}>
|
||||
<For each={discoverStore.categories}>
|
||||
{(category) => {
|
||||
const isSelected = () =>
|
||||
discoverStore.selectedCategory() === category.id;
|
||||
|
||||
return (
|
||||
<SelectableBox
|
||||
selected={isSelected}
|
||||
onMouseDown={() => handleCategorySelect(category.id)}
|
||||
>
|
||||
<SelectableText
|
||||
selected={isSelected}
|
||||
primary
|
||||
>
|
||||
{category.icon} {category.name}
|
||||
</SelectableText>
|
||||
</SelectableBox>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</box>
|
||||
return (
|
||||
<SelectableBox
|
||||
selected={isSelected}
|
||||
onMouseDown={() => handleCategorySelect(category.id)}
|
||||
>
|
||||
<SelectableText selected={isSelected} primary>
|
||||
{category.icon} {category.name}
|
||||
</SelectableText>
|
||||
</SelectableBox>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</box>
|
||||
</box>
|
||||
<box
|
||||
flexDirection="column"
|
||||
@@ -85,10 +83,10 @@ export function DiscoverPage(props: PageProps) {
|
||||
borderColor={theme.border}
|
||||
>
|
||||
<box padding={1}>
|
||||
<SelectableText
|
||||
selected={() => false}
|
||||
primary={props.depth() == DiscoverPagePaneType.SHOWS}
|
||||
>
|
||||
<SelectableText
|
||||
selected={() => 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 ? (
|
||||
<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>
|
||||
}
|
||||
@@ -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)}
|
||||
|
||||
@@ -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) {
|
||||
</box>
|
||||
}
|
||||
>
|
||||
<scrollbox height="100%" focused={props.depth() == FeedPaneType.FEED}>
|
||||
<scrollbox height="100%" focused={nav.activeDepth == FeedPaneType.FEED}>
|
||||
<For
|
||||
each={Object.entries(episodesByDate()).sort(([a], [b]) =>
|
||||
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 (
|
||||
<>
|
||||
<box
|
||||
|
||||
@@ -9,9 +9,9 @@ import { useFeedStore } from "@/stores/feed";
|
||||
import { useDownloadStore } from "@/stores/download";
|
||||
import { DownloadStatus } from "@/types/episode";
|
||||
import { format } from "date-fns";
|
||||
import { PageProps } from "@/App";
|
||||
import { useTheme } from "@/context/ThemeContext";
|
||||
import { useAudioNavStore, AudioSource } from "@/stores/audio-nav";
|
||||
import { useNavigation } from "@/context/NavigationContext";
|
||||
|
||||
enum MyShowsPaneType {
|
||||
SHOWS = 1,
|
||||
@@ -20,13 +20,16 @@ enum MyShowsPaneType {
|
||||
|
||||
export const MyShowsPaneCount = 2;
|
||||
|
||||
export function MyShowsPage(props: PageProps) {
|
||||
export function MyShowsPage() {
|
||||
const feedStore = useFeedStore();
|
||||
const downloadStore = useDownloadStore();
|
||||
const audioNav = useAudioNavStore();
|
||||
const [isRefreshing, setIsRefreshing] = createSignal(false);
|
||||
const [showIndex, setShowIndex] = createSignal(0);
|
||||
const [episodeIndex, setEpisodeIndex] = createSignal(0);
|
||||
const { theme } = useTheme();
|
||||
const mutedColor = () => 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) {
|
||||
>
|
||||
<scrollbox
|
||||
height="100%"
|
||||
focused={props.depth() == MyShowsPaneType.SHOWS}
|
||||
focused={nav.activeDepth == MyShowsPaneType.SHOWS}
|
||||
>
|
||||
<For each={shows()}>
|
||||
{(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,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<text
|
||||
@@ -184,7 +188,7 @@ export function MyShowsPage(props: PageProps) {
|
||||
>
|
||||
<scrollbox
|
||||
height="100%"
|
||||
focused={props.depth() == MyShowsPaneType.EPISODES}
|
||||
focused={nav.activeDepth == MyShowsPaneType.EPISODES}
|
||||
>
|
||||
<For each={episodes()}>
|
||||
{(episode, index) => (
|
||||
|
||||
@@ -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() && <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}>
|
||||
<strong>{audio.currentEpisode()?.title}</strong>
|
||||
</text>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
<box
|
||||
@@ -98,33 +92,42 @@ export function SearchPage(props: PageProps) {
|
||||
</Show>
|
||||
</box>
|
||||
|
||||
{/* Main Content - Results or History */}
|
||||
<box flexDirection="row" height="100%" gap={2}>
|
||||
{/* Results Panel */}
|
||||
<box flexDirection="column" flexGrow={1} border borderColor={theme.border}>
|
||||
<box padding={1}>
|
||||
<text
|
||||
fg={props.depth() === SearchPaneType.RESULTS ? theme.primary : theme.muted}
|
||||
>
|
||||
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>
|
||||
{/* Main Content - Results or History */}
|
||||
<box flexDirection="row" height="100%" gap={2}>
|
||||
{/* Results Panel */}
|
||||
<box
|
||||
flexDirection="column"
|
||||
flexGrow={1}
|
||||
border
|
||||
borderColor={theme.border}
|
||||
>
|
||||
<box padding={1}>
|
||||
<text
|
||||
fg={
|
||||
nav.activeDepth === SearchPaneType.RESULTS
|
||||
? theme.primary
|
||||
: theme.muted
|
||||
}
|
||||
>
|
||||
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
|
||||
results={searchStore.results()}
|
||||
selectedIndex={resultIndex()}
|
||||
focused={props.depth() === SearchPaneType.RESULTS}
|
||||
focused={nav.activeDepth === SearchPaneType.RESULTS}
|
||||
onSelect={handleResultSelect}
|
||||
onChange={setResultIndex}
|
||||
isSearching={searchStore.isSearching()}
|
||||
@@ -138,7 +141,11 @@ export function SearchPage(props: PageProps) {
|
||||
<box padding={1} flexDirection="column">
|
||||
<box paddingBottom={1}>
|
||||
<text
|
||||
fg={props.depth() === SearchPaneType.HISTORY ? theme.primary : theme.muted}
|
||||
fg={
|
||||
nav.activeDepth === SearchPaneType.HISTORY
|
||||
? theme.primary
|
||||
: theme.muted
|
||||
}
|
||||
>
|
||||
History
|
||||
</text>
|
||||
@@ -146,7 +153,7 @@ export function SearchPage(props: PageProps) {
|
||||
<SearchHistory
|
||||
history={searchStore.history()}
|
||||
selectedIndex={historyIndex()}
|
||||
focused={props.depth() === SearchPaneType.HISTORY}
|
||||
focused={nav.activeDepth === SearchPaneType.HISTORY}
|
||||
onSelect={handleHistorySelect}
|
||||
onRemove={searchStore.removeFromHistory}
|
||||
onClear={searchStore.clearHistory}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useTheme } from "@/context/ThemeContext";
|
||||
import { PreferencesPanel } from "./PreferencesPanel";
|
||||
import { SyncPanel } from "./SyncPanel";
|
||||
import { VisualizerSettings } from "./VisualizerSettings";
|
||||
import { PageProps } from "@/App";
|
||||
import { useNavigation } from "@/context/NavigationContext";
|
||||
|
||||
enum SettingsPaneType {
|
||||
SYNC = 1,
|
||||
@@ -24,11 +24,9 @@ const SECTIONS: Array<{ id: SettingsPaneType; label: string }> = [
|
||||
{ id: SettingsPaneType.ACCOUNT, label: "Account" },
|
||||
];
|
||||
|
||||
export function SettingsPage(props: PageProps) {
|
||||
export function SettingsPage() {
|
||||
const { theme } = useTheme();
|
||||
const [activeSection, setActiveSection] = createSignal<SettingsPaneType>(
|
||||
SettingsPaneType.SYNC,
|
||||
);
|
||||
const nav = useNavigation();
|
||||
|
||||
return (
|
||||
<box flexDirection="column" gap={1} height="100%" width="100%">
|
||||
@@ -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)}
|
||||
>
|
||||
<text
|
||||
fg={
|
||||
activeSection() === section.id ? theme.text : theme.textMuted
|
||||
nav.activeDepth === section.id ? theme.text : theme.textMuted
|
||||
}
|
||||
>
|
||||
[{index() + 1}] {section.label}
|
||||
@@ -56,18 +54,25 @@ export function SettingsPage(props: PageProps) {
|
||||
</For>
|
||||
</box>
|
||||
|
||||
<box border borderColor={theme.border} flexGrow={1} padding={1} flexDirection="column" gap={1}>
|
||||
{activeSection() === SettingsPaneType.SYNC && <SyncPanel />}
|
||||
{activeSection() === SettingsPaneType.SOURCES && (
|
||||
<box
|
||||
border
|
||||
borderColor={theme.border}
|
||||
flexGrow={1}
|
||||
padding={1}
|
||||
flexDirection="column"
|
||||
gap={1}
|
||||
>
|
||||
{nav.activeDepth === SettingsPaneType.SYNC && <SyncPanel />}
|
||||
{nav.activeDepth === SettingsPaneType.SOURCES && (
|
||||
<SourceManager focused />
|
||||
)}
|
||||
{activeSection() === SettingsPaneType.PREFERENCES && (
|
||||
{nav.activeDepth === SettingsPaneType.PREFERENCES && (
|
||||
<PreferencesPanel />
|
||||
)}
|
||||
{activeSection() === SettingsPaneType.VISUALIZER && (
|
||||
{nav.activeDepth === SettingsPaneType.VISUALIZER && (
|
||||
<VisualizerSettings />
|
||||
)}
|
||||
{activeSection() === SettingsPaneType.ACCOUNT && (
|
||||
{nav.activeDepth === SettingsPaneType.ACCOUNT && (
|
||||
<box flexDirection="column" gap={1}>
|
||||
<text fg={theme.textMuted}>Account</text>
|
||||
</box>
|
||||
|
||||
Reference in New Issue
Block a user