broke
This commit is contained in:
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
79
src/components/ResultCard.tsx
Normal file
79
src/components/ResultCard.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
75
src/components/ResultDetail.tsx
Normal file
75
src/components/ResultDetail.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
34
src/components/SourceBadge.tsx
Normal file
34
src/components/SourceBadge.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
Reference in New Issue
Block a user