diff --git a/src/App.tsx b/src/App.tsx
index 8042780..fbb6da4 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -13,7 +13,8 @@ import { useRenderer } from "@opentui/solid";
import type { AuthScreen } from "@/types/auth";
import type { Episode } from "@/types/episode";
import { DIRECTION, LayerGraph, TABS } from "./utils/navigation";
-import { useTheme } from "./context/ThemeContext";
+import { useTheme, ThemeProvider } from "./context/ThemeContext";
+import { KeybindProvider, useKeybinds } from "./context/KeybindContext";
const DEBUG = import.meta.env.DEBUG;
@@ -34,6 +35,7 @@ export function App() {
const toast = useToast();
const renderer = useRenderer();
const { theme } = useTheme();
+ const keybind = useKeybinds();
useMultimediaKeys({
playerFocused: () => activeTab() === TABS.PLAYER && layerDepth() > 0,
@@ -62,66 +64,116 @@ export function App() {
});
});
+ // Handle keyboard input with dynamic keybinds
useKeyboard(
(keyEvent) => {
- //handle intra layer navigation
- if (keyEvent.name == "up" || keyEvent.name) {
+ const name = keyEvent.name;
+
+ // Navigation: up/down
+ if (keybind.match("up", keyEvent) || keybind.match("down", keyEvent)) {
+ // TODO: Implement navigation logic
+ }
+
+ // Navigation: left/right
+ if (keybind.match("left", keyEvent) || keybind.match("right", keyEvent)) {
+ // TODO: Implement navigation logic
+ }
+
+ // Cycle through options
+ if (keybind.match("cycle", keyEvent)) {
+ // TODO: Implement cycle logic
+ }
+
+ // Dive into content
+ if (keybind.match("dive", keyEvent)) {
+ // TODO: Implement dive logic
+ }
+
+ // Out of content
+ if (keybind.match("out", keyEvent)) {
+ setActiveDepth((prev) => Math.max(0, prev - 1));
+ return;
+ }
+
+ // Audio controls
+ if (keybind.match("audio-toggle", keyEvent)) {
+ audio.togglePlayback();
+ return;
+ }
+
+ if (keybind.match("audio-next", keyEvent)) {
+ audio.seekRelative(30); // Skip forward 30 seconds
+ return;
+ }
+
+ if (keybind.match("audio-prev", keyEvent)) {
+ audio.seekRelative(-30); // Skip back 30 seconds
+ return;
+ }
+
+ // Quit application
+ if (keybind.match("quit", keyEvent)) {
+ process.exit(0);
}
},
- { release: false }, // Not strictly necessary
+ { release: false },
);
return (
- (
-
-
- Error: {err?.message ?? String(err)}
- {"\n"}
- Press a number key (1-6) to switch tabs.
-
-
- )}
- >
- {DEBUG && (
-
- █
- █
- █
- █
- █
- █
- █
- █
- █
- █
- █
- █
- █
- █
- █
- █
- █
- █
- █
- █
- █
- █
- █
- █
-
- )}
-
-
-
- {LayerGraph[activeTab()]({ depth: activeDepth })}
- {/**TODO: Contextual controls based on tab/depth**/}
-
-
+
+
+ (
+
+
+ Error: {err?.message ?? String(err)}
+ {"\n"}
+ Press a number key (1-6) to switch tabs.
+
+
+ )}
+ >
+ {DEBUG && (
+
+ █
+ █
+ █
+ █
+ █
+ █
+ █
+ █
+ █
+ █
+ █
+ █
+ █
+ █
+ █
+ █
+ █
+ █
+ █
+ █
+ █
+ █
+ █
+ █
+
+ )}
+
+
+
+ {LayerGraph[activeTab()]({ depth: activeDepth })}
+ {/**TODO: Contextual controls based on tab/depth**/}
+
+
+
+
);
}
diff --git a/src/components/Selectable.tsx b/src/components/Selectable.tsx
index 690ca89..69692f4 100644
--- a/src/components/Selectable.tsx
+++ b/src/components/Selectable.tsx
@@ -1,29 +1,28 @@
import { useTheme } from "@/context/ThemeContext";
-import type { JSXElement } from "solid-js";
+import { children as solidChildren } from "solid-js";
+import type { ParentComponent } from "solid-js";
import type { BoxOptions, TextOptions } from "@opentui/core";
-export function SelectableBox({
- selected,
- children,
- ...props
-}: {
- selected: () => boolean;
-
- children: JSXElement;
-} & BoxOptions) {
+export const SelectableBox: ParentComponent<
+ {
+ selected: () => boolean;
+ } & BoxOptions
+> = (props) => {
const { theme } = useTheme();
+ const child = solidChildren(() => props.children);
+
return (
- {children}
+ {child()}
);
-}
+};
enum ColorSet {
PRIMARY,
@@ -45,35 +44,31 @@ function getTextColor(set: ColorSet, selected: () => boolean) {
}
}
-export function SelectableText({
- selected,
- children,
- primary,
- secondary,
- tertiary,
- ...props
-}: {
- selected: () => boolean;
- primary?: boolean;
- secondary?: boolean;
- tertiary?: boolean;
- children: JSXElement;
-} & TextOptions) {
+export const SelectableText: ParentComponent<
+ {
+ selected: () => boolean;
+ primary?: boolean;
+ secondary?: boolean;
+ tertiary?: boolean;
+ } & TextOptions
+> = (props) => {
+ const child = solidChildren(() => props.children);
+
return (
- {children}
+ {child()}
);
-}
+};
diff --git a/src/pages/Feed/FeedPage.tsx b/src/pages/Feed/FeedPage.tsx
index db46213..4d3873e 100644
--- a/src/pages/Feed/FeedPage.tsx
+++ b/src/pages/Feed/FeedPage.tsx
@@ -5,6 +5,7 @@
import { createSignal, For, Show } from "solid-js";
import { useFeedStore } from "@/stores/feed";
+import { useKeyboard } from "@opentui/solid";
import { format } from "date-fns";
import type { Episode } from "@/types/episode";
import type { Feed } from "@/types/feed";
@@ -18,10 +19,14 @@ enum FeedPaneType {
}
export const FeedPaneCount = 1;
+/** Episodes to load per batch */
+const ITEMS_PER_BATCH = 50;
+
export function FeedPage(props: PageProps) {
const feedStore = useFeedStore();
const [selectedIndex, setSelectedIndex] = createSignal(0);
const [isRefreshing, setIsRefreshing] = createSignal(false);
+ const [loadedEpisodesCount, setLoadedEpisodesCount] = createSignal(ITEMS_PER_BATCH);
const allEpisodes = () => feedStore.getAllEpisodesChronological();
@@ -29,9 +34,14 @@ export function FeedPage(props: PageProps) {
return format(date, "MMM d, yyyy");
};
+ const paginatedEpisodes = () => {
+ const episodes = allEpisodes();
+ return episodes.slice(0, loadedEpisodesCount());
+ };
+
const episodesByDate = () => {
const groups: Record = {};
- const sortedEpisodes = allEpisodes();
+ const sortedEpisodes = paginatedEpisodes();
for (const episode of sortedEpisodes) {
const dateKey = formatDate(new Date(episode.episode.pubDate));
@@ -54,6 +64,11 @@ export function FeedPage(props: PageProps) {
setIsRefreshing(false);
};
+ const handleScrollDown = async () => {
+ if (feedStore.isLoadingMore() || !feedStore.hasMoreEpisodes()) return;
+ await feedStore.loadMoreEpisodes();
+ };
+
const { theme } = useTheme();
return (
+ {/* Loading indicator */}
+
+
+ Loading more episodes...
+
+
diff --git a/src/utils/system-theme.ts b/src/utils/system-theme.ts
index d9d4ee9..2fc4420 100644
--- a/src/utils/system-theme.ts
+++ b/src/utils/system-theme.ts
@@ -64,6 +64,19 @@ export function generateSystemTheme(
const diffAddedLineNumberBg = tint(grays[3], ansi.green, diffAlpha);
const diffRemovedLineNumberBg = tint(grays[3], ansi.red, diffAlpha);
+ // Create darker shades for selected text colors to ensure contrast
+ const darken = (color: RGBA, factor: number = 0.6) => {
+ return RGBA.fromInts(
+ Math.round(color.r * 255 * factor),
+ Math.round(color.g * 255 * factor),
+ Math.round(color.b * 255 * factor)
+ );
+ };
+
+ const selectedPrimary = darken(ansi.cyan, isDark ? 0.4 : 0.6);
+ const selectedSecondary = darken(ansi.magenta, isDark ? 0.4 : 0.6);
+ const selectedTertiary = darken(textMuted, isDark ? 0.5 : 0.5);
+
return {
theme: {
primary: ansi.cyan,
@@ -75,6 +88,12 @@ export function generateSystemTheme(
info: ansi.cyan,
text: fg,
textMuted,
+ textPrimary: fg,
+ textSecondary: textMuted,
+ textTertiary: textMuted,
+ textSelectedPrimary: selectedPrimary,
+ textSelectedSecondary: selectedSecondary,
+ textSelectedTertiary: selectedTertiary,
selectedListItemText: bg,
background: transparent,
backgroundPanel: grays[2],