Compare commits
4 Commits
1a5efceebd
...
1618588a30
| Author | SHA1 | Date | |
|---|---|---|---|
| 1618588a30 | |||
| c9a370a424 | |||
| b45e7bf538 | |||
| 1e6618211a |
@@ -18,8 +18,9 @@ export type KeybindsResolved = {
|
|||||||
inverseModifier: string;
|
inverseModifier: string;
|
||||||
leader: string; // will not trigger while focused on input
|
leader: string; // will not trigger while focused on input
|
||||||
quit: string[];
|
quit: string[];
|
||||||
|
select: string[]; // for selecting/activating items
|
||||||
"audio-toggle": string[];
|
"audio-toggle": string[];
|
||||||
"audio-pause": [];
|
"audio-pause": string[];
|
||||||
"audio-play": string[];
|
"audio-play": string[];
|
||||||
"audio-next": string[];
|
"audio-next": string[];
|
||||||
"audio-prev": string[];
|
"audio-prev": string[];
|
||||||
@@ -36,6 +37,7 @@ export enum KeybindAction {
|
|||||||
DIVE,
|
DIVE,
|
||||||
OUT,
|
OUT,
|
||||||
QUIT,
|
QUIT,
|
||||||
|
SELECT,
|
||||||
AUDIO_TOGGLE,
|
AUDIO_TOGGLE,
|
||||||
AUDIO_PAUSE,
|
AUDIO_PAUSE,
|
||||||
AUDIO_PLAY,
|
AUDIO_PLAY,
|
||||||
@@ -60,6 +62,7 @@ export const { use: useKeybinds, provider: KeybindProvider } =
|
|||||||
inverseModifier: "",
|
inverseModifier: "",
|
||||||
leader: "",
|
leader: "",
|
||||||
quit: [],
|
quit: [],
|
||||||
|
select: [],
|
||||||
refresh: [],
|
refresh: [],
|
||||||
"audio-toggle": [],
|
"audio-toggle": [],
|
||||||
"audio-pause": [],
|
"audio-pause": [],
|
||||||
|
|||||||
@@ -2,13 +2,14 @@
|
|||||||
* DiscoverPage component - Main discover/browse interface for PodTUI
|
* DiscoverPage component - Main discover/browse interface for PodTUI
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createSignal, For, Show } from "solid-js";
|
import { createSignal, For, Show, onMount } from "solid-js";
|
||||||
import { useKeyboard } from "@opentui/solid";
|
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 { SelectableBox, SelectableText } from "@/components/Selectable";
|
import { SelectableBox, SelectableText } from "@/components/Selectable";
|
||||||
import { useNavigation } from "@/context/NavigationContext";
|
import { useNavigation } from "@/context/NavigationContext";
|
||||||
|
import { KeybindProvider, useKeybinds } from "@/context/KeybindContext";
|
||||||
|
|
||||||
enum DiscoverPagePaneType {
|
enum DiscoverPagePaneType {
|
||||||
CATEGORIES = 1,
|
CATEGORIES = 1,
|
||||||
@@ -21,6 +22,38 @@ export function DiscoverPage() {
|
|||||||
const [showIndex, setShowIndex] = createSignal(0);
|
const [showIndex, setShowIndex] = createSignal(0);
|
||||||
const [categoryIndex, setCategoryIndex] = createSignal(0);
|
const [categoryIndex, setCategoryIndex] = createSignal(0);
|
||||||
const nav = useNavigation();
|
const nav = useNavigation();
|
||||||
|
const keybind = useKeybinds();
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
useKeyboard(
|
||||||
|
(keyEvent: any) => {
|
||||||
|
const isDown = keybind.match("down", keyEvent);
|
||||||
|
const isUp = keybind.match("up", keyEvent);
|
||||||
|
const isCycle = keybind.match("cycle", keyEvent);
|
||||||
|
const isSelect = keybind.match("select", keyEvent);
|
||||||
|
|
||||||
|
if (isSelect) {
|
||||||
|
const filteredPodcasts = discoverStore.filteredPodcasts();
|
||||||
|
if (filteredPodcasts.length > 0 && showIndex() < filteredPodcasts.length) {
|
||||||
|
setShowIndex(showIndex() + 1);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredPodcasts = discoverStore.filteredPodcasts();
|
||||||
|
if (filteredPodcasts.length === 0) return;
|
||||||
|
|
||||||
|
if (isDown) {
|
||||||
|
setShowIndex((i) => (i + 1) % filteredPodcasts.length);
|
||||||
|
} else if (isUp) {
|
||||||
|
setShowIndex((i) => (i - 1 + filteredPodcasts.length) % filteredPodcasts.length);
|
||||||
|
} else if (isCycle) {
|
||||||
|
setShowIndex((i) => (i + 1) % filteredPodcasts.length);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ release: false },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const handleCategorySelect = (categoryId: string) => {
|
const handleCategorySelect = (categoryId: string) => {
|
||||||
discoverStore.setSelectedCategory(categoryId);
|
discoverStore.setSelectedCategory(categoryId);
|
||||||
@@ -43,13 +76,17 @@ export function DiscoverPage() {
|
|||||||
<box
|
<box
|
||||||
border
|
border
|
||||||
padding={1}
|
padding={1}
|
||||||
borderColor={theme.border}
|
borderColor={
|
||||||
|
nav.activeDepth() != DiscoverPagePaneType.CATEGORIES
|
||||||
|
? theme.border
|
||||||
|
: theme.accent
|
||||||
|
}
|
||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
gap={1}
|
gap={1}
|
||||||
>
|
>
|
||||||
<text
|
<text
|
||||||
fg={
|
fg={
|
||||||
nav.activeDepth == DiscoverPagePaneType.CATEGORIES
|
nav.activeDepth() == DiscoverPagePaneType.CATEGORIES
|
||||||
? theme.accent
|
? theme.accent
|
||||||
: theme.text
|
: theme.text
|
||||||
}
|
}
|
||||||
@@ -80,12 +117,16 @@ export function DiscoverPage() {
|
|||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
flexGrow={1}
|
flexGrow={1}
|
||||||
border
|
border
|
||||||
borderColor={theme.border}
|
borderColor={
|
||||||
|
nav.activeDepth() == DiscoverPagePaneType.SHOWS
|
||||||
|
? theme.accent
|
||||||
|
: theme.border
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<box padding={1}>
|
<box padding={1}>
|
||||||
<SelectableText
|
<SelectableText
|
||||||
selected={() => false}
|
selected={() => false}
|
||||||
primary={nav.activeDepth == DiscoverPagePaneType.SHOWS}
|
primary={nav.activeDepth() == DiscoverPagePaneType.SHOWS}
|
||||||
>
|
>
|
||||||
Trending in{" "}
|
Trending in{" "}
|
||||||
{DISCOVER_CATEGORIES.find(
|
{DISCOVER_CATEGORIES.find(
|
||||||
@@ -111,7 +152,9 @@ export function DiscoverPage() {
|
|||||||
discoverStore.filteredPodcasts().length === 0
|
discoverStore.filteredPodcasts().length === 0
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<scrollbox>
|
<scrollbox
|
||||||
|
focused={nav.activeDepth() == DiscoverPagePaneType.SHOWS}
|
||||||
|
>
|
||||||
<box flexDirection="column">
|
<box flexDirection="column">
|
||||||
<For each={discoverStore.filteredPodcasts()}>
|
<For each={discoverStore.filteredPodcasts()}>
|
||||||
{(podcast, index) => (
|
{(podcast, index) => (
|
||||||
@@ -119,7 +162,7 @@ export function DiscoverPage() {
|
|||||||
podcast={podcast}
|
podcast={podcast}
|
||||||
selected={
|
selected={
|
||||||
index() === showIndex() &&
|
index() === showIndex() &&
|
||||||
nav.activeDepth == DiscoverPagePaneType.SHOWS
|
nav.activeDepth() == DiscoverPagePaneType.SHOWS
|
||||||
}
|
}
|
||||||
onSelect={() => handleShowSelect(index())}
|
onSelect={() => handleShowSelect(index())}
|
||||||
onSubscribe={() => handleSubscribe(podcast)}
|
onSubscribe={() => handleSubscribe(podcast)}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Reverse chronological order, grouped by date
|
* Reverse chronological order, grouped by date
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createSignal, For, Show } from "solid-js";
|
import { createSignal, For, Show, onMount } from "solid-js";
|
||||||
import { useFeedStore } from "@/stores/feed";
|
import { useFeedStore } from "@/stores/feed";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import type { Episode } from "@/types/episode";
|
import type { Episode } from "@/types/episode";
|
||||||
@@ -13,6 +13,8 @@ import { SelectableBox, SelectableText } from "@/components/Selectable";
|
|||||||
import { useNavigation } from "@/context/NavigationContext";
|
import { useNavigation } from "@/context/NavigationContext";
|
||||||
import { LoadingIndicator } from "@/components/LoadingIndicator";
|
import { LoadingIndicator } from "@/components/LoadingIndicator";
|
||||||
import { TABS } from "@/utils/navigation";
|
import { TABS } from "@/utils/navigation";
|
||||||
|
import { useKeyboard } from "@opentui/solid";
|
||||||
|
import { KeybindProvider, useKeybinds } from "@/context/KeybindContext";
|
||||||
|
|
||||||
enum FeedPaneType {
|
enum FeedPaneType {
|
||||||
FEED = 1,
|
FEED = 1,
|
||||||
@@ -29,6 +31,39 @@ export function FeedPage() {
|
|||||||
string | undefined
|
string | undefined
|
||||||
>();
|
>();
|
||||||
const allEpisodes = () => feedStore.getAllEpisodesChronological();
|
const allEpisodes = () => feedStore.getAllEpisodesChronological();
|
||||||
|
const keybind = useKeybinds();
|
||||||
|
const [focusedIndex, setFocusedIndex] = createSignal(0);
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
useKeyboard(
|
||||||
|
(keyEvent: any) => {
|
||||||
|
const isDown = keybind.match("down", keyEvent);
|
||||||
|
const isUp = keybind.match("up", keyEvent);
|
||||||
|
const isCycle = keybind.match("cycle", keyEvent);
|
||||||
|
const isSelect = keybind.match("select", keyEvent);
|
||||||
|
|
||||||
|
if (isSelect) {
|
||||||
|
const episodes = allEpisodes();
|
||||||
|
if (episodes.length > 0 && episodes[focusedIndex()]) {
|
||||||
|
setSelectedEpisodeID(episodes[focusedIndex()].episode.id);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const episodes = allEpisodes();
|
||||||
|
if (episodes.length === 0) return;
|
||||||
|
|
||||||
|
if (isDown) {
|
||||||
|
setFocusedIndex((i) => (i + 1) % episodes.length);
|
||||||
|
} else if (isUp) {
|
||||||
|
setFocusedIndex((i) => (i - 1 + episodes.length) % episodes.length);
|
||||||
|
} else if (isCycle) {
|
||||||
|
setFocusedIndex((i) => (i + 1) % episodes.length);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ release: false },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const formatDate = (date: Date): string => {
|
const formatDate = (date: Date): string => {
|
||||||
return format(date, "MMM d, yyyy");
|
return format(date, "MMM d, yyyy");
|
||||||
@@ -105,6 +140,13 @@ export function FeedPage() {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
const isFocused = () => {
|
||||||
|
const episodes = allEpisodes();
|
||||||
|
const currentIndex = episodes.findIndex(
|
||||||
|
(e: any) => e.episode.id === item.episode.id,
|
||||||
|
);
|
||||||
|
return currentIndex === focusedIndex();
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<SelectableBox
|
<SelectableBox
|
||||||
selected={isSelected}
|
selected={isSelected}
|
||||||
@@ -115,7 +157,11 @@ export function FeedPage() {
|
|||||||
paddingTop={0}
|
paddingTop={0}
|
||||||
paddingBottom={0}
|
paddingBottom={0}
|
||||||
onMouseDown={() => {
|
onMouseDown={() => {
|
||||||
// Selection is handled by App's keyboard navigation
|
setSelectedEpisodeID(item.episode.id);
|
||||||
|
const episodes = allEpisodes();
|
||||||
|
setFocusedIndex(
|
||||||
|
episodes.findIndex((e: any) => e.episode.id === item.episode.id),
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectableText selected={isSelected} primary>
|
<SelectableText selected={isSelected} primary>
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
* Right panel: episodes for the selected show
|
* Right panel: episodes for the selected show
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createSignal, For, Show, createMemo, createEffect } from "solid-js";
|
import { createSignal, For, Show, createMemo, createEffect, onMount } from "solid-js";
|
||||||
|
import { useKeyboard } from "@opentui/solid";
|
||||||
import { useFeedStore } from "@/stores/feed";
|
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";
|
||||||
@@ -13,6 +14,7 @@ import { useTheme } from "@/context/ThemeContext";
|
|||||||
import { useAudioNavStore, AudioSource } from "@/stores/audio-nav";
|
import { useAudioNavStore, AudioSource } from "@/stores/audio-nav";
|
||||||
import { useNavigation } from "@/context/NavigationContext";
|
import { useNavigation } from "@/context/NavigationContext";
|
||||||
import { LoadingIndicator } from "@/components/LoadingIndicator";
|
import { LoadingIndicator } from "@/components/LoadingIndicator";
|
||||||
|
import { KeybindProvider, useKeybinds } from "@/context/KeybindContext";
|
||||||
|
|
||||||
enum MyShowsPaneType {
|
enum MyShowsPaneType {
|
||||||
SHOWS = 1,
|
SHOWS = 1,
|
||||||
@@ -31,6 +33,53 @@ export function MyShowsPage() {
|
|||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const mutedColor = () => theme.muted || theme.text;
|
const mutedColor = () => theme.muted || theme.text;
|
||||||
const nav = useNavigation();
|
const nav = useNavigation();
|
||||||
|
const keybind = useKeybinds();
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
useKeyboard(
|
||||||
|
(keyEvent: any) => {
|
||||||
|
const isDown = keybind.match("down", keyEvent);
|
||||||
|
const isUp = keybind.match("up", keyEvent);
|
||||||
|
const isCycle = keybind.match("cycle", keyEvent);
|
||||||
|
const isSelect = keybind.match("select", keyEvent);
|
||||||
|
|
||||||
|
const shows = feedStore.getFilteredFeeds();
|
||||||
|
const episodesList = episodes();
|
||||||
|
const selected = selectedShow();
|
||||||
|
|
||||||
|
if (isSelect) {
|
||||||
|
if (shows.length > 0 && showIndex() < shows.length) {
|
||||||
|
setShowIndex(showIndex() + 1);
|
||||||
|
}
|
||||||
|
if (episodesList.length > 0 && episodeIndex() < episodesList.length) {
|
||||||
|
setEpisodeIndex(episodeIndex() + 1);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shows.length > 0) {
|
||||||
|
if (isDown) {
|
||||||
|
setShowIndex((i) => (i + 1) % shows.length);
|
||||||
|
} else if (isUp) {
|
||||||
|
setShowIndex((i) => (i - 1 + shows.length) % shows.length);
|
||||||
|
} else if (isCycle) {
|
||||||
|
setShowIndex((i) => (i + 1) % shows.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (episodesList.length > 0) {
|
||||||
|
if (isDown) {
|
||||||
|
setEpisodeIndex((i) => (i + 1) % episodesList.length);
|
||||||
|
} else if (isUp) {
|
||||||
|
setEpisodeIndex((i) => (i - 1 + episodesList.length) % episodesList.length);
|
||||||
|
} else if (isCycle) {
|
||||||
|
setEpisodeIndex((i) => (i + 1) % episodesList.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ release: false },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
/** 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;
|
||||||
@@ -129,8 +178,14 @@ export function MyShowsPage() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<scrollbox
|
<scrollbox
|
||||||
|
border
|
||||||
height="100%"
|
height="100%"
|
||||||
focused={nav.activeDepth == MyShowsPaneType.SHOWS}
|
borderColor={
|
||||||
|
nav.activeDepth() == MyShowsPaneType.SHOWS
|
||||||
|
? theme.accent
|
||||||
|
: theme.border
|
||||||
|
}
|
||||||
|
focused={nav.activeDepth() == MyShowsPaneType.SHOWS}
|
||||||
>
|
>
|
||||||
<For each={shows()}>
|
<For each={shows()}>
|
||||||
{(feed, index) => (
|
{(feed, index) => (
|
||||||
@@ -188,8 +243,14 @@ export function MyShowsPage() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<scrollbox
|
<scrollbox
|
||||||
|
border
|
||||||
height="100%"
|
height="100%"
|
||||||
focused={nav.activeDepth == MyShowsPaneType.EPISODES}
|
borderColor={
|
||||||
|
nav.activeDepth() == MyShowsPaneType.EPISODES
|
||||||
|
? theme.accent
|
||||||
|
: theme.border
|
||||||
|
}
|
||||||
|
focused={nav.activeDepth() == MyShowsPaneType.EPISODES}
|
||||||
>
|
>
|
||||||
<For each={episodes()}>
|
<For each={episodes()}>
|
||||||
{(episode, index) => (
|
{(episode, index) => (
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import { RealtimeWaveform } from "./RealtimeWaveform";
|
|||||||
import { useAudio } from "@/hooks/useAudio";
|
import { useAudio } from "@/hooks/useAudio";
|
||||||
import { useAppStore } from "@/stores/app";
|
import { useAppStore } from "@/stores/app";
|
||||||
import { useTheme } from "@/context/ThemeContext";
|
import { useTheme } from "@/context/ThemeContext";
|
||||||
|
import { useNavigation } from "@/context/NavigationContext";
|
||||||
|
import { useKeybinds } from "@/context/KeybindContext";
|
||||||
|
import { useKeyboard } from "@opentui/solid";
|
||||||
|
import { onMount } from "solid-js";
|
||||||
|
|
||||||
enum PlayerPaneType {
|
enum PlayerPaneType {
|
||||||
PLAYER = 1,
|
PLAYER = 1,
|
||||||
@@ -12,6 +16,31 @@ export const PlayerPaneCount = 1;
|
|||||||
export function PlayerPage() {
|
export function PlayerPage() {
|
||||||
const audio = useAudio();
|
const audio = useAudio();
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
|
const nav = useNavigation();
|
||||||
|
|
||||||
|
const keybind = useKeybinds();
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
useKeyboard(
|
||||||
|
(keyEvent: any) => {
|
||||||
|
if (keybind.match("audio-toggle", keyEvent)) {
|
||||||
|
audio.togglePlayback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keybind.match("audio-seek-forward", keyEvent)) {
|
||||||
|
audio.seek(audio.currentEpisode()?.duration ?? 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keybind.match("audio-seek-backward", keyEvent)) {
|
||||||
|
audio.seek(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ release: false },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const progressPercent = () => {
|
const progressPercent = () => {
|
||||||
const d = audio.duration();
|
const d = audio.duration();
|
||||||
@@ -41,7 +70,7 @@ export function PlayerPage() {
|
|||||||
|
|
||||||
<box
|
<box
|
||||||
border
|
border
|
||||||
borderColor={theme.border}
|
borderColor={nav.activeDepth() == PlayerPaneType.PLAYER ? theme.accent : theme.border}
|
||||||
padding={1}
|
padding={1}
|
||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
gap={1}
|
gap={1}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* SearchPage component - Main search interface for PodTUI
|
* SearchPage component - Main search interface for PodTUI
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createSignal, createEffect, Show } from "solid-js";
|
import { createSignal, createEffect, Show, onMount } from "solid-js";
|
||||||
import { useKeyboard } from "@opentui/solid";
|
import { useKeyboard } from "@opentui/solid";
|
||||||
import { useSearchStore } from "@/stores/search";
|
import { useSearchStore } from "@/stores/search";
|
||||||
import { SearchResults } from "./SearchResults";
|
import { SearchResults } from "./SearchResults";
|
||||||
@@ -11,6 +11,7 @@ import type { SearchResult } from "@/types/source";
|
|||||||
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";
|
import { useNavigation } from "@/context/NavigationContext";
|
||||||
|
import { KeybindProvider, useKeybinds } from "@/context/KeybindContext";
|
||||||
|
|
||||||
enum SearchPaneType {
|
enum SearchPaneType {
|
||||||
INPUT = 1,
|
INPUT = 1,
|
||||||
@@ -26,6 +27,38 @@ export function SearchPage() {
|
|||||||
const [historyIndex, setHistoryIndex] = createSignal(0);
|
const [historyIndex, setHistoryIndex] = createSignal(0);
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const nav = useNavigation();
|
const nav = useNavigation();
|
||||||
|
const keybind = useKeybinds();
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
useKeyboard(
|
||||||
|
(keyEvent: any) => {
|
||||||
|
const isDown = keybind.match("down", keyEvent);
|
||||||
|
const isUp = keybind.match("up", keyEvent);
|
||||||
|
const isCycle = keybind.match("cycle", keyEvent);
|
||||||
|
const isSelect = keybind.match("select", keyEvent);
|
||||||
|
|
||||||
|
if (isSelect) {
|
||||||
|
const results = searchStore.results();
|
||||||
|
if (results.length > 0 && resultIndex() < results.length) {
|
||||||
|
setResultIndex(resultIndex() + 1);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = searchStore.results();
|
||||||
|
if (results.length === 0) return;
|
||||||
|
|
||||||
|
if (isDown) {
|
||||||
|
setResultIndex((i) => (i + 1) % results.length);
|
||||||
|
} else if (isUp) {
|
||||||
|
setResultIndex((i) => (i - 1 + results.length) % results.length);
|
||||||
|
} else if (isCycle) {
|
||||||
|
setResultIndex((i) => (i + 1) % results.length);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ release: false },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const handleSearch = async () => {
|
const handleSearch = async () => {
|
||||||
const query = inputValue().trim();
|
const query = inputValue().trim();
|
||||||
@@ -69,7 +102,7 @@ export function SearchPage() {
|
|||||||
setInputValue(value);
|
setInputValue(value);
|
||||||
}}
|
}}
|
||||||
placeholder="Enter podcast name, topic, or author..."
|
placeholder="Enter podcast name, topic, or author..."
|
||||||
focused={nav.activeDepth === SearchPaneType.INPUT}
|
focused={nav.activeDepth() === SearchPaneType.INPUT}
|
||||||
width={50}
|
width={50}
|
||||||
/>
|
/>
|
||||||
<box
|
<box
|
||||||
@@ -99,12 +132,16 @@ export function SearchPage() {
|
|||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
flexGrow={1}
|
flexGrow={1}
|
||||||
border
|
border
|
||||||
borderColor={theme.border}
|
borderColor={
|
||||||
|
nav.activeDepth() === SearchPaneType.RESULTS
|
||||||
|
? theme.accent
|
||||||
|
: theme.border
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<box padding={1}>
|
<box padding={1}>
|
||||||
<text
|
<text
|
||||||
fg={
|
fg={
|
||||||
nav.activeDepth === SearchPaneType.RESULTS
|
nav.activeDepth() === SearchPaneType.RESULTS
|
||||||
? theme.primary
|
? theme.primary
|
||||||
: theme.muted
|
: theme.muted
|
||||||
}
|
}
|
||||||
@@ -127,7 +164,7 @@ export function SearchPage() {
|
|||||||
<SearchResults
|
<SearchResults
|
||||||
results={searchStore.results()}
|
results={searchStore.results()}
|
||||||
selectedIndex={resultIndex()}
|
selectedIndex={resultIndex()}
|
||||||
focused={nav.activeDepth === SearchPaneType.RESULTS}
|
focused={nav.activeDepth() === SearchPaneType.RESULTS}
|
||||||
onSelect={handleResultSelect}
|
onSelect={handleResultSelect}
|
||||||
onChange={setResultIndex}
|
onChange={setResultIndex}
|
||||||
isSearching={searchStore.isSearching()}
|
isSearching={searchStore.isSearching()}
|
||||||
@@ -142,7 +179,7 @@ export function SearchPage() {
|
|||||||
<box paddingBottom={1}>
|
<box paddingBottom={1}>
|
||||||
<text
|
<text
|
||||||
fg={
|
fg={
|
||||||
nav.activeDepth === SearchPaneType.HISTORY
|
nav.activeDepth() === SearchPaneType.HISTORY
|
||||||
? theme.primary
|
? theme.primary
|
||||||
: theme.muted
|
: theme.muted
|
||||||
}
|
}
|
||||||
@@ -153,7 +190,7 @@ export function SearchPage() {
|
|||||||
<SearchHistory
|
<SearchHistory
|
||||||
history={searchStore.history()}
|
history={searchStore.history()}
|
||||||
selectedIndex={historyIndex()}
|
selectedIndex={historyIndex()}
|
||||||
focused={nav.activeDepth === SearchPaneType.HISTORY}
|
focused={nav.activeDepth() === SearchPaneType.HISTORY}
|
||||||
onSelect={handleHistorySelect}
|
onSelect={handleHistorySelect}
|
||||||
onRemove={searchStore.removeFromHistory}
|
onRemove={searchStore.removeFromHistory}
|
||||||
onClear={searchStore.clearHistory}
|
onClear={searchStore.clearHistory}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createSignal, For } from "solid-js";
|
import { createSignal, For, onMount } from "solid-js";
|
||||||
import { useKeyboard } from "@opentui/solid";
|
import { useKeyboard } from "@opentui/solid";
|
||||||
import { SourceManager } from "./SourceManager";
|
import { SourceManager } from "./SourceManager";
|
||||||
import { useTheme } from "@/context/ThemeContext";
|
import { useTheme } from "@/context/ThemeContext";
|
||||||
@@ -6,6 +6,7 @@ import { PreferencesPanel } from "./PreferencesPanel";
|
|||||||
import { SyncPanel } from "./SyncPanel";
|
import { SyncPanel } from "./SyncPanel";
|
||||||
import { VisualizerSettings } from "./VisualizerSettings";
|
import { VisualizerSettings } from "./VisualizerSettings";
|
||||||
import { useNavigation } from "@/context/NavigationContext";
|
import { useNavigation } from "@/context/NavigationContext";
|
||||||
|
import { KeybindProvider, useKeybinds } from "@/context/KeybindContext";
|
||||||
|
|
||||||
enum SettingsPaneType {
|
enum SettingsPaneType {
|
||||||
SYNC = 1,
|
SYNC = 1,
|
||||||
@@ -27,6 +28,42 @@ const SECTIONS: Array<{ id: SettingsPaneType; label: string }> = [
|
|||||||
export function SettingsPage() {
|
export function SettingsPage() {
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const nav = useNavigation();
|
const nav = useNavigation();
|
||||||
|
const keybind = useKeybinds();
|
||||||
|
|
||||||
|
// Helper function to check if a depth is active
|
||||||
|
const isActive = (depth: SettingsPaneType): boolean => {
|
||||||
|
return nav.activeDepth() === depth;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to get the current depth as a number
|
||||||
|
const currentDepth = () => nav.activeDepth() as number;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
useKeyboard(
|
||||||
|
(keyEvent: any) => {
|
||||||
|
const isDown = keybind.match("down", keyEvent);
|
||||||
|
const isUp = keybind.match("up", keyEvent);
|
||||||
|
const isCycle = keybind.match("cycle", keyEvent);
|
||||||
|
const isSelect = keybind.match("select", keyEvent);
|
||||||
|
|
||||||
|
if (isSelect) {
|
||||||
|
nav.setActiveDepth((nav.activeDepth() % SettingsPaneCount) + 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextDepth = isDown
|
||||||
|
? (nav.activeDepth() % SettingsPaneCount) + 1
|
||||||
|
: (nav.activeDepth() - 2 + SettingsPaneCount) % SettingsPaneCount + 1;
|
||||||
|
|
||||||
|
if (isCycle) {
|
||||||
|
nav.setActiveDepth((nav.activeDepth() % SettingsPaneCount) + 1);
|
||||||
|
} else {
|
||||||
|
nav.setActiveDepth(nextDepth);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ release: false },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<box flexDirection="column" gap={1} height="100%" width="100%">
|
<box flexDirection="column" gap={1} height="100%" width="100%">
|
||||||
@@ -38,13 +75,13 @@ export function SettingsPage() {
|
|||||||
borderColor={theme.border}
|
borderColor={theme.border}
|
||||||
padding={0}
|
padding={0}
|
||||||
backgroundColor={
|
backgroundColor={
|
||||||
nav.activeDepth === section.id ? theme.primary : undefined
|
currentDepth() === section.id ? theme.primary : undefined
|
||||||
}
|
}
|
||||||
onMouseDown={() => nav.setActiveDepth(section.id)}
|
onMouseDown={() => nav.setActiveDepth(section.id)}
|
||||||
>
|
>
|
||||||
<text
|
<text
|
||||||
fg={
|
fg={
|
||||||
nav.activeDepth === section.id ? theme.text : theme.textMuted
|
currentDepth() === section.id ? theme.text : theme.textMuted
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
[{index() + 1}] {section.label}
|
[{index() + 1}] {section.label}
|
||||||
@@ -56,23 +93,23 @@ export function SettingsPage() {
|
|||||||
|
|
||||||
<box
|
<box
|
||||||
border
|
border
|
||||||
borderColor={theme.border}
|
borderColor={isActive(SettingsPaneType.SYNC) || isActive(SettingsPaneType.SOURCES) || isActive(SettingsPaneType.PREFERENCES) || isActive(SettingsPaneType.VISUALIZER) || isActive(SettingsPaneType.ACCOUNT) ? theme.accent : theme.border}
|
||||||
flexGrow={1}
|
flexGrow={1}
|
||||||
padding={1}
|
padding={1}
|
||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
gap={1}
|
gap={1}
|
||||||
>
|
>
|
||||||
{nav.activeDepth === SettingsPaneType.SYNC && <SyncPanel />}
|
{isActive(SettingsPaneType.SYNC) && <SyncPanel />}
|
||||||
{nav.activeDepth === SettingsPaneType.SOURCES && (
|
{isActive(SettingsPaneType.SOURCES) && (
|
||||||
<SourceManager focused />
|
<SourceManager focused />
|
||||||
)}
|
)}
|
||||||
{nav.activeDepth === SettingsPaneType.PREFERENCES && (
|
{isActive(SettingsPaneType.PREFERENCES) && (
|
||||||
<PreferencesPanel />
|
<PreferencesPanel />
|
||||||
)}
|
)}
|
||||||
{nav.activeDepth === SettingsPaneType.VISUALIZER && (
|
{isActive(SettingsPaneType.VISUALIZER) && (
|
||||||
<VisualizerSettings />
|
<VisualizerSettings />
|
||||||
)}
|
)}
|
||||||
{nav.activeDepth === SettingsPaneType.ACCOUNT && (
|
{isActive(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>
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const DEFAULT_KEYBINDS: KeybindsResolved = {
|
|||||||
right: ["right", "l"],
|
right: ["right", "l"],
|
||||||
cycle: ["tab"],
|
cycle: ["tab"],
|
||||||
dive: ["return"],
|
dive: ["return"],
|
||||||
|
select: ["return"],
|
||||||
out: ["esc"],
|
out: ["esc"],
|
||||||
inverseModifier: "shift",
|
inverseModifier: "shift",
|
||||||
leader: ":",
|
leader: ":",
|
||||||
|
|||||||
Reference in New Issue
Block a user