navigation controls + starting indicators

This commit is contained in:
2026-02-20 01:13:32 -05:00
parent cc09786592
commit 8d350d9eb5
6 changed files with 128 additions and 107 deletions

View File

@@ -28,7 +28,15 @@ export function App() {
const audio = useAudio(); const audio = useAudio();
const toast = useToast(); const toast = useToast();
const renderer = useRenderer(); const renderer = useRenderer();
const { theme } = useTheme(); const themeContext = useTheme();
const theme = themeContext.theme;
// Create a reactive expression for background color
const backgroundColor = () => {
return themeContext.selected === "system"
? "transparent"
: themeContext.theme.surface;
};
const keybind = useKeybinds(); const keybind = useKeybinds();
const audioNav = useAudioNavStore(); const audioNav = useAudioNavStore();
@@ -62,11 +70,11 @@ export function App() {
useKeyboard( useKeyboard(
(keyEvent) => { (keyEvent) => {
const isCycle = keybind.match("cycle", keyEvent);
const isUp = keybind.match("up", keyEvent); const isUp = keybind.match("up", keyEvent);
const isDown = keybind.match("down", keyEvent); const isDown = keybind.match("down", keyEvent);
const isLeft = keybind.match("left", keyEvent); const isLeft = keybind.match("left", keyEvent);
const isRight = keybind.match("right", keyEvent); const isRight = keybind.match("right", keyEvent);
const isCycle = keybind.match("cycle", keyEvent);
const isDive = keybind.match("dive", keyEvent); const isDive = keybind.match("dive", keyEvent);
const isOut = keybind.match("out", keyEvent); const isOut = keybind.match("out", keyEvent);
const isToggle = keybind.match("audio-toggle", keyEvent); const isToggle = keybind.match("audio-toggle", keyEvent);
@@ -75,27 +83,31 @@ 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({ const isInverting = keybind.isInverting(keyEvent);
up: isUp,
down: isDown,
left: isLeft,
right: isRight,
cycle: isCycle,
dive: isDive,
out: isOut,
audioToggle: isToggle,
audioNext: isNext,
audioPrev: isPrev,
audioSeekForward: isSeekForward,
audioSeekBackward: isSeekBackward,
quit: isQuit,
});
// only handling top navigation here, cycle through tabs, just to high priority(player) all else to be handled in each tab // only handling top navigation here, cycle through tabs, just to high priority(player) all else to be handled in each tab
if (nav.activeDepth == 0) { if (nav.activeDepth == 0) {
if (isCycle) { if (
(isCycle && !isInverting) ||
(isDown && !isInverting) ||
(isUp && isInverting)
) {
nav.nextTab(); nav.nextTab();
return;
} }
if (
(isCycle && isInverting) ||
(isDown && isInverting) ||
(isUp && !isInverting)
) {
nav.prevTab();
return;
}
if ((isRight && !isInverting) || (isLeft && isInverting)) {
nav.setActiveDepth(1);
}
}
if (nav.activeDepth == 1) {
} }
}, },
{ release: false }, { release: false },
@@ -117,7 +129,11 @@ export function App() {
flexDirection="column" flexDirection="column"
width="100%" width="100%"
height="100%" height="100%"
backgroundColor={theme.surface} backgroundColor={
themeContext.selected === "system"
? "transparent"
: themeContext.theme.surface
}
> >
<LoadingIndicator /> <LoadingIndicator />
{DEBUG && ( {DEBUG && (
@@ -149,10 +165,7 @@ export function App() {
</box> </box>
)} )}
<box flexDirection="row" width="100%" height="100%"> <box flexDirection="row" width="100%" height="100%">
<TabNavigation <TabNavigation />
activeTab={nav.activeTab}
onTabSelect={nav.setActiveTab}
/>
{LayerGraph[nav.activeTab]()} {LayerGraph[nav.activeTab]()}
</box> </box>
</box> </box>

View File

@@ -1,12 +1,8 @@
import { useTheme } from "@/context/ThemeContext"; import { useTheme } from "@/context/ThemeContext";
import { TABS } from "@/utils/navigation"; import { TABS, TabsCount } from "@/utils/navigation";
import { For } from "solid-js"; import { For } from "solid-js";
import { SelectableBox, SelectableText } from "@/components/Selectable"; import { SelectableBox, SelectableText } from "@/components/Selectable";
import { useNavigation } from "@/context/NavigationContext";
interface TabNavigationProps {
activeTab: TABS;
onTabSelect: (tab: TABS) => void;
}
export const tabs: TabDefinition[] = [ export const tabs: TabDefinition[] = [
{ id: TABS.FEED, label: "Feed" }, { id: TABS.FEED, label: "Feed" },
@@ -17,26 +13,30 @@ export const tabs: TabDefinition[] = [
{ id: TABS.SETTINGS, label: "Settings" }, { id: TABS.SETTINGS, label: "Settings" },
]; ];
export function TabNavigation(props: TabNavigationProps) { export function TabNavigation() {
const { theme } = useTheme(); const { theme } = useTheme();
const { activeTab, setActiveTab, activeDepth } = useNavigation();
return ( return (
<box <box
backgroundColor={theme.surface} border
borderColor={activeDepth !== 0 ? "transparent" : theme.accent}
backgroundColor={"transparent"}
style={{ style={{
flexDirection: "column", flexDirection: "column",
width: 10, width: 12,
flexGrow: 1, height: TabsCount * 3 + 2,
}} }}
> >
<For each={tabs}> <For each={tabs}>
{(tab) => ( {(tab) => (
<SelectableBox <SelectableBox
border border
selected={() => tab.id == props.activeTab} height={3}
onMouseDown={() => props.onTabSelect(tab.id)} selected={() => tab.id == activeTab}
onMouseDown={() => setActiveTab(tab.id)}
> >
<SelectableText <SelectableText
selected={() => tab.id == props.activeTab} selected={() => tab.id == activeTab}
primary primary
alignSelf="center" alignSelf="center"
> >

View File

@@ -6,7 +6,7 @@
"cycle": ["tab"], // this will cycle no matter the depth/orientation "cycle": ["tab"], // this will cycle no matter the depth/orientation
"dive": ["return"], "dive": ["return"],
"out": ["esc"], "out": ["esc"],
"inverse": ["shift"], "inverseModifier": ["shift"],
"leader": ":", // will not trigger while focused on input "leader": ":", // will not trigger while focused on input
"quit": ["<leader>q"], "quit": ["<leader>q"],
"refresh": ["<leader>r"], "refresh": ["<leader>r"],

View File

@@ -15,7 +15,7 @@ export type KeybindsResolved = {
cycle: string[]; // this will cycle no matter the depth/orientation cycle: string[]; // this will cycle no matter the depth/orientation
dive: string[]; dive: string[];
out: string[]; out: string[];
inverse: 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[];
"audio-toggle": string[]; "audio-toggle": string[];
@@ -57,7 +57,7 @@ export const { use: useKeybinds, provider: KeybindProvider } =
cycle: [], cycle: [],
dive: [], dive: [],
out: [], out: [],
inverse: [], inverseModifier: "",
leader: "", leader: "",
quit: [], quit: [],
refresh: [], refresh: [],
@@ -100,6 +100,18 @@ export const { use: useKeybinds, provider: KeybindProvider } =
return false; return false;
} }
function isInverting(evt: {
name: string;
ctrl?: boolean;
meta?: boolean;
shift?: boolean;
}) {
if (store.inverseModifier === "ctrl" && evt.ctrl) return true;
if (store.inverseModifier === "meta" && evt.meta) return true;
if (store.inverseModifier === "shift" && evt.shift) return true;
return false;
}
// Load on mount // Load on mount
onMount(() => { onMount(() => {
load().catch(() => {}); load().catch(() => {});
@@ -115,6 +127,7 @@ export const { use: useKeybinds, provider: KeybindProvider } =
save, save,
print, print,
match, match,
isInverting,
}; };
}, },
}); });

View File

@@ -12,6 +12,7 @@ import { useTheme } from "@/context/ThemeContext";
import { SelectableBox, SelectableText } from "@/components/Selectable"; 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";
enum FeedPaneType { enum FeedPaneType {
FEED = 1, FEED = 1,
@@ -23,27 +24,21 @@ const ITEMS_PER_BATCH = 50;
export function FeedPage() { export function FeedPage() {
const feedStore = useFeedStore(); const feedStore = useFeedStore();
const [isRefreshing, setIsRefreshing] = createSignal(false);
const [loadedEpisodesCount, setLoadedEpisodesCount] =
createSignal(ITEMS_PER_BATCH);
const nav = useNavigation(); const nav = useNavigation();
const { theme } = useTheme();
const [selectedEpisodeID, setSelectedEpisodeID] = createSignal<
string | undefined
>();
const allEpisodes = () => feedStore.getAllEpisodesChronological(); const allEpisodes = () => feedStore.getAllEpisodesChronological();
const paginatedEpisodes = () => {
const episodes = allEpisodes();
return episodes.slice(0, loadedEpisodesCount());
};
const formatDate = (date: Date): string => { const formatDate = (date: Date): string => {
return format(date, "MMM d, yyyy"); return format(date, "MMM d, yyyy");
}; };
const groupEpisodesByDate = () => { const groupEpisodesByDate = () => {
const groups: Record<string, Array<{ episode: Episode; feed: Feed }>> = {}; const groups: Record<string, Array<{ episode: Episode; feed: Feed }>> = {};
const episodes = paginatedEpisodes();
for (const item of episodes) { for (const item of allEpisodes()) {
const dateKey = formatDate(new Date(item.episode.pubDate)); const dateKey = formatDate(new Date(item.episode.pubDate));
if (!groups[dateKey]) { if (!groups[dateKey]) {
groups[dateKey] = []; groups[dateKey] = [];
@@ -51,8 +46,7 @@ export function FeedPage() {
groups[dateKey].push(item); groups[dateKey].push(item);
} }
return Object.entries(groups) return Object.entries(groups).sort(([a, _aItems], [b, _bItems]) => {
.sort(([a, _aItems], [b, _bItems]) => {
// Convert date strings back to Date objects for proper chronological sorting // Convert date strings back to Date objects for proper chronological sorting
const dateA = new Date(a); const dateA = new Date(a);
const dateB = new Date(b); const dateB = new Date(b);
@@ -68,7 +62,6 @@ export function FeedPage() {
return `${mins}m`; return `${mins}m`;
}; };
const { theme } = useTheme();
return ( return (
<box <box
backgroundColor={theme.background} backgroundColor={theme.background}
@@ -76,11 +69,6 @@ export function FeedPage() {
height="100%" height="100%"
width="100%" width="100%"
> >
{/* Status line */}
<Show when={isRefreshing()}>
<text fg={theme.warning}>Refreshing feeds...</text>
</Show>
<Show <Show
when={allEpisodes().length > 0} when={allEpisodes().length > 0}
fallback={ fallback={
@@ -99,9 +87,21 @@ export function FeedPage() {
{date} {date}
</SelectableText> </SelectableText>
<For each={items}> <For each={items}>
{(item) => ( {(item) => {
const isSelected = () => {
if (
nav.activeTab == TABS.FEED &&
nav.activeDepth == FeedPaneType.FEED &&
selectedEpisodeID() &&
selectedEpisodeID() === item.episode.id
) {
return true;
}
return false;
};
return (
<SelectableBox <SelectableBox
selected={() => false} selected={isSelected}
flexDirection="column" flexDirection="column"
gap={0} gap={0}
paddingLeft={1} paddingLeft={1}
@@ -112,29 +112,24 @@ export function FeedPage() {
// Selection is handled by App's keyboard navigation // Selection is handled by App's keyboard navigation
}} }}
> >
<SelectableText selected={() => false} primary> <SelectableText selected={isSelected} primary>
{item.episode.title} {item.episode.title}
</SelectableText> </SelectableText>
<box flexDirection="row" gap={2} paddingLeft={2}> <box flexDirection="row" gap={2} paddingLeft={2}>
<SelectableText selected={() => false} primary> <SelectableText selected={isSelected} primary>
{item.feed.podcast.title} {item.feed.podcast.title}
</SelectableText> </SelectableText>
<SelectableText selected={() => false} tertiary> <SelectableText selected={isSelected} tertiary>
{formatDuration(item.episode.duration)} {formatDuration(item.episode.duration)}
</SelectableText> </SelectableText>
</box> </box>
</SelectableBox> </SelectableBox>
)} );
}}
</For> </For>
</box> </box>
)} )}
</For> </For>
{/* Loading indicator */}
<Show when={feedStore.isLoadingMore()}>
<box padding={1}>
<LoadingIndicator />
</box>
</Show>
</scrollbox> </scrollbox>
</Show> </Show>
</box> </box>

View File

@@ -28,7 +28,7 @@ const DEFAULT_KEYBINDS: KeybindsResolved = {
cycle: ["tab"], cycle: ["tab"],
dive: ["return"], dive: ["return"],
out: ["esc"], out: ["esc"],
inverse: ["shift"], inverseModifier: "shift",
leader: ":", leader: ":",
quit: ["<leader>q"], quit: ["<leader>q"],
"audio-toggle": ["<leader>p"], "audio-toggle": ["<leader>p"],