This commit is contained in:
2026-02-04 09:39:58 -05:00
parent bd4747679d
commit f7df578461
26 changed files with 907 additions and 783 deletions

View File

@@ -28,10 +28,8 @@ export function CategoryFilter(props: CategoryFilterProps) {
backgroundColor={isSelected() ? "#444" : undefined}
onMouseDown={() => props.onSelect?.(category.id)}
>
<text>
<span fg={isSelected() ? "cyan" : "gray"}>
{category.icon} {category.name}
</span>
<text fg={isSelected() ? "cyan" : "gray"}>
{category.icon} {category.name}
</text>
</box>
)

View File

@@ -96,47 +96,27 @@ export function CodeValidation(props: CodeValidationProps) {
}
return (
<box
flexDirection="column"
border
padding={2}
gap={1}
onKeyPress={props.focused ? handleKeyPress : undefined}
>
<box flexDirection="column" border padding={2} gap={1}>
<text>
<strong>Enter Sync Code</strong>
</text>
<box height={1} />
<text>
<span fg="gray">
Enter your 8-character sync code to link your account.
</span>
</text>
<text>
<span fg="gray">
You can get this code from the web portal.
</span>
</text>
<text fg="gray">Enter your 8-character sync code to link your account.</text>
<text fg="gray">You can get this code from the web portal.</text>
<box height={1} />
{/* Code display */}
<box flexDirection="column" gap={0}>
<text>
<span fg={focusField() === "code" ? "cyan" : undefined}>
Code ({codeProgress()}):
</span>
<text fg={focusField() === "code" ? "cyan" : undefined}>
Code ({codeProgress()}):
</text>
<box border padding={1}>
<text>
<span
fg={code().length === AUTH_CONFIG.codeValidation.codeLength ? "green" : "yellow"}
>
{codeDisplay()}
</span>
<text fg={code().length === AUTH_CONFIG.codeValidation.codeLength ? "green" : "yellow"}>
{codeDisplay()}
</text>
</box>
@@ -150,9 +130,7 @@ export function CodeValidation(props: CodeValidationProps) {
/>
{codeError() && (
<text>
<span fg="red">{codeError()}</span>
</text>
<text fg="red">{codeError()}</text>
)}
</box>
@@ -165,10 +143,8 @@ export function CodeValidation(props: CodeValidationProps) {
padding={1}
backgroundColor={focusField() === "submit" ? "#333" : undefined}
>
<text>
<span fg={focusField() === "submit" ? "cyan" : undefined}>
{auth.isLoading ? "Validating..." : "[Enter] Validate Code"}
</span>
<text fg={focusField() === "submit" ? "cyan" : undefined}>
{auth.isLoading ? "Validating..." : "[Enter] Validate Code"}
</text>
</box>
@@ -177,26 +153,20 @@ export function CodeValidation(props: CodeValidationProps) {
padding={1}
backgroundColor={focusField() === "back" ? "#333" : undefined}
>
<text>
<span fg={focusField() === "back" ? "yellow" : "gray"}>
[Esc] Back to Login
</span>
<text fg={focusField() === "back" ? "yellow" : "gray"}>
[Esc] Back to Login
</text>
</box>
</box>
{/* Auth error message */}
{auth.error && (
<text>
<span fg="red">{auth.error.message}</span>
</text>
<text fg="red">{auth.error.message}</text>
)}
<box height={1} />
<text>
<span fg="gray">Tab to navigate, Enter to select, Esc to go back</span>
</text>
<text fg="gray">Tab to navigate, Enter to select, Esc to go back</text>
</box>
)
}

View File

@@ -116,19 +116,15 @@ export function DiscoverPage(props: DiscoverPageProps) {
<box flexDirection="column" height="100%" gap={1}>
{/* Header */}
<box flexDirection="row" justifyContent="space-between" alignItems="center">
<text>
<strong>Discover Podcasts</strong>
</text>
<box flexDirection="row" gap={2}>
<text>
<span fg="gray">
{discoverStore.filteredPodcasts().length} shows
</span>
<strong>Discover Podcasts</strong>
</text>
<box flexDirection="row" gap={2}>
<text fg="gray">
{discoverStore.filteredPodcasts().length} shows
</text>
<box onMouseDown={() => discoverStore.refresh()}>
<text>
<span fg="cyan">[R] Refresh</span>
</text>
<text fg="cyan">[R] Refresh</text>
</box>
</box>
</box>
@@ -136,10 +132,8 @@ export function DiscoverPage(props: DiscoverPageProps) {
{/* Category Filter */}
<box border padding={1}>
<box flexDirection="column" gap={1}>
<text>
<span fg={focusArea() === "categories" ? "cyan" : "gray"}>
Categories:
</span>
<text fg={focusArea() === "categories" ? "cyan" : "gray"}>
Categories:
</text>
<CategoryFilter
categories={discoverStore.categories}
@@ -152,17 +146,15 @@ export function DiscoverPage(props: DiscoverPageProps) {
{/* Trending Shows */}
<box flexDirection="column" flexGrow={1} border>
<box padding={1} borderBottom>
<text>
<span fg={focusArea() === "shows" ? "cyan" : "gray"}>
<box padding={1}>
<text fg={focusArea() === "shows" ? "cyan" : "gray"}>
Trending in {
DISCOVER_CATEGORIES.find(
(c) => c.id === discoverStore.selectedCategory()
)?.name ?? "All"
}
</span>
</text>
</box>
</text>
</box>
<TrendingShows
podcasts={discoverStore.filteredPodcasts()}
selectedIndex={showIndex()}
@@ -175,18 +167,10 @@ export function DiscoverPage(props: DiscoverPageProps) {
{/* Footer Hints */}
<box flexDirection="row" gap={2}>
<text>
<span fg="gray">[Tab] Switch focus</span>
</text>
<text>
<span fg="gray">[j/k] Navigate</span>
</text>
<text>
<span fg="gray">[Enter] Subscribe</span>
</text>
<text>
<span fg="gray">[R] Refresh</span>
</text>
<text fg="gray">[Tab] Switch focus</text>
<text fg="gray">[j/k] Navigate</text>
<text fg="gray">[Enter] Subscribe</text>
<text fg="gray">[R] Refresh</text>
</box>
</box>
)

View File

@@ -73,30 +73,14 @@ export function FeedDetail(props: FeedDetailProps) {
}
return (
<box
flexDirection="column"
gap={1}
onKeyPress={props.focused ? handleKeyPress : undefined}
>
<box flexDirection="column" gap={1}>
{/* Header with back button */}
<box flexDirection="row" justifyContent="space-between">
<box
border
padding={0}
onMouseDown={props.onBack}
>
<text>
<span fg="cyan">[Esc] Back</span>
</text>
<box border padding={0} onMouseDown={props.onBack}>
<text fg="cyan">[Esc] Back</text>
</box>
<box
border
padding={0}
onMouseDown={() => setShowInfo((v) => !v)}
>
<text>
<span fg="cyan">[i] {showInfo() ? "Hide" : "Show"} Info</span>
</text>
<box border padding={0} onMouseDown={() => setShowInfo((v) => !v)}>
<text fg="cyan">[i] {showInfo() ? "Hide" : "Show"} Info</text>
</box>
</box>
@@ -107,37 +91,31 @@ export function FeedDetail(props: FeedDetailProps) {
<strong>{props.feed.customName || props.feed.podcast.title}</strong>
</text>
{props.feed.podcast.author && (
<text>
<span fg="gray">by </span>
<span fg="cyan">{props.feed.podcast.author}</span>
</text>
<box flexDirection="row" gap={1}>
<text fg="gray">by</text>
<text fg="cyan">{props.feed.podcast.author}</text>
</box>
)}
<box height={1} />
<text>
<span fg="gray">
{props.feed.podcast.description?.slice(0, 200)}
{(props.feed.podcast.description?.length || 0) > 200 ? "..." : ""}
</span>
<text fg="gray">
{props.feed.podcast.description?.slice(0, 200)}
{(props.feed.podcast.description?.length || 0) > 200 ? "..." : ""}
</text>
<box height={1} />
<box flexDirection="row" gap={2}>
<text>
<span fg="gray">Episodes: </span>
<span fg="white">{props.feed.episodes.length}</span>
</text>
<text>
<span fg="gray">Updated: </span>
<span fg="white">{formatDate(props.feed.lastUpdated)}</span>
</text>
<text>
<span fg={props.feed.visibility === "public" ? "green" : "yellow"}>
{props.feed.visibility === "public" ? "[Public]" : "[Private]"}
</span>
<box flexDirection="row" gap={1}>
<text fg="gray">Episodes:</text>
<text fg="white">{props.feed.episodes.length}</text>
</box>
<box flexDirection="row" gap={1}>
<text fg="gray">Updated:</text>
<text fg="white">{formatDate(props.feed.lastUpdated)}</text>
</box>
<text fg={props.feed.visibility === "public" ? "green" : "yellow"}>
{props.feed.visibility === "public" ? "[Public]" : "[Private]"}
</text>
{props.feed.isPinned && (
<text>
<span fg="yellow">[Pinned]</span>
</text>
<text fg="yellow">[Pinned]</text>
)}
</box>
</box>
@@ -147,8 +125,8 @@ export function FeedDetail(props: FeedDetailProps) {
<box flexDirection="row" justifyContent="space-between">
<text>
<strong>Episodes</strong>
<span fg="gray"> ({episodes().length} total)</span>
</text>
<text fg="gray">({episodes().length} total)</text>
</box>
{/* Episode list */}
@@ -168,25 +146,17 @@ export function FeedDetail(props: FeedDetailProps) {
}}
>
<box flexDirection="row" gap={1}>
<text>
<span fg={index() === selectedIndex() ? "cyan" : "gray"}>
{index() === selectedIndex() ? ">" : " "}
</span>
<text fg={index() === selectedIndex() ? "cyan" : "gray"}>
{index() === selectedIndex() ? ">" : " "}
</text>
<text>
<span fg={index() === selectedIndex() ? "white" : undefined}>
{episode.episodeNumber ? `#${episode.episodeNumber} - ` : ""}
{episode.title}
</span>
<text fg={index() === selectedIndex() ? "white" : undefined}>
{episode.episodeNumber ? `#${episode.episodeNumber} - ` : ""}
{episode.title}
</text>
</box>
<box flexDirection="row" gap={2} paddingLeft={2}>
<text>
<span fg="gray">{formatDate(episode.pubDate)}</span>
</text>
<text>
<span fg="gray">{formatDuration(episode.duration)}</span>
</text>
<text fg="gray">{formatDate(episode.pubDate)}</text>
<text fg="gray">{formatDuration(episode.duration)}</text>
</box>
</box>
)}
@@ -194,10 +164,8 @@ export function FeedDetail(props: FeedDetailProps) {
</scrollbox>
{/* Help text */}
<text>
<span fg="gray">
j/k to navigate, Enter to play, i to toggle info, Esc to go back
</span>
<text fg="gray">
j/k to navigate, Enter to play, i to toggle info, Esc to go back
</text>
</box>
)

View File

@@ -4,7 +4,8 @@
*/
import { createSignal } from "solid-js"
import type { FeedFilter, FeedVisibility, FeedSortField } from "../types/feed"
import { FeedVisibility, FeedSortField } from "../types/feed"
import type { FeedFilter } from "../types/feed"
interface FeedFilterProps {
filter: FeedFilter
@@ -45,14 +46,19 @@ export function FeedFilterComponent(props: FeedFilterProps) {
const cycleVisibility = () => {
const current = props.filter.visibility
let next: FeedVisibility | "all"
if (current === "all") next = "public"
else if (current === "public") next = "private"
if (current === "all") next = FeedVisibility.PUBLIC
else if (current === FeedVisibility.PUBLIC) next = FeedVisibility.PRIVATE
else next = "all"
props.onFilterChange({ ...props.filter, visibility: next })
}
const cycleSort = () => {
const sortOptions: FeedSortField[] = ["updated", "title", "episodeCount", "latestEpisode"]
const sortOptions: FeedSortField[] = [
FeedSortField.UPDATED,
FeedSortField.TITLE,
FeedSortField.EPISODE_COUNT,
FeedSortField.LATEST_EPISODE,
]
const currentIndex = sortOptions.indexOf(props.filter.sortBy as FeedSortField)
const nextIndex = (currentIndex + 1) % sortOptions.length
props.onFilterChange({ ...props.filter, sortBy: sortOptions[nextIndex] })
@@ -100,13 +106,7 @@ export function FeedFilterComponent(props: FeedFilterProps) {
}
return (
<box
flexDirection="column"
border
padding={1}
gap={1}
onKeyPress={props.focused ? handleKeyPress : undefined}
>
<box flexDirection="column" border padding={1} gap={1}>
<text>
<strong>Filter Feeds</strong>
</text>
@@ -118,12 +118,10 @@ export function FeedFilterComponent(props: FeedFilterProps) {
padding={0}
backgroundColor={focusField() === "visibility" ? "#333" : undefined}
>
<text>
<span fg={focusField() === "visibility" ? "cyan" : "gray"}>
Show:{" "}
</span>
<span fg={visibilityColor()}>{visibilityLabel()}</span>
</text>
<box flexDirection="row" gap={1}>
<text fg={focusField() === "visibility" ? "cyan" : "gray"}>Show:</text>
<text fg={visibilityColor()}>{visibilityLabel()}</text>
</box>
</box>
{/* Sort filter */}
@@ -132,10 +130,10 @@ export function FeedFilterComponent(props: FeedFilterProps) {
padding={0}
backgroundColor={focusField() === "sort" ? "#333" : undefined}
>
<text>
<span fg={focusField() === "sort" ? "cyan" : "gray"}>Sort: </span>
<span fg="white">{sortLabel()}</span>
</text>
<box flexDirection="row" gap={1}>
<text fg={focusField() === "sort" ? "cyan" : "gray"}>Sort:</text>
<text fg="white">{sortLabel()}</text>
</box>
</box>
{/* Pinned filter */}
@@ -144,22 +142,18 @@ export function FeedFilterComponent(props: FeedFilterProps) {
padding={0}
backgroundColor={focusField() === "pinned" ? "#333" : undefined}
>
<text>
<span fg={focusField() === "pinned" ? "cyan" : "gray"}>
Pinned:{" "}
</span>
<span fg={props.filter.pinnedOnly ? "yellow" : "gray"}>
<box flexDirection="row" gap={1}>
<text fg={focusField() === "pinned" ? "cyan" : "gray"}>Pinned:</text>
<text fg={props.filter.pinnedOnly ? "yellow" : "gray"}>
{props.filter.pinnedOnly ? "Yes" : "No"}
</span>
</text>
</text>
</box>
</box>
</box>
{/* Search box */}
<box flexDirection="row" gap={1}>
<text>
<span fg={focusField() === "search" ? "cyan" : "gray"}>Search:</span>
</text>
<text fg={focusField() === "search" ? "cyan" : "gray"}>Search:</text>
<input
value={searchValue()}
onInput={handleSearchInput}
@@ -169,9 +163,7 @@ export function FeedFilterComponent(props: FeedFilterProps) {
/>
</box>
<text>
<span fg="gray">Tab to navigate, Enter/Space to toggle</span>
</text>
<text fg="gray">Tab to navigate, Enter/Space to toggle</text>
</box>
)
}

View File

@@ -47,23 +47,15 @@ export function FeedItem(props: FeedItemProps) {
paddingLeft={1}
paddingRight={1}
>
<text>
<span fg={props.isSelected ? "cyan" : "gray"}>
{props.isSelected ? ">" : " "}
</span>
<text fg={props.isSelected ? "cyan" : "gray"}>
{props.isSelected ? ">" : " "}
</text>
<text>
<span fg={visibilityColor()}>{visibilityIcon()}</span>
</text>
<text>
<span fg={props.isSelected ? "white" : undefined}>
{props.feed.customName || props.feed.podcast.title}
</span>
<text fg={visibilityColor()}>{visibilityIcon()}</text>
<text fg={props.isSelected ? "white" : undefined}>
{props.feed.customName || props.feed.podcast.title}
</text>
{props.showEpisodeCount && (
<text>
<span fg="gray">({episodeCount()})</span>
</text>
<text fg="gray">({episodeCount()})</text>
)}
</box>
)
@@ -81,50 +73,34 @@ export function FeedItem(props: FeedItemProps) {
>
{/* Title row */}
<box flexDirection="row" gap={1}>
<text>
<span fg={props.isSelected ? "cyan" : "gray"}>
{props.isSelected ? ">" : " "}
</span>
<text fg={props.isSelected ? "cyan" : "gray"}>
{props.isSelected ? ">" : " "}
</text>
<text>
<span fg={visibilityColor()}>{visibilityIcon()}</span>
</text>
<text>
<span fg="yellow">{pinnedIndicator()}</span>
</text>
<text>
<span fg={props.isSelected ? "white" : undefined}>
<strong>{props.feed.customName || props.feed.podcast.title}</strong>
</span>
<text fg={visibilityColor()}>{visibilityIcon()}</text>
<text fg="yellow">{pinnedIndicator()}</text>
<text fg={props.isSelected ? "white" : undefined}>
<strong>{props.feed.customName || props.feed.podcast.title}</strong>
</text>
</box>
{/* Details row */}
<box flexDirection="row" gap={2} paddingLeft={4}>
{props.showEpisodeCount && (
<text>
<span fg="gray">
{episodeCount()} episodes ({unplayedCount()} new)
</span>
<text fg="gray">
{episodeCount()} episodes ({unplayedCount()} new)
</text>
)}
{props.showLastUpdated && (
<text>
<span fg="gray">
Updated: {formatDate(props.feed.lastUpdated)}
</span>
</text>
<text fg="gray">Updated: {formatDate(props.feed.lastUpdated)}</text>
)}
</box>
{/* Description (truncated) */}
{props.feed.podcast.description && (
<box paddingLeft={4} paddingTop={0}>
<text>
<span fg="gray">
{props.feed.podcast.description.slice(0, 60)}
{props.feed.podcast.description.length > 60 ? "..." : ""}
</span>
<text fg="gray">
{props.feed.podcast.description.slice(0, 60)}
{props.feed.podcast.description.length > 60 ? "..." : ""}
</text>
</box>
)}

View File

@@ -6,7 +6,8 @@
import { createSignal, For, Show } from "solid-js"
import { FeedItem } from "./FeedItem"
import { useFeedStore } from "../stores/feed"
import type { Feed, FeedFilter, FeedVisibility, FeedSortField } from "../types/feed"
import { FeedVisibility, FeedSortField } from "../types/feed"
import type { Feed } from "../types/feed"
interface FeedListProps {
focused?: boolean
@@ -67,14 +68,19 @@ export function FeedList(props: FeedListProps) {
const cycleVisibilityFilter = () => {
const current = feedStore.filter().visibility
let next: FeedVisibility | "all"
if (current === "all") next = "public"
else if (current === "public") next = "private"
if (current === "all") next = FeedVisibility.PUBLIC
else if (current === FeedVisibility.PUBLIC) next = FeedVisibility.PRIVATE
else next = "all"
feedStore.setFilter({ ...feedStore.filter(), visibility: next })
}
const cycleSortField = () => {
const sortOptions: FeedSortField[] = ["updated", "title", "episodeCount", "latestEpisode"]
const sortOptions: FeedSortField[] = [
FeedSortField.UPDATED,
FeedSortField.TITLE,
FeedSortField.EPISODE_COUNT,
FeedSortField.LATEST_EPISODE,
]
const current = feedStore.filter().sortBy as FeedSortField
const idx = sortOptions.indexOf(current)
const next = sortOptions[(idx + 1) % sortOptions.length]
@@ -112,35 +118,27 @@ export function FeedList(props: FeedListProps) {
}
return (
<box
flexDirection="column"
gap={1}
onKeyPress={props.focused ? handleKeyPress : undefined}
>
<box flexDirection="column" gap={1}>
{/* Header with filter controls */}
<box flexDirection="row" justifyContent="space-between" paddingBottom={0}>
<text>
<strong>My Feeds</strong>
<span fg="gray"> ({filteredFeeds().length} feeds)</span>
</text>
<text fg="gray">({filteredFeeds().length} feeds)</text>
<box flexDirection="row" gap={1}>
<box
border
padding={0}
onMouseDown={cycleVisibilityFilter}
>
<text>
<span fg="cyan">[f] {visibilityLabel()}</span>
</text>
<text fg="cyan">[f] {visibilityLabel()}</text>
</box>
<box
border
padding={0}
onMouseDown={cycleSortField}
>
<text>
<span fg="cyan">[s] {sortLabel()}</span>
</text>
<text fg="cyan">[s] {sortLabel()}</text>
</box>
</box>
</box>
@@ -150,25 +148,16 @@ export function FeedList(props: FeedListProps) {
when={filteredFeeds().length > 0}
fallback={
<box border padding={2}>
<text>
<span fg="gray">
No feeds found. Add podcasts from the Discover or Search tabs.
</span>
<text fg="gray">
No feeds found. Add podcasts from the Discover or Search tabs.
</text>
</box>
}
>
<scrollbox
height={15}
focused={props.focused}
selectedIndex={selectedIndex()}
>
<scrollbox height={15} focused={props.focused}>
<For each={filteredFeeds()}>
{(feed, index) => (
<box
onMouseDown={() => handleFeedClick(feed, index())}
onDoubleClick={() => handleFeedDoubleClick(feed)}
>
<box onMouseDown={() => handleFeedClick(feed, index())}>
<FeedItem
feed={feed}
isSelected={index() === selectedIndex()}
@@ -184,10 +173,8 @@ export function FeedList(props: FeedListProps) {
{/* Navigation help */}
<box paddingTop={0}>
<text>
<span fg="gray">
j/k navigate | Enter open | p pin | f filter | s sort | Click to select
</span>
<text fg="gray">
j/k navigate | Enter open | p pin | f filter | s sort | Click to select
</text>
</box>
</box>

View File

@@ -81,13 +81,7 @@ export function LoginScreen(props: LoginScreenProps) {
}
return (
<box
flexDirection="column"
border
padding={2}
gap={1}
onKeyPress={props.focused ? handleKeyPress : undefined}
>
<box flexDirection="column" border padding={2} gap={1}>
<text>
<strong>Sign In</strong>
</text>
@@ -96,11 +90,7 @@ export function LoginScreen(props: LoginScreenProps) {
{/* Email field */}
<box flexDirection="column" gap={0}>
<text>
<span fg={focusField() === "email" ? "cyan" : undefined}>
Email:
</span>
</text>
<text fg={focusField() === "email" ? "cyan" : undefined}>Email:</text>
<input
value={email()}
onInput={setEmail}
@@ -109,18 +99,14 @@ export function LoginScreen(props: LoginScreenProps) {
width={30}
/>
{emailError() && (
<text>
<span fg="red">{emailError()}</span>
</text>
<text fg="red">{emailError()}</text>
)}
</box>
{/* Password field */}
<box flexDirection="column" gap={0}>
<text>
<span fg={focusField() === "password" ? "cyan" : undefined}>
Password:
</span>
<text fg={focusField() === "password" ? "cyan" : undefined}>
Password:
</text>
<input
value={password()}
@@ -130,9 +116,7 @@ export function LoginScreen(props: LoginScreenProps) {
width={30}
/>
{passwordError() && (
<text>
<span fg="red">{passwordError()}</span>
</text>
<text fg="red">{passwordError()}</text>
)}
</box>
@@ -145,27 +129,21 @@ export function LoginScreen(props: LoginScreenProps) {
padding={1}
backgroundColor={focusField() === "submit" ? "#333" : undefined}
>
<text>
<span fg={focusField() === "submit" ? "cyan" : undefined}>
{auth.isLoading ? "Signing in..." : "[Enter] Sign In"}
</span>
<text fg={focusField() === "submit" ? "cyan" : undefined}>
{auth.isLoading ? "Signing in..." : "[Enter] Sign In"}
</text>
</box>
</box>
{/* Auth error message */}
{auth.error && (
<text>
<span fg="red">{auth.error.message}</span>
</text>
<text fg="red">{auth.error.message}</text>
)}
<box height={1} />
{/* Alternative auth options */}
<text>
<span fg="gray">Or authenticate with:</span>
</text>
<text fg="gray">Or authenticate with:</text>
<box flexDirection="row" gap={2}>
<box
@@ -173,10 +151,8 @@ export function LoginScreen(props: LoginScreenProps) {
padding={1}
backgroundColor={focusField() === "code" ? "#333" : undefined}
>
<text>
<span fg={focusField() === "code" ? "yellow" : "gray"}>
[C] Sync Code
</span>
<text fg={focusField() === "code" ? "yellow" : "gray"}>
[C] Sync Code
</text>
</box>
@@ -185,19 +161,15 @@ export function LoginScreen(props: LoginScreenProps) {
padding={1}
backgroundColor={focusField() === "oauth" ? "#333" : undefined}
>
<text>
<span fg={focusField() === "oauth" ? "yellow" : "gray"}>
[O] OAuth Info
</span>
<text fg={focusField() === "oauth" ? "yellow" : "gray"}>
[O] OAuth Info
</text>
</box>
</box>
<box height={1} />
<text>
<span fg="gray">Tab to navigate, Enter to select</span>
</text>
<text fg="gray">Tab to navigate, Enter to select</text>
</box>
)
}

View File

@@ -38,13 +38,7 @@ export function OAuthPlaceholder(props: OAuthPlaceholderProps) {
}
return (
<box
flexDirection="column"
border
padding={2}
gap={1}
onKeyPress={props.focused ? handleKeyPress : undefined}
>
<box flexDirection="column" border padding={2} gap={1}>
<text>
<strong>OAuth Authentication</strong>
</text>
@@ -52,18 +46,16 @@ export function OAuthPlaceholder(props: OAuthPlaceholderProps) {
<box height={1} />
{/* OAuth providers list */}
<text>
<span fg="cyan">Available OAuth Providers:</span>
</text>
<text fg="cyan">Available OAuth Providers:</text>
<box flexDirection="column" gap={0} paddingLeft={2}>
{OAUTH_PROVIDERS.map((provider) => (
<text>
<span fg={provider.enabled ? "green" : "gray"}>
<box flexDirection="row" gap={1}>
<text fg={provider.enabled ? "green" : "gray"}>
{provider.enabled ? "[+]" : "[-]"} {provider.name}
</span>
<span fg="gray"> - {provider.description}</span>
</text>
</text>
<text fg="gray">- {provider.description}</text>
</box>
))}
</box>
@@ -71,39 +63,33 @@ export function OAuthPlaceholder(props: OAuthPlaceholderProps) {
{/* Limitation message */}
<box border padding={1} borderColor="yellow">
<text>
<span fg="yellow">Terminal Limitations</span>
</text>
<text fg="yellow">Terminal Limitations</text>
</box>
<box paddingLeft={1}>
{OAUTH_LIMITATION_MESSAGE.split("\n").map((line) => (
<text>
<span fg="gray">{line}</span>
</text>
<text fg="gray">{line}</text>
))}
</box>
<box height={1} />
{/* Alternative options */}
<text>
<span fg="cyan">Recommended Alternatives:</span>
</text>
<text fg="cyan">Recommended Alternatives:</text>
<box flexDirection="column" gap={0} paddingLeft={2}>
<text>
<span fg="green">[1]</span>
<span fg="white"> Use a sync code from the web portal</span>
</text>
<text>
<span fg="green">[2]</span>
<span fg="white"> Use email/password authentication</span>
</text>
<text>
<span fg="green">[3]</span>
<span fg="white"> Use file-based sync (no account needed)</span>
</text>
<box flexDirection="row" gap={1}>
<text fg="green">[1]</text>
<text fg="white">Use a sync code from the web portal</text>
</box>
<box flexDirection="row" gap={1}>
<text fg="green">[2]</text>
<text fg="white">Use email/password authentication</text>
</box>
<box flexDirection="row" gap={1}>
<text fg="green">[3]</text>
<text fg="white">Use file-based sync (no account needed)</text>
</box>
</box>
<box height={1} />
@@ -115,10 +101,8 @@ export function OAuthPlaceholder(props: OAuthPlaceholderProps) {
padding={1}
backgroundColor={focusField() === "code" ? "#333" : undefined}
>
<text>
<span fg={focusField() === "code" ? "cyan" : undefined}>
[C] Enter Sync Code
</span>
<text fg={focusField() === "code" ? "cyan" : undefined}>
[C] Enter Sync Code
</text>
</box>
@@ -127,19 +111,15 @@ export function OAuthPlaceholder(props: OAuthPlaceholderProps) {
padding={1}
backgroundColor={focusField() === "back" ? "#333" : undefined}
>
<text>
<span fg={focusField() === "back" ? "yellow" : "gray"}>
[Esc] Back to Login
</span>
<text fg={focusField() === "back" ? "yellow" : "gray"}>
[Esc] Back to Login
</text>
</box>
</box>
<box height={1} />
<text>
<span fg="gray">Tab to navigate, Enter to select, Esc to go back</span>
</text>
<text fg="gray">Tab to navigate, Enter to select, Esc to go back</text>
</box>
)
}

View File

@@ -14,8 +14,7 @@ type PodcastCardProps = {
}
export function PodcastCard(props: PodcastCardProps) {
const handleSubscribeClick = (e: MouseEvent) => {
e.stopPropagation?.()
const handleSubscribeClick = () => {
props.onSubscribe?.()
}
@@ -28,55 +27,43 @@ export function PodcastCard(props: PodcastCardProps) {
>
{/* Title Row */}
<box flexDirection="row" gap={2} alignItems="center">
<text>
<span fg={props.selected ? "cyan" : "white"}>
<strong>{props.podcast.title}</strong>
</span>
<text fg={props.selected ? "cyan" : "white"}>
<strong>{props.podcast.title}</strong>
</text>
<Show when={props.podcast.isSubscribed}>
<text>
<span fg="green">[+]</span>
</text>
<text fg="green">[+]</text>
</Show>
</box>
{/* Author */}
<Show when={props.podcast.author && !props.compact}>
<text>
<span fg="gray">by {props.podcast.author}</span>
</text>
<text fg="gray">by {props.podcast.author}</text>
</Show>
{/* Description */}
<Show when={props.podcast.description && !props.compact}>
<text>
<span fg={props.selected ? "white" : "gray"}>
{props.podcast.description!.length > 80
? props.podcast.description!.slice(0, 80) + "..."
: props.podcast.description}
</span>
<text fg={props.selected ? "white" : "gray"}>
{props.podcast.description!.length > 80
? props.podcast.description!.slice(0, 80) + "..."
: props.podcast.description}
</text>
</Show>
{/* Categories and Subscribe Button */}
<box flexDirection="row" justifyContent="space-between" marginTop={props.compact ? 0 : 1}>
<box flexDirection="row" gap={1}>
<Show when={props.podcast.categories && props.podcast.categories.length > 0}>
{props.podcast.categories!.slice(0, 2).map((cat) => (
<text>
<span fg="yellow">[{cat}]</span>
</text>
<Show when={(props.podcast.categories ?? []).length > 0}>
{(props.podcast.categories ?? []).slice(0, 2).map((cat) => (
<text fg="yellow">[{cat}]</text>
))}
</Show>
</box>
<Show when={props.selected}>
<box onMouseDown={handleSubscribeClick}>
<text>
<span fg={props.podcast.isSubscribed ? "red" : "green"}>
{props.podcast.isSubscribed ? "[Unsubscribe]" : "[Subscribe]"}
</span>
<text fg={props.podcast.isSubscribed ? "red" : "green"}>
{props.podcast.isSubscribed ? "[Unsubscribe]" : "[Subscribe]"}
</text>
</box>
</Show>

View File

@@ -0,0 +1,79 @@
import { Show } from "solid-js"
import type { SearchResult } from "../types/source"
import { SourceBadge } from "./SourceBadge"
type ResultCardProps = {
result: SearchResult
selected: boolean
onSelect: () => void
onSubscribe?: () => void
}
export function ResultCard(props: ResultCardProps) {
const podcast = () => props.result.podcast
return (
<box
flexDirection="column"
padding={1}
border={props.selected}
borderColor={props.selected ? "cyan" : undefined}
backgroundColor={props.selected ? "#222" : undefined}
onMouseDown={props.onSelect}
>
<box flexDirection="row" justifyContent="space-between" alignItems="center">
<box flexDirection="row" gap={2} alignItems="center">
<text fg={props.selected ? "cyan" : "white"}>
<strong>{podcast().title}</strong>
</text>
<SourceBadge
sourceId={props.result.sourceId}
sourceName={props.result.sourceName}
sourceType={props.result.sourceType}
/>
</box>
<Show when={podcast().isSubscribed}>
<text fg="green">[Subscribed]</text>
</Show>
</box>
<Show when={podcast().author}>
<text fg="gray">by {podcast().author}</text>
</Show>
<Show when={podcast().description}>
{(description) => (
<text fg={props.selected ? "white" : "gray"}>
{description().length > 120
? description().slice(0, 120) + "..."
: description()}
</text>
)}
</Show>
<Show when={(podcast().categories ?? []).length > 0}>
<box flexDirection="row" gap={1}>
{(podcast().categories ?? []).slice(0, 3).map((category) => (
<text fg="yellow">[{category}]</text>
))}
</box>
</Show>
<Show when={!podcast().isSubscribed}>
<box
border
padding={0}
paddingLeft={1}
paddingRight={1}
width={18}
onMouseDown={(event) => {
event.stopPropagation?.()
props.onSubscribe?.()
}}
>
<text fg="cyan">[+] Add to Feeds</text>
</box>
</Show>
</box>
)
}

View File

@@ -0,0 +1,75 @@
import { Show } from "solid-js"
import { format } from "date-fns"
import type { SearchResult } from "../types/source"
import { SourceBadge } from "./SourceBadge"
type ResultDetailProps = {
result?: SearchResult
onSubscribe?: (result: SearchResult) => void
}
export function ResultDetail(props: ResultDetailProps) {
return (
<box flexDirection="column" border padding={1} gap={1} height="100%">
<Show
when={props.result}
fallback={
<text fg="gray">Select a result to see details.</text>
}
>
{(result) => (
<>
<text fg="white">
<strong>{result().podcast.title}</strong>
</text>
<SourceBadge
sourceId={result().sourceId}
sourceName={result().sourceName}
sourceType={result().sourceType}
/>
<Show when={result().podcast.author}>
<text fg="gray">by {result().podcast.author}</text>
</Show>
<Show when={result().podcast.description}>
<text fg="gray">{result().podcast.description}</text>
</Show>
<Show when={(result().podcast.categories ?? []).length > 0}>
<box flexDirection="row" gap={1}>
{(result().podcast.categories ?? []).map((category) => (
<text fg="yellow">[{category}]</text>
))}
</box>
</Show>
<text fg="gray">Feed: {result().podcast.feedUrl}</text>
<text fg="gray">
Updated: {format(result().podcast.lastUpdated, "MMM d, yyyy")}
</text>
<Show when={!result().podcast.isSubscribed}>
<box
border
padding={0}
paddingLeft={1}
paddingRight={1}
width={18}
onMouseDown={() => props.onSubscribe?.(result())}
>
<text fg="cyan">[+] Add to Feeds</text>
</box>
</Show>
<Show when={result().podcast.isSubscribed}>
<text fg="green">Already subscribed</text>
</Show>
</>
)}
</Show>
</box>
)
}

View File

@@ -20,22 +20,17 @@ export function SearchHistory(props: SearchHistoryProps) {
props.onSelect?.(query)
}
const handleRemoveClick = (e: MouseEvent, query: string) => {
e.stopPropagation?.()
const handleRemoveClick = (query: string) => {
props.onRemove?.(query)
}
return (
<box flexDirection="column" gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text>
<span fg="gray">Recent Searches</span>
</text>
<text fg="gray">Recent Searches</text>
<Show when={props.history.length > 0}>
<box onMouseDown={() => props.onClear?.()} padding={0}>
<text>
<span fg="red">[Clear All]</span>
</text>
<text fg="red">[Clear All]</text>
</box>
</Show>
</box>
@@ -44,13 +39,11 @@ export function SearchHistory(props: SearchHistoryProps) {
when={props.history.length > 0}
fallback={
<box padding={1}>
<text>
<span fg="gray">No recent searches</span>
</text>
<text fg="gray">No recent searches</text>
</box>
}
>
<scrollbox height={10} showScrollIndicator>
<scrollbox height={10}>
<box flexDirection="column">
<For each={props.history}>
{(query, index) => {
@@ -67,20 +60,11 @@ export function SearchHistory(props: SearchHistoryProps) {
onMouseDown={() => handleSearchClick(index(), query)}
>
<box flexDirection="row" gap={1}>
<text>
<span fg="gray">{">"}</span>
</text>
<text>
<span fg={isSelected() ? "cyan" : "white"}>{query}</span>
</text>
<text fg="gray">{">"}</text>
<text fg={isSelected() ? "cyan" : "white"}>{query}</text>
</box>
<box
onMouseDown={(e: MouseEvent) => handleRemoveClick(e, query)}
padding={0}
>
<text>
<span fg="red">[x]</span>
</text>
<box onMouseDown={() => handleRemoveClick(query)} padding={0}>
<text fg="red">[x]</text>
</box>
</box>
)

View File

@@ -47,6 +47,7 @@ export function SearchPage(props: SearchPageProps) {
const handleResultSelect = (result: SearchResult) => {
props.onSubscribe?.(result)
searchStore.markSubscribed(result.podcast.id)
}
// Keyboard navigation
@@ -162,23 +163,24 @@ export function SearchPage(props: SearchPageProps) {
<box flexDirection="column" height="100%" gap={1}>
{/* Search Header */}
<box flexDirection="column" gap={1}>
<text>
<strong>Search Podcasts</strong>
</text>
<text>
<strong>Search Podcasts</strong>
</text>
{/* Search Input */}
<box flexDirection="row" gap={1} alignItems="center">
<text>
<span fg="gray">Search:</span>
</text>
<text fg="gray">Search:</text>
<input
value={inputValue()}
onInput={setInputValue}
onInput={(value) => {
setInputValue(value)
if (props.focused && focusArea() === "input") {
props.onInputFocusChange?.(true)
}
}}
placeholder="Enter podcast name, topic, or author..."
focused={props.focused && focusArea() === "input"}
width={50}
onFocus={() => props.onInputFocusChange?.(true)}
onBlur={() => props.onInputFocusChange?.(false)}
/>
<box
border
@@ -187,22 +189,16 @@ export function SearchPage(props: SearchPageProps) {
paddingRight={1}
onMouseDown={handleSearch}
>
<text>
<span fg="cyan">[Enter] Search</span>
</text>
<text fg="cyan">[Enter] Search</text>
</box>
</box>
{/* Status */}
<Show when={searchStore.isSearching()}>
<text>
<span fg="yellow">Searching...</span>
</text>
<text fg="yellow">Searching...</text>
</Show>
<Show when={searchStore.error()}>
<text>
<span fg="red">{searchStore.error()}</span>
</text>
<text fg="red">{searchStore.error()}</text>
</Show>
</box>
@@ -210,23 +206,19 @@ export function SearchPage(props: SearchPageProps) {
<box flexDirection="row" height="100%" gap={2}>
{/* Results Panel */}
<box flexDirection="column" flexGrow={1} border>
<box padding={1} borderBottom>
<text>
<span fg={focusArea() === "results" ? "cyan" : "gray"}>
Results ({searchStore.results().length})
</span>
<box padding={1}>
<text fg={focusArea() === "results" ? "cyan" : "gray"}>
Results ({searchStore.results().length})
</text>
</box>
<Show
when={searchStore.results().length > 0}
fallback={
<box padding={2}>
<text>
<span fg="gray">
{searchStore.query()
? "No results found"
: "Enter a search term to find podcasts"}
</span>
<text fg="gray">
{searchStore.query()
? "No results found"
: "Enter a search term to find podcasts"}
</text>
</box>
}
@@ -237,24 +229,24 @@ export function SearchPage(props: SearchPageProps) {
focused={focusArea() === "results"}
onSelect={handleResultSelect}
onChange={setResultIndex}
isSearching={searchStore.isSearching()}
error={searchStore.error()}
/>
</Show>
</box>
{/* History Sidebar */}
<box width={30} border>
<box padding={1} flexDirection="column">
<box borderBottom paddingBottom={1}>
<text>
<span fg={focusArea() === "history" ? "cyan" : "gray"}>
<box width={30} border>
<box padding={1} flexDirection="column">
<box paddingBottom={1}>
<text fg={focusArea() === "history" ? "cyan" : "gray"}>
History
</span>
</text>
</box>
<SearchHistory
history={searchStore.history()}
selectedIndex={historyIndex()}
focused={focusArea() === "history"}
</text>
</box>
<SearchHistory
history={searchStore.history()}
selectedIndex={historyIndex()}
focused={focusArea() === "history"}
onSelect={handleHistorySelect}
onRemove={searchStore.removeFromHistory}
onClear={searchStore.clearHistory}
@@ -266,18 +258,10 @@ export function SearchPage(props: SearchPageProps) {
{/* Footer Hints */}
<box flexDirection="row" gap={2}>
<text>
<span fg="gray">[Tab] Switch focus</span>
</text>
<text>
<span fg="gray">[/] Focus search</span>
</text>
<text>
<span fg="gray">[Enter] Select</span>
</text>
<text>
<span fg="gray">[Esc] Back to search</span>
</text>
<text fg="gray">[Tab] Switch focus</text>
<text fg="gray">[/] Focus search</text>
<text fg="gray">[Enter] Select</text>
<text fg="gray">[Esc] Back to search</text>
</box>
</box>
)

View File

@@ -4,6 +4,8 @@
import { For, Show } from "solid-js"
import type { SearchResult } from "../types/source"
import { ResultCard } from "./ResultCard"
import { ResultDetail } from "./ResultDetail"
type SearchResultsProps = {
results: SearchResult[]
@@ -11,88 +13,63 @@ type SearchResultsProps = {
focused: boolean
onSelect?: (result: SearchResult) => void
onChange?: (index: number) => void
isSearching?: boolean
error?: string | null
}
export function SearchResults(props: SearchResultsProps) {
const handleMouseDown = (index: number, result: SearchResult) => {
const handleSelect = (index: number) => {
props.onChange?.(index)
props.onSelect?.(result)
}
return (
<Show
when={props.results.length > 0}
fallback={
<box padding={1}>
<text>
<span fg="gray">No results found. Try a different search term.</span>
</text>
</box>
}
>
<scrollbox height="100%" showScrollIndicator>
<box flexDirection="column">
<For each={props.results}>
{(result, index) => {
const isSelected = () => index() === props.selectedIndex
const podcast = result.podcast
return (
<box
flexDirection="column"
padding={1}
backgroundColor={isSelected() ? "#333" : undefined}
onMouseDown={() => handleMouseDown(index(), result)}
>
<box flexDirection="row" gap={2}>
<text>
<span fg={isSelected() ? "cyan" : "white"}>
<strong>{podcast.title}</strong>
</span>
</text>
<Show when={podcast.isSubscribed}>
<text>
<span fg="green">[Subscribed]</span>
</text>
</Show>
<text>
<span fg="gray">({result.sourceId})</span>
</text>
</box>
<Show when={podcast.author}>
<text>
<span fg="gray">by {podcast.author}</span>
</text>
</Show>
<Show when={podcast.description}>
<text>
<span fg={isSelected() ? "white" : "gray"}>
{podcast.description!.length > 100
? podcast.description!.slice(0, 100) + "..."
: podcast.description}
</span>
</text>
</Show>
<Show when={podcast.categories && podcast.categories.length > 0}>
<box flexDirection="row" gap={1}>
<For each={podcast.categories!.slice(0, 3)}>
{(category) => (
<text>
<span fg="yellow">[{category}]</span>
</text>
)}
</For>
</box>
</Show>
<Show when={!props.isSearching} fallback={
<box padding={1}>
<text fg="yellow">Searching...</text>
</box>
}>
<Show
when={!props.error}
fallback={
<box padding={1}>
<text fg="red">{props.error}</text>
</box>
}
>
<Show
when={props.results.length > 0}
fallback={
<box padding={1}>
<text fg="gray">No results found. Try a different search term.</text>
</box>
}
>
<box flexDirection="row" gap={1} height="100%">
<box flexDirection="column" flexGrow={1}>
<scrollbox height="100%">
<box flexDirection="column" gap={1}>
<For each={props.results}>
{(result, index) => (
<ResultCard
result={result}
selected={index() === props.selectedIndex}
onSelect={() => handleSelect(index())}
onSubscribe={() => props.onSelect?.(result)}
/>
)}
</For>
</box>
)
}}
</For>
</box>
</scrollbox>
</scrollbox>
</box>
<box width={36}>
<ResultDetail
result={props.results[props.selectedIndex]}
onSubscribe={(result) => props.onSelect?.(result)}
/>
</box>
</box>
</Show>
</Show>
</Show>
)
}

View File

@@ -0,0 +1,34 @@
import { SourceType } from "../types/source"
type SourceBadgeProps = {
sourceId: string
sourceName?: string
sourceType?: SourceType
}
const typeLabel = (sourceType?: SourceType) => {
if (sourceType === SourceType.API) return "API"
if (sourceType === SourceType.RSS) return "RSS"
if (sourceType === SourceType.CUSTOM) return "Custom"
return "Source"
}
const typeColor = (sourceType?: SourceType) => {
if (sourceType === SourceType.API) return "cyan"
if (sourceType === SourceType.RSS) return "green"
if (sourceType === SourceType.CUSTOM) return "yellow"
return "gray"
}
export function SourceBadge(props: SourceBadgeProps) {
const label = () => props.sourceName || props.sourceId
return (
<box flexDirection="row" gap={1} padding={0}>
<text fg={typeColor(props.sourceType)}>
[{typeLabel(props.sourceType)}]
</text>
<text fg="gray">{label()}</text>
</box>
)
}

View File

@@ -106,35 +106,21 @@ export function SourceManager(props: SourceManagerProps) {
}
return (
<box
flexDirection="column"
border
padding={1}
gap={1}
onKeyPress={props.focused ? handleKeyPress : undefined}
>
<box flexDirection="column" border padding={1} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text>
<strong>Podcast Sources</strong>
</text>
<box border padding={0} onMouseDown={props.onClose}>
<text>
<span fg="cyan">[Esc] Close</span>
</text>
<text fg="cyan">[Esc] Close</text>
</box>
</box>
<text>
<span fg="gray">
Manage where to search for podcasts
</span>
</text>
<text fg="gray">Manage where to search for podcasts</text>
{/* Source list */}
<box border padding={1} flexDirection="column">
<text>
<span fg={focusArea() === "list" ? "cyan" : "gray"}>Sources:</span>
</text>
<text fg={focusArea() === "list" ? "cyan" : "gray"}>Sources:</text>
<scrollbox height={6}>
<For each={sources()}>
{(source, index) => (
@@ -153,61 +139,43 @@ export function SourceManager(props: SourceManagerProps) {
feedStore.toggleSource(source.id)
}}
>
<text>
<span
fg={
focusArea() === "list" && index() === selectedIndex()
? "cyan"
: "gray"
}
>
{focusArea() === "list" && index() === selectedIndex()
? ">"
: " "}
</span>
<text fg={
focusArea() === "list" && index() === selectedIndex()
? "cyan"
: "gray"
}>
{focusArea() === "list" && index() === selectedIndex()
? ">"
: " "}
</text>
<text>
<span fg={source.enabled ? "green" : "red"}>
{source.enabled ? "[x]" : "[ ]"}
</span>
<text fg={source.enabled ? "green" : "red"}>
{source.enabled ? "[x]" : "[ ]"}
</text>
<text>
<span fg="yellow">{getSourceIcon(source)}</span>
</text>
<text>
<span
fg={
focusArea() === "list" && index() === selectedIndex()
? "white"
: undefined
}
>
{source.name}
</span>
<text fg="yellow">{getSourceIcon(source)}</text>
<text
fg={
focusArea() === "list" && index() === selectedIndex()
? "white"
: undefined
}
>
{source.name}
</text>
</box>
)}
</For>
</scrollbox>
<text>
<span fg="gray">
Space/Enter to toggle, d to delete, a to add
</span>
</text>
<text fg="gray">Space/Enter to toggle, d to delete, a to add</text>
</box>
{/* Add new source form */}
<box border padding={1} flexDirection="column" gap={1}>
<text>
<span fg={focusArea() === "add" || focusArea() === "url" ? "cyan" : "gray"}>
Add New Source:
</span>
<text fg={focusArea() === "add" || focusArea() === "url" ? "cyan" : "gray"}>
Add New Source:
</text>
<box flexDirection="row" gap={1}>
<text>
<span fg="gray">Name:</span>
</text>
<text fg="gray">Name:</text>
<input
value={newSourceName()}
onInput={setNewSourceName}
@@ -218,9 +186,7 @@ export function SourceManager(props: SourceManagerProps) {
</box>
<box flexDirection="row" gap={1}>
<text>
<span fg="gray">URL:</span>
</text>
<text fg="gray">URL:</text>
<input
value={newSourceUrl()}
onInput={(v) => {
@@ -239,22 +205,16 @@ export function SourceManager(props: SourceManagerProps) {
width={15}
onMouseDown={handleAddSource}
>
<text>
<span fg="green">[+] Add Source</span>
</text>
<text fg="green">[+] Add Source</text>
</box>
</box>
{/* Error message */}
{error() && (
<text>
<span fg="red">{error()}</span>
</text>
<text fg="red">{error()}</text>
)}
<text>
<span fg="gray">Tab to switch sections, Esc to close</span>
</text>
<text fg="gray">Tab to switch sections, Esc to close</text>
</box>
)
}

View File

@@ -59,13 +59,7 @@ export function SyncProfile(props: SyncProfileProps) {
}
return (
<box
flexDirection="column"
border
padding={2}
gap={1}
onKeyPress={props.focused ? handleKeyPress : undefined}
>
<box flexDirection="column" border padding={2} gap={1}>
<text>
<strong>User Profile</strong>
</text>
@@ -76,24 +70,14 @@ export function SyncProfile(props: SyncProfileProps) {
<box flexDirection="row" gap={2}>
{/* ASCII avatar */}
<box border padding={1} width={8} height={4} justifyContent="center" alignItems="center">
<text>
<span fg="cyan">{userInitials()}</span>
</text>
<text fg="cyan">{userInitials()}</text>
</box>
{/* User details */}
<box flexDirection="column" gap={0}>
<text>
<span fg="white">{user()?.name || "Guest User"}</span>
</text>
<text>
<span fg="gray">{user()?.email || "No email"}</span>
</text>
<text>
<span fg="gray">
Joined: {formatDate(user()?.createdAt)}
</span>
</text>
<text fg="white">{user()?.name || "Guest User"}</text>
<text fg="gray">{user()?.email || "No email"}</text>
<text fg="gray">Joined: {formatDate(user()?.createdAt)}</text>
</box>
</box>
@@ -101,37 +85,23 @@ export function SyncProfile(props: SyncProfileProps) {
{/* Sync status section */}
<box border padding={1} flexDirection="column" gap={0}>
<text>
<span fg="cyan">Sync Status</span>
</text>
<text fg="cyan">Sync Status</text>
<box flexDirection="row" gap={1}>
<text>
<span fg="gray">Status:</span>
</text>
<text>
<span fg={user()?.syncEnabled ? "green" : "yellow"}>
{user()?.syncEnabled ? "Enabled" : "Disabled"}
</span>
<text fg="gray">Status:</text>
<text fg={user()?.syncEnabled ? "green" : "yellow"}>
{user()?.syncEnabled ? "Enabled" : "Disabled"}
</text>
</box>
<box flexDirection="row" gap={1}>
<text>
<span fg="gray">Last Sync:</span>
</text>
<text>
<span fg="white">{formatDate(lastSyncTime())}</span>
</text>
<text fg="gray">Last Sync:</text>
<text fg="white">{formatDate(lastSyncTime())}</text>
</box>
<box flexDirection="row" gap={1}>
<text>
<span fg="gray">Method:</span>
</text>
<text>
<span fg="white">File-based (JSON/XML)</span>
</text>
<text fg="gray">Method:</text>
<text fg="white">File-based (JSON/XML)</text>
</box>
</box>
@@ -144,10 +114,8 @@ export function SyncProfile(props: SyncProfileProps) {
padding={1}
backgroundColor={focusField() === "sync" ? "#333" : undefined}
>
<text>
<span fg={focusField() === "sync" ? "cyan" : undefined}>
[S] Manage Sync
</span>
<text fg={focusField() === "sync" ? "cyan" : undefined}>
[S] Manage Sync
</text>
</box>
@@ -156,10 +124,8 @@ export function SyncProfile(props: SyncProfileProps) {
padding={1}
backgroundColor={focusField() === "export" ? "#333" : undefined}
>
<text>
<span fg={focusField() === "export" ? "cyan" : undefined}>
[E] Export Data
</span>
<text fg={focusField() === "export" ? "cyan" : undefined}>
[E] Export Data
</text>
</box>
@@ -168,19 +134,15 @@ export function SyncProfile(props: SyncProfileProps) {
padding={1}
backgroundColor={focusField() === "logout" ? "#333" : undefined}
>
<text>
<span fg={focusField() === "logout" ? "red" : "gray"}>
[L] Logout
</span>
<text fg={focusField() === "logout" ? "red" : "gray"}>
[L] Logout
</text>
</box>
</box>
<box height={1} />
<text>
<span fg="gray">Tab to navigate, Enter to select</span>
</text>
<text fg="gray">Tab to navigate, Enter to select</text>
</box>
)
}

View File

@@ -20,22 +20,18 @@ export function TrendingShows(props: TrendingShowsProps) {
<box flexDirection="column" height="100%">
<Show when={props.isLoading}>
<box padding={2}>
<text>
<span fg="yellow">Loading trending shows...</span>
</text>
<text fg="yellow">Loading trending shows...</text>
</box>
</Show>
<Show when={!props.isLoading && props.podcasts.length === 0}>
<box padding={2}>
<text>
<span fg="gray">No podcasts found in this category.</span>
</text>
<text fg="gray">No podcasts found in this category.</text>
</box>
</Show>
<Show when={!props.isLoading && props.podcasts.length > 0}>
<scrollbox height="100%" showScrollIndicator>
<scrollbox height="100%">
<box flexDirection="column">
<For each={props.podcasts}>
{(podcast, index) => (