This commit is contained in:
2026-02-10 15:30:53 -05:00
parent f707594d0c
commit 3d5bc84550
18 changed files with 89 additions and 60 deletions

View File

@@ -1,4 +1,5 @@
import type { TabId } from "./Tab"
import { useTheme } from "@/context/ThemeContext"
type NavigationProps = {
activeTab: TabId
@@ -6,9 +7,10 @@ type NavigationProps = {
}
export function Navigation(props: NavigationProps) {
const { theme } = useTheme();
return (
<box style={{ flexDirection: "row", width: "100%", height: 1 }}>
<text>
<text fg={theme.text}>
{props.activeTab === "feed" ? "[" : " "}Feed{props.activeTab === "feed" ? "]" : " "}
<span> </span>
{props.activeTab === "shows" ? "[" : " "}My Shows{props.activeTab === "shows" ? "]" : " "}

View File

@@ -1,24 +1,26 @@
import { shortcuts } from "@/config/shortcuts";
import { useTheme } from "@/context/ThemeContext";
export function ShortcutHelp() {
const { theme } = useTheme();
return (
<box border title="Shortcuts" style={{ padding: 1 }}>
<box style={{ flexDirection: "column" }}>
<box style={{ flexDirection: "row" }}>
<text>{shortcuts[0]?.keys ?? ""} </text>
<text>{shortcuts[0]?.action ?? ""}</text>
<text fg={theme.text}>{shortcuts[0]?.keys ?? ""} </text>
<text fg={theme.text}>{shortcuts[0]?.action ?? ""}</text>
</box>
<box style={{ flexDirection: "row" }}>
<text>{shortcuts[1]?.keys ?? ""} </text>
<text>{shortcuts[1]?.action ?? ""}</text>
<text fg={theme.text}>{shortcuts[1]?.keys ?? ""} </text>
<text fg={theme.text}>{shortcuts[1]?.action ?? ""}</text>
</box>
<box style={{ flexDirection: "row" }}>
<text>{shortcuts[2]?.keys ?? ""} </text>
<text>{shortcuts[2]?.action ?? ""}</text>
<text fg={theme.text}>{shortcuts[2]?.keys ?? ""} </text>
<text fg={theme.text}>{shortcuts[2]?.action ?? ""}</text>
</box>
<box style={{ flexDirection: "row" }}>
<text>{shortcuts[3]?.keys ?? ""} </text>
<text>{shortcuts[3]?.action ?? ""}</text>
<text fg={theme.text}>{shortcuts[3]?.keys ?? ""} </text>
<text fg={theme.text}>{shortcuts[3]?.action ?? ""}</text>
</box>
</box>
</box>

View File

@@ -129,10 +129,10 @@ export function FeedDetail(props: FeedDetailProps) {
{/* Episodes header */}
<box flexDirection="row" justifyContent="space-between">
<text>
<text fg={theme.text}>
<strong>Episodes</strong>
</text>
<text fg="gray">({episodes().length} total)</text>
<text fg={theme.textMuted}>({episodes().length} total)</text>
</box>
{/* Episode list */}

View File

@@ -29,7 +29,7 @@ export function PlayerPage(props: PageProps) {
return (
<box flexDirection="column" gap={1} width="100%">
<box flexDirection="row" justifyContent="space-between">
<text>
<text fg={theme.text}>
<strong>Now Playing</strong>
</text>
<text fg={theme.muted}>
@@ -40,7 +40,7 @@ export function PlayerPage(props: PageProps) {
{audio.error() && <text fg={theme.error}>{audio.error()}</text>}
<box 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>

View File

@@ -15,6 +15,7 @@ import {
} from "@/utils/cavacore";
import { AudioStreamReader } from "@/utils/audio-stream-reader";
import { useAudio } from "@/hooks/useAudio";
import { useTheme } from "@/context/ThemeContext";
// ── Types ────────────────────────────────────────────────────────────
@@ -44,6 +45,7 @@ const SAMPLES_PER_FRAME = 512;
// ── Component ────────────────────────────────────────────────────────
export function RealtimeWaveform(props: RealtimeWaveformProps) {
const { theme } = useTheme();
const audio = useAudio();
// Frequency bar values (0.01.0 per bar)
@@ -247,7 +249,7 @@ export function RealtimeWaveform(props: RealtimeWaveformProps) {
};
return (
<box border padding={1} onMouseDown={handleClick}>
<box border borderColor={theme.border} padding={1} onMouseDown={handleClick}>
{renderLine()}
</box>
);

View File

@@ -62,7 +62,7 @@ export function SearchPage(props: PageProps) {
<box flexDirection="column" height="100%" gap={1} width="100%">
{/* Search Header */}
<box flexDirection="column" gap={1}>
<text>
<text fg={theme.text}>
<strong>Search Podcasts</strong>
</text>
@@ -101,7 +101,7 @@ export function SearchPage(props: PageProps) {
{/* Main Content - Results or History */}
<box flexDirection="row" height="100%" gap={2}>
{/* Results Panel */}
<box flexDirection="column" flexGrow={1} border>
<box flexDirection="column" flexGrow={1} border borderColor={theme.border}>
<box padding={1}>
<text
fg={props.depth() === SearchPaneType.RESULTS ? theme.primary : theme.muted}
@@ -134,7 +134,7 @@ export function SearchPage(props: PageProps) {
</box>
{/* History Sidebar */}
<box width={30} border>
<box width={30} border borderColor={theme.border}>
<box padding={1} flexDirection="column">
<box paddingBottom={1}>
<text

View File

@@ -6,19 +6,21 @@ const createSignal = <T,>(value: T): [() => T, (next: T) => void] => {
}
import { SyncStatus } from "./SyncStatus"
import { useTheme } from "@/context/ThemeContext"
export function ExportDialog() {
const { theme } = useTheme();
const filename = createSignal("podcast-sync.json")
const format = createSignal<"json" | "xml">("json")
return (
<box border title="Export" style={{ padding: 1, flexDirection: "column", gap: 1 }}>
<box style={{ flexDirection: "row", gap: 1 }}>
<text>File:</text>
<text fg={theme.text}>File:</text>
<input value={filename[0]()} onInput={filename[1]} style={{ width: 30 }} />
</box>
<box style={{ flexDirection: "row", gap: 1 }}>
<text>Format:</text>
<text fg={theme.text}>Format:</text>
<tab_select
options={[
{ name: "JSON", description: "Portable" },
@@ -27,8 +29,8 @@ export function ExportDialog() {
onSelect={(index) => format[1](index === 0 ? "json" : "xml")}
/>
</box>
<box border>
<text>Export {format[0]()} to {filename[0]()}</text>
<box border borderColor={theme.border}>
<text fg={theme.text}>Export {format[0]()} to {filename[0]()}</text>
</box>
<SyncStatus />
</box>

View File

@@ -1,4 +1,5 @@
import { detectFormat } from "@/utils/file-detector";
import { useTheme } from "@/context/ThemeContext";
type FilePickerProps = {
value: string;
@@ -6,6 +7,7 @@ type FilePickerProps = {
};
export function FilePicker(props: FilePickerProps) {
const { theme } = useTheme();
const format = detectFormat(props.value);
return (
@@ -16,7 +18,7 @@ export function FilePicker(props: FilePickerProps) {
placeholder="/path/to/sync-file.json"
style={{ width: 40 }}
/>
<text>Format: {format}</text>
<text fg={theme.text}>Format: {format}</text>
</box>
);
}

View File

@@ -6,15 +6,17 @@ const createSignal = <T,>(value: T): [() => T, (next: T) => void] => {
}
import { FilePicker } from "./FilePicker"
import { useTheme } from "@/context/ThemeContext"
export function ImportDialog() {
const { theme } = useTheme();
const filePath = createSignal("")
return (
<box border title="Import" style={{ padding: 1, flexDirection: "column", gap: 1 }}>
<FilePicker value={filePath[0]()} onChange={filePath[1]} />
<box border>
<text>Import selected file</text>
<box border borderColor={theme.border}>
<text fg={theme.text}>Import selected file</text>
</box>
</box>
)

View File

@@ -83,8 +83,8 @@ export function LoginScreen(props: LoginScreenProps) {
};
return (
<box flexDirection="column" border padding={2} gap={1}>
<text>
<box flexDirection="column" border borderColor={theme.border} padding={2} gap={1}>
<text fg={theme.text}>
<strong>Sign In</strong>
</text>
@@ -92,7 +92,7 @@ export function LoginScreen(props: LoginScreenProps) {
{/* Email field */}
<box flexDirection="column" gap={0}>
<text fg={focusField() === "email" ? theme.primary : undefined}>
<text fg={focusField() === "email" ? theme.primary : theme.textMuted}>
Email:
</text>
<input
@@ -107,7 +107,7 @@ export function LoginScreen(props: LoginScreenProps) {
{/* Password field */}
<box flexDirection="column" gap={0}>
<text fg={focusField() === "password" ? theme.primary : undefined}>
<text fg={focusField() === "password" ? theme.primary : theme.textMuted}>
Password:
</text>
<input
@@ -126,6 +126,7 @@ export function LoginScreen(props: LoginScreenProps) {
<box flexDirection="row" gap={2}>
<box
border
borderColor={theme.border}
padding={1}
backgroundColor={
focusField() === "submit" ? theme.primary : undefined
@@ -148,6 +149,7 @@ export function LoginScreen(props: LoginScreenProps) {
<box flexDirection="row" gap={2}>
<box
border
borderColor={theme.border}
padding={1}
backgroundColor={focusField() === "code" ? theme.primary : undefined}
>
@@ -158,6 +160,7 @@ export function LoginScreen(props: LoginScreenProps) {
<box
border
borderColor={theme.border}
padding={1}
backgroundColor={focusField() === "oauth" ? theme.primary : undefined}
>

View File

@@ -94,7 +94,7 @@ export function PreferencesPanel() {
<text fg={focusField() === "theme" ? theme.primary : theme.textMuted}>
Theme:
</text>
<box border padding={0}>
<box border borderColor={theme.border} padding={0}>
<text fg={theme.text}>
{THEME_LABELS.find((t) => t.value === settings().theme)?.label}
</text>
@@ -106,7 +106,7 @@ export function PreferencesPanel() {
<text fg={focusField() === "font" ? theme.primary : theme.textMuted}>
Font Size:
</text>
<box border padding={0}>
<box border borderColor={theme.border} padding={0}>
<text fg={theme.text}>{settings().fontSize}px</text>
</box>
<text fg={theme.textMuted}>[Left/Right]</text>
@@ -116,7 +116,7 @@ export function PreferencesPanel() {
<text fg={focusField() === "speed" ? theme.primary : theme.textMuted}>
Playback:
</text>
<box border padding={0}>
<box border borderColor={theme.border} padding={0}>
<text fg={theme.text}>{settings().playbackSpeed}x</text>
</box>
<text fg={theme.textMuted}>[Left/Right]</text>
@@ -128,7 +128,7 @@ export function PreferencesPanel() {
>
Show Explicit:
</text>
<box border padding={0}>
<box border borderColor={theme.border} padding={0}>
<text
fg={preferences().showExplicit ? theme.success : theme.textMuted}
>
@@ -142,7 +142,7 @@ export function PreferencesPanel() {
<text fg={focusField() === "auto" ? theme.primary : theme.textMuted}>
Auto Download:
</text>
<box border padding={0}>
<box border borderColor={theme.border} padding={0}>
<text
fg={preferences().autoDownload ? theme.success : theme.textMuted}
>

View File

@@ -37,6 +37,7 @@ export function SettingsPage(props: PageProps) {
{(section, index) => (
<box
border
borderColor={theme.border}
padding={0}
backgroundColor={
activeSection() === section.id ? theme.primary : undefined
@@ -55,7 +56,7 @@ export function SettingsPage(props: PageProps) {
</For>
</box>
<box border flexGrow={1} padding={1} flexDirection="column" gap={1}>
<box border borderColor={theme.border} flexGrow={1} padding={1} flexDirection="column" gap={1}>
{activeSection() === SettingsPaneType.SYNC && <SyncPanel />}
{activeSection() === SettingsPaneType.SOURCES && (
<SourceManager focused />

View File

@@ -166,12 +166,12 @@ export function SourceManager(props: SourceManagerProps) {
const sourceLanguage = () => selectedSource()?.language || "en_us";
return (
<box flexDirection="column" border padding={1} gap={1}>
<box flexDirection="column" border borderColor={theme.border} padding={1} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text>
<text fg={theme.text}>
<strong>Podcast Sources</strong>
</text>
<box border padding={0} onMouseDown={props.onClose}>
<box border borderColor={theme.border} padding={0} onMouseDown={props.onClose}>
<text fg={theme.primary}>[Esc] Close</text>
</box>
</box>
@@ -179,7 +179,7 @@ export function SourceManager(props: SourceManagerProps) {
<text fg={theme.textMuted}>Manage where to search for podcasts</text>
{/* Source list */}
<box border padding={1} flexDirection="column" gap={1}>
<box border borderColor={theme.border} padding={1} flexDirection="column" gap={1}>
<text fg={focusArea() === "list" ? theme.primary : theme.textMuted}>
Sources:
</text>
@@ -243,6 +243,7 @@ export function SourceManager(props: SourceManagerProps) {
<box flexDirection="row" gap={2}>
<box
border
borderColor={theme.border}
padding={0}
backgroundColor={
focusArea() === "country" ? theme.primary : undefined
@@ -256,6 +257,7 @@ export function SourceManager(props: SourceManagerProps) {
</box>
<box
border
borderColor={theme.border}
padding={0}
backgroundColor={
focusArea() === "language" ? theme.primary : undefined
@@ -272,6 +274,7 @@ export function SourceManager(props: SourceManagerProps) {
</box>
<box
border
borderColor={theme.border}
padding={0}
backgroundColor={
focusArea() === "explicit" ? theme.primary : undefined
@@ -293,7 +296,7 @@ export function SourceManager(props: SourceManagerProps) {
</box>
{/* Add new source form */}
<box border padding={1} flexDirection="column" gap={1}>
<box border borderColor={theme.border} padding={1} flexDirection="column" gap={1}>
<text
fg={
focusArea() === "add" || focusArea() === "url"
@@ -329,7 +332,7 @@ export function SourceManager(props: SourceManagerProps) {
/>
</box>
<box border padding={0} width={15} onMouseDown={handleAddSource}>
<box border borderColor={theme.border} padding={0} width={15} onMouseDown={handleAddSource}>
<text fg={theme.success}>[+] Add Source</text>
</box>
</box>

View File

@@ -1,14 +1,17 @@
import { useTheme } from "@/context/ThemeContext"
type SyncErrorProps = {
message: string
onRetry: () => void
}
export function SyncError(props: SyncErrorProps) {
const { theme } = useTheme();
return (
<box border title="Error" style={{ padding: 1, flexDirection: "column", gap: 1 }}>
<text>{props.message}</text>
<box border onMouseDown={props.onRetry}>
<text>Retry</text>
<text fg={theme.text}>{props.message}</text>
<box border borderColor={theme.border} onMouseDown={props.onRetry}>
<text fg={theme.text}>Retry</text>
</box>
</box>
)

View File

@@ -8,18 +8,20 @@ const createSignal = <T,>(value: T): [() => T, (next: T) => void] => {
import { ImportDialog } from "./ImportDialog"
import { ExportDialog } from "./ExportDialog"
import { SyncStatus } from "./SyncStatus"
import { useTheme } from "@/context/ThemeContext"
export function SyncPanel() {
const { theme } = useTheme();
const mode = createSignal<"import" | "export" | null>(null)
return (
<box style={{ flexDirection: "column", gap: 1 }}>
<box style={{ flexDirection: "row", gap: 1 }}>
<box border onMouseDown={() => mode[1]("import")}>
<text>Import</text>
<box border borderColor={theme.border} onMouseDown={() => mode[1]("import")}>
<text fg={theme.text}>Import</text>
</box>
<box border onMouseDown={() => mode[1]("export")}>
<text>Export</text>
<box border borderColor={theme.border} onMouseDown={() => mode[1]("export")}>
<text fg={theme.text}>Export</text>
</box>
</box>
<SyncStatus />

View File

@@ -1,8 +1,11 @@
import { useTheme } from "@/context/ThemeContext"
type SyncProgressProps = {
value: number
}
export function SyncProgress(props: SyncProgressProps) {
const { theme } = useTheme();
const width = 30
let filled = (props.value / 100) * width
filled = filled >= 0 ? filled : 0
@@ -18,8 +21,8 @@ export function SyncProgress(props: SyncProgressProps) {
return (
<box style={{ flexDirection: "column" }}>
<text>{bar}</text>
<text>{props.value}%</text>
<text fg={theme.text}>{bar}</text>
<text fg={theme.text}>{props.value}%</text>
</box>
)
}

View File

@@ -7,10 +7,12 @@ const createSignal = <T,>(value: T): [() => T, (next: T) => void] => {
import { SyncProgress } from "./SyncProgress"
import { SyncError } from "./SyncError"
import { useTheme } from "@/context/ThemeContext"
type SyncState = "idle" | "syncing" | "complete" | "error"
export function SyncStatus() {
const { theme } = useTheme();
const state = createSignal<SyncState>("idle")
const message = createSignal("Idle")
const progress = createSignal(0)
@@ -35,15 +37,15 @@ export function SyncStatus() {
}
return (
<box border title="Sync Status" style={{ padding: 1, flexDirection: "column", gap: 1 }}>
<box border title="Sync Status" borderColor={theme.border} style={{ padding: 1, flexDirection: "column", gap: 1 }}>
<box style={{ flexDirection: "row", gap: 1 }}>
<text>Status:</text>
<text>{message[0]()}</text>
<text fg={theme.text}>Status:</text>
<text fg={theme.text}>{message[0]()}</text>
</box>
<SyncProgress value={progress[0]()} />
{state[0]() === "error" ? <SyncError message={message[0]()} onRetry={() => toggle()} /> : null}
<box border onMouseDown={toggle}>
<text>Cycle Status</text>
<box border borderColor={theme.border} onMouseDown={toggle}>
<text fg={theme.text}>Cycle Status</text>
</box>
</box>
)

View File

@@ -99,7 +99,7 @@ export function VisualizerSettings() {
<text fg={focusField() === "bars" ? theme.primary : theme.textMuted}>
Bars:
</text>
<box border padding={0}>
<box border borderColor={theme.border} padding={0}>
<text fg={theme.text}>{viz().bars}</text>
</box>
<text fg={theme.textMuted}>[Left/Right +/-8]</text>
@@ -113,7 +113,7 @@ export function VisualizerSettings() {
>
Auto Sensitivity:
</text>
<box border padding={0}>
<box border borderColor={theme.border} padding={0}>
<text
fg={viz().sensitivity === 1 ? theme.success : theme.textMuted}
>
@@ -127,7 +127,7 @@ export function VisualizerSettings() {
<text fg={focusField() === "noise" ? theme.primary : theme.textMuted}>
Noise Reduction:
</text>
<box border padding={0}>
<box border borderColor={theme.border} padding={0}>
<text fg={theme.text}>{viz().noiseReduction.toFixed(2)}</text>
</box>
<text fg={theme.textMuted}>[Left/Right +/-0.05]</text>
@@ -139,7 +139,7 @@ export function VisualizerSettings() {
>
Low Cutoff:
</text>
<box border padding={0}>
<box border borderColor={theme.border} padding={0}>
<text fg={theme.text}>{viz().lowCutOff} Hz</text>
</box>
<text fg={theme.textMuted}>[Left/Right +/-10]</text>
@@ -151,7 +151,7 @@ export function VisualizerSettings() {
>
High Cutoff:
</text>
<box border padding={0}>
<box border borderColor={theme.border} padding={0}>
<text fg={theme.text}>{viz().highCutOff} Hz</text>
</box>
<text fg={theme.textMuted}>[Left/Right +/-500]</text>