Compare commits

...

3 Commits

Author SHA1 Message Date
0c16353e2e fix nav context 2026-02-20 01:28:46 -05:00
8d350d9eb5 navigation controls + starting indicators 2026-02-20 01:13:32 -05:00
cc09786592 sort fix 2026-02-19 21:15:38 -05:00
7 changed files with 157 additions and 116 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,26 +83,43 @@ 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 (
(isDive && !isInverting) ||
(isOut && isInverting) ||
(isRight && !isInverting) ||
(isLeft && isInverting)
) {
nav.setActiveDepth(1);
}
}
if (nav.activeDepth() == 1) {
if (
(isDive && isInverting) ||
(isOut && !isInverting) ||
(isRight && isInverting) ||
(isLeft && !isInverting)
) {
nav.setActiveDepth(0);
} }
} }
}, },
@@ -117,7 +142,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,11 +178,8 @@ export function App() {
</box> </box>
)} )}
<box flexDirection="row" width="100%" height="100%"> <box flexDirection="row" width="100%" height="100%">
<TabNavigation <TabNavigation />
activeTab={nav.activeTab} {LayerGraph[nav.activeTab()]()}
onTabSelect={nav.setActiveTab}
/>
{LayerGraph[nav.activeTab]()}
</box> </box>
</box> </box>
</ErrorBoundary> </ErrorBoundary>

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,32 +13,36 @@ 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 ? theme.border : 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 >
selected={() => tab.id == props.activeTab} <SelectableText
primary selected={() => tab.id == activeTab()}
alignSelf="center" primary
> alignSelf="center"
{tab.label} >
</SelectableText> {tab.label}
</SelectableBox> </SelectableText>
</SelectableBox>
)} )}
</For> </For>
</box> </box>

View File

@@ -6,10 +6,10 @@
"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"],
"audio-toggle": ["<leader>p"], "audio-toggle": ["<leader>p"],
"audio-pause": [], "audio-pause": [],
"audio-play": [], "audio-play": [],

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

@@ -29,15 +29,9 @@ export const { use: useNavigation, provider: NavigationProvider } =
}; };
return { return {
get activeTab() { activeTab,
return activeTab(); activeDepth,
}, inputFocused,
get activeDepth() {
return activeDepth();
},
get inputFocused() {
return inputFocused();
},
setActiveTab, setActiveTab,
setActiveDepth, setActiveDepth,
setInputFocused, setInputFocused,

View File

@@ -12,38 +12,32 @@ 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,
} }
export const FeedPaneCount = 1; export const FeedPaneCount = 1;
/** Episodes to load per batch */
const ITEMS_PER_BATCH = 50; 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,7 +45,13 @@ export function FeedPage() {
groups[dateKey].push(item); groups[dateKey].push(item);
} }
return groups; return Object.entries(groups).sort(([a, _aItems], [b, _bItems]) => {
// Convert date strings back to Date objects for proper chronological sorting
const dateA = new Date(a);
const dateB = new Date(b);
// Sort in descending order (newest first)
return dateB.getTime() - dateA.getTime();
});
}; };
const formatDuration = (seconds: number): string => { const formatDuration = (seconds: number): string => {
@@ -61,19 +61,17 @@ export function FeedPage() {
return `${mins}m`; return `${mins}m`;
}; };
const { theme } = useTheme();
return ( return (
<box <box
border
borderColor={
nav.activeDepth() !== FeedPaneType.FEED ? theme.border : theme.accent
}
backgroundColor={theme.background} backgroundColor={theme.background}
flexDirection="column" flexDirection="column"
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={
@@ -84,50 +82,60 @@ export function FeedPage() {
</box> </box>
} }
> >
<scrollbox height="100%" focused={nav.activeDepth == FeedPaneType.FEED}> <scrollbox
<For each={Object.entries(groupEpisodesByDate()).sort(([a], [b]) => b.localeCompare(a))}> height="100%"
{([date, episodes]) => ( focused={nav.activeDepth() == FeedPaneType.FEED}
>
<For each={groupEpisodesByDate()}>
{([date, items]) => (
<box flexDirection="column" gap={1} padding={1}> <box flexDirection="column" gap={1} padding={1}>
<SelectableText selected={() => false} primary> <SelectableText selected={() => false} primary>
{date} {date}
</SelectableText> </SelectableText>
<For each={episodes}> <For each={items}>
{(item) => ( {(item) => {
<SelectableBox const isSelected = () => {
selected={() => false} if (
flexDirection="column" nav.activeTab() == TABS.FEED &&
gap={0} nav.activeDepth() == FeedPaneType.FEED &&
paddingLeft={1} selectedEpisodeID() &&
paddingRight={1} selectedEpisodeID() === item.episode.id
paddingTop={0} ) {
paddingBottom={0} return true;
onMouseDown={() => { }
// Selection is handled by App's keyboard navigation return false;
}} };
> return (
<SelectableText selected={() => false} primary> <SelectableBox
{item.episode.title} selected={isSelected}
</SelectableText> flexDirection="column"
<box flexDirection="row" gap={2} paddingLeft={2}> gap={0}
<SelectableText selected={() => false} primary> paddingLeft={1}
{item.feed.podcast.title} paddingRight={1}
paddingTop={0}
paddingBottom={0}
onMouseDown={() => {
// Selection is handled by App's keyboard navigation
}}
>
<SelectableText selected={isSelected} primary>
{item.episode.title}
</SelectableText> </SelectableText>
<SelectableText selected={() => false} tertiary> <box flexDirection="row" gap={2} paddingLeft={2}>
{formatDuration(item.episode.duration)} <SelectableText selected={isSelected} primary>
</SelectableText> {item.feed.podcast.title}
</box> </SelectableText>
</SelectableBox> <SelectableText selected={isSelected} tertiary>
)} {formatDuration(item.episode.duration)}
</SelectableText>
</box>
</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"],