cooking
This commit is contained in:
267
src/App.tsx
267
src/App.tsx
@@ -1,7 +1,6 @@
|
||||
import { createSignal, createMemo, ErrorBoundary } from "solid-js";
|
||||
import { createSignal, createMemo, ErrorBoundary, Accessor } from "solid-js";
|
||||
import { useSelectionHandler } from "@opentui/solid";
|
||||
import { Layout } from "./Layout";
|
||||
import { TabId, TabNavigation } from "./components/TabNavigation";
|
||||
import { TabNavigation } from "./components/TabNavigation";
|
||||
import { FeedPage } from "@/tabs/Feed/FeedPage";
|
||||
import { MyShowsPage } from "@/tabs/MyShows/MyShowsPage";
|
||||
import { LoginScreen } from "@/tabs/Settings/LoginScreen";
|
||||
@@ -10,7 +9,6 @@ import { OAuthPlaceholder } from "@/tabs/Settings/OAuthPlaceholder";
|
||||
import { SyncProfile } from "@/tabs/Settings/SyncProfile";
|
||||
import { SearchPage } from "@/tabs/Search/SearchPage";
|
||||
import { DiscoverPage } from "@/tabs/Discover/DiscoverPage";
|
||||
import { Player } from "@/tabs/Player/Player";
|
||||
import { SettingsScreen } from "@/tabs/Settings/SettingsScreen";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useFeedStore } from "@/stores/feed";
|
||||
@@ -24,9 +22,16 @@ import { useRenderer } from "@opentui/solid";
|
||||
import type { AuthScreen } from "@/types/auth";
|
||||
import type { Episode } from "@/types/episode";
|
||||
import { DIRECTION } from "./types/navigation";
|
||||
import { LayerGraph, TABS } from "./utils/navigation";
|
||||
import { useTheme } from "./context/ThemeContext";
|
||||
|
||||
export interface PageProps {
|
||||
depth: Accessor<number>;
|
||||
}
|
||||
|
||||
export function App() {
|
||||
const [activeTab, setActiveTab] = createSignal<TabId>("feed");
|
||||
const [activeTab, setActiveTab] = createSignal<TABS>(TABS.FEED);
|
||||
const [activeDepth, setActiveDepth] = createSignal(0); // not fixed matrix size
|
||||
const [authScreen, setAuthScreen] = createSignal<AuthScreen>("login");
|
||||
const [showAuthPanel, setShowAuthPanel] = createSignal(false);
|
||||
const [inputFocused, setInputFocused] = createSignal(false);
|
||||
@@ -37,31 +42,18 @@ export function App() {
|
||||
const toast = useToast();
|
||||
const renderer = useRenderer();
|
||||
|
||||
// Global multimedia key handling — active when Player tab is NOT
|
||||
// focused (Player.tsx handles its own keys when focused).
|
||||
useMultimediaKeys({
|
||||
playerFocused: () => activeTab() === "player" && layerDepth() > 0,
|
||||
playerFocused: () => activeTab() === TABS.PLAYER && layerDepth() > 0,
|
||||
inputFocused: () => inputFocused(),
|
||||
hasEpisode: () => !!audio.currentEpisode(),
|
||||
});
|
||||
|
||||
const handlePlayEpisode = (episode: Episode) => {
|
||||
audio.play(episode);
|
||||
setActiveTab("player");
|
||||
setActiveTab(TABS.PLAYER);
|
||||
setLayerDepth(1);
|
||||
};
|
||||
|
||||
// My Shows page returns panel renderers
|
||||
const myShows = MyShowsPage({
|
||||
get focused() {
|
||||
return activeTab() === "shows" && layerDepth() > 0;
|
||||
},
|
||||
onPlayEpisode: (episode, feed) => {
|
||||
handlePlayEpisode(episode);
|
||||
},
|
||||
onExit: () => setLayerDepth(0),
|
||||
});
|
||||
|
||||
useAppKeyboard({
|
||||
layerDepth,
|
||||
onAction: (action, direction) => {
|
||||
@@ -91,7 +83,6 @@ export function App() {
|
||||
},
|
||||
});
|
||||
|
||||
// Copy selected text to clipboard when selection ends (mouse release)
|
||||
useSelectionHandler((selection: any) => {
|
||||
if (!selection) return;
|
||||
const text = selection.getSelectedText?.();
|
||||
@@ -107,225 +98,7 @@ export function App() {
|
||||
});
|
||||
});
|
||||
|
||||
const getPanels = createMemo(() => {
|
||||
const tab = activeTab();
|
||||
|
||||
switch (tab) {
|
||||
case "feed":
|
||||
return {
|
||||
panels: [
|
||||
{
|
||||
title: "Feed - Latest Episodes",
|
||||
content: (
|
||||
<FeedPage
|
||||
focused={layerDepth() > 0}
|
||||
onPlayEpisode={(episode, feed) => {
|
||||
handlePlayEpisode(episode);
|
||||
}}
|
||||
onExit={() => setLayerDepth(0)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
activePanelIndex: 0,
|
||||
hint: "j/k navigate | Enter play | r refresh | Esc back",
|
||||
};
|
||||
|
||||
case "shows":
|
||||
return {
|
||||
panels: [
|
||||
{
|
||||
title: "My Shows",
|
||||
width: 35,
|
||||
content: myShows.showsPanel(),
|
||||
focused: myShows.focusPane() === "shows",
|
||||
},
|
||||
{
|
||||
title: myShows.selectedShow()
|
||||
? `${myShows.selectedShow()!.podcast.title} - Episodes`
|
||||
: "Episodes",
|
||||
content: myShows.episodesPanel(),
|
||||
focused: myShows.focusPane() === "episodes",
|
||||
},
|
||||
],
|
||||
activePanelIndex: myShows.focusPane() === "shows" ? 0 : 1,
|
||||
hint: "h/l switch panes | j/k navigate | Enter play | r refresh | d unsubscribe | Esc back",
|
||||
};
|
||||
|
||||
case "settings":
|
||||
if (showAuthPanel()) {
|
||||
if (auth.isAuthenticated) {
|
||||
return {
|
||||
panels: [
|
||||
{
|
||||
title: "Account",
|
||||
content: (
|
||||
<SyncProfile
|
||||
focused={layerDepth() > 0}
|
||||
onLogout={() => {
|
||||
auth.logout();
|
||||
setShowAuthPanel(false);
|
||||
}}
|
||||
onManageSync={() => setShowAuthPanel(false)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
activePanelIndex: 0,
|
||||
hint: "Esc back",
|
||||
};
|
||||
}
|
||||
|
||||
const authContent = () => {
|
||||
switch (authScreen()) {
|
||||
case "code":
|
||||
return (
|
||||
<CodeValidation
|
||||
focused={layerDepth() > 0}
|
||||
onBack={() => setAuthScreen("login")}
|
||||
/>
|
||||
);
|
||||
case "oauth":
|
||||
return (
|
||||
<OAuthPlaceholder
|
||||
focused={layerDepth() > 0}
|
||||
onBack={() => setAuthScreen("login")}
|
||||
onNavigateToCode={() => setAuthScreen("code")}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<LoginScreen
|
||||
focused={layerDepth() > 0}
|
||||
onNavigateToCode={() => setAuthScreen("code")}
|
||||
onNavigateToOAuth={() => setAuthScreen("oauth")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
panels: [
|
||||
{
|
||||
title: "Sign In",
|
||||
content: authContent(),
|
||||
},
|
||||
],
|
||||
activePanelIndex: 0,
|
||||
hint: "Esc back",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
panels: [
|
||||
{
|
||||
title: "Settings",
|
||||
content: (
|
||||
<SettingsScreen
|
||||
onOpenAccount={() => setShowAuthPanel(true)}
|
||||
accountLabel={
|
||||
auth.isAuthenticated
|
||||
? `Signed in as ${auth.user?.email}`
|
||||
: "Not signed in"
|
||||
}
|
||||
accountStatus={
|
||||
auth.isAuthenticated ? "signed-in" : "signed-out"
|
||||
}
|
||||
onExit={() => setLayerDepth(0)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
activePanelIndex: 0,
|
||||
hint: "j/k navigate | Enter select | Esc back",
|
||||
};
|
||||
|
||||
case "discover":
|
||||
return {
|
||||
panels: [
|
||||
{
|
||||
title: "Discover",
|
||||
content: (
|
||||
<DiscoverPage
|
||||
focused={layerDepth() > 0}
|
||||
onExit={() => setLayerDepth(0)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
activePanelIndex: 0,
|
||||
hint: "Tab switch focus | j/k navigate | Enter subscribe | r refresh | Esc back",
|
||||
};
|
||||
|
||||
case "search":
|
||||
return {
|
||||
panels: [
|
||||
{
|
||||
title: "Search",
|
||||
content: (
|
||||
<SearchPage
|
||||
focused={layerDepth() > 0}
|
||||
onInputFocusChange={setInputFocused}
|
||||
onExit={() => setLayerDepth(0)}
|
||||
onSubscribe={(result) => {
|
||||
const feeds = feedStore.feeds();
|
||||
const alreadySubscribed = feeds.some(
|
||||
(feed) =>
|
||||
feed.podcast.id === result.podcast.id ||
|
||||
feed.podcast.feedUrl === result.podcast.feedUrl,
|
||||
);
|
||||
|
||||
if (!alreadySubscribed) {
|
||||
feedStore.addFeed(
|
||||
{ ...result.podcast, isSubscribed: true },
|
||||
result.sourceId,
|
||||
FeedVisibility.PUBLIC,
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
activePanelIndex: 0,
|
||||
hint: "Tab switch focus | / search | Enter select | Esc back",
|
||||
};
|
||||
|
||||
case "player":
|
||||
return {
|
||||
panels: [
|
||||
{
|
||||
title: "Player",
|
||||
content: (
|
||||
<Player
|
||||
focused={layerDepth() > 0}
|
||||
onExit={() => setLayerDepth(0)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
activePanelIndex: 0,
|
||||
hint: "Space play/pause | Esc back",
|
||||
};
|
||||
|
||||
default:
|
||||
return {
|
||||
panels: [
|
||||
{
|
||||
title: tab,
|
||||
content: (
|
||||
<box padding={2}>
|
||||
<text>Coming soon</text>
|
||||
</box>
|
||||
),
|
||||
},
|
||||
],
|
||||
activePanelIndex: 0,
|
||||
hint: "",
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<ErrorBoundary
|
||||
fallback={(err) => (
|
||||
@@ -338,13 +111,15 @@ export function App() {
|
||||
</box>
|
||||
)}
|
||||
>
|
||||
<Layout
|
||||
header={
|
||||
<box
|
||||
flexDirection="row"
|
||||
width="100%"
|
||||
height="100%"
|
||||
backgroundColor={theme.surface}
|
||||
>
|
||||
<TabNavigation activeTab={activeTab()} onTabSelect={setActiveTab} />
|
||||
}
|
||||
panels={getPanels().panels}
|
||||
activePanelIndex={getPanels().activePanelIndex}
|
||||
/>
|
||||
{LayerGraph[activeTab()]({ depth: activeDepth })}
|
||||
</box>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
111
src/Layout.tsx
111
src/Layout.tsx
@@ -1,111 +0,0 @@
|
||||
import type { JSX } from "solid-js";
|
||||
import type { RGBA } from "@opentui/core";
|
||||
import { Show, For } from "solid-js";
|
||||
import { useTheme } from "@/context/ThemeContext";
|
||||
|
||||
type PanelConfig = {
|
||||
/** Panel content */
|
||||
content: JSX.Element;
|
||||
/** Panel title shown in header */
|
||||
title?: string;
|
||||
/** Fixed width (leave undefined for flex) */
|
||||
width?: number;
|
||||
/** Whether this panel is currently focused */
|
||||
focused?: boolean;
|
||||
};
|
||||
|
||||
type LayoutProps = {
|
||||
/** Top tab bar */
|
||||
header?: JSX.Element;
|
||||
/** Bottom status bar */
|
||||
footer?: JSX.Element;
|
||||
/** Panels to display left-to-right like a file explorer */
|
||||
panels: PanelConfig[];
|
||||
/** Index of the currently active/focused panel */
|
||||
activePanelIndex?: number;
|
||||
};
|
||||
|
||||
export function Layout(props: LayoutProps) {
|
||||
const panelBg = (index: number): RGBA => {
|
||||
const backgrounds = theme.layerBackgrounds;
|
||||
const layers = [
|
||||
backgrounds?.layer0 ?? theme.background,
|
||||
backgrounds?.layer1 ?? theme.backgroundPanel,
|
||||
backgrounds?.layer2 ?? theme.backgroundElement,
|
||||
backgrounds?.layer3 ?? theme.backgroundMenu,
|
||||
];
|
||||
return layers[Math.min(index, layers.length - 1)];
|
||||
};
|
||||
|
||||
const borderColor = (index: number): RGBA | string => {
|
||||
const isActive = index === (props.activePanelIndex ?? 0);
|
||||
return isActive
|
||||
? (theme.accent ?? theme.primary)
|
||||
: (theme.border ?? theme.textMuted);
|
||||
};
|
||||
const { theme } = useTheme();
|
||||
|
||||
return (
|
||||
<box
|
||||
flexDirection="row"
|
||||
width="100%"
|
||||
height="100%"
|
||||
backgroundColor={theme.surface}
|
||||
>
|
||||
{props.header}
|
||||
|
||||
<box flexDirection="row" style={{ flexGrow: 1 }}>
|
||||
<For each={props.panels}>
|
||||
{(panel, index) => (
|
||||
<box
|
||||
flexDirection="column"
|
||||
border
|
||||
borderColor={theme.border}
|
||||
backgroundColor={panelBg(index())}
|
||||
style={{
|
||||
flexGrow: panel.width ? 0 : 1,
|
||||
width: panel.width,
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
{/* Panel header */}
|
||||
<Show when={panel.title}>
|
||||
<box
|
||||
style={{
|
||||
height: 1,
|
||||
paddingLeft: 1,
|
||||
paddingRight: 1,
|
||||
backgroundColor:
|
||||
index() === (props.activePanelIndex ?? 0)
|
||||
? (theme.accent ?? theme.primary)
|
||||
: (theme.surface ?? theme.backgroundPanel),
|
||||
}}
|
||||
>
|
||||
<text
|
||||
fg={
|
||||
index() === (props.activePanelIndex ?? 0)
|
||||
? "black"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<strong>{panel.title}</strong>
|
||||
</text>
|
||||
</box>
|
||||
</Show>
|
||||
|
||||
{/* Panel body */}
|
||||
<box
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
padding: 1,
|
||||
}}
|
||||
>
|
||||
{panel.content}
|
||||
</box>
|
||||
</box>
|
||||
)}
|
||||
</For>
|
||||
</box>
|
||||
</box>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +1,19 @@
|
||||
import { useTheme } from "@/context/ThemeContext";
|
||||
import { TABS } from "@/utils/navigation";
|
||||
import { For } from "solid-js";
|
||||
|
||||
interface TabNavigationProps {
|
||||
activeTab: TabId;
|
||||
onTabSelect: (tab: TabId) => void;
|
||||
activeTab: TABS;
|
||||
onTabSelect: (tab: TABS) => void;
|
||||
}
|
||||
|
||||
export const tabs: TabDefinition[] = [
|
||||
{ id: "feed", label: "Feed" },
|
||||
{ id: "shows", label: "My Shows" },
|
||||
{ id: "discover", label: "Discover" },
|
||||
{ id: "search", label: "Search" },
|
||||
{ id: "player", label: "Player" },
|
||||
{ id: "settings", label: "Settings" },
|
||||
{ id: TABS.FEED, label: "Feed" },
|
||||
{ id: TABS.MYSHOWS, label: "My Shows" },
|
||||
{ id: TABS.DISCOVER, label: "Discover" },
|
||||
{ id: TABS.SEARCH, label: "Search" },
|
||||
{ id: TABS.PLAYER, label: "Player" },
|
||||
{ id: TABS.SETTINGS, label: "Settings" },
|
||||
];
|
||||
|
||||
export function TabNavigation(props: TabNavigationProps) {
|
||||
@@ -52,15 +53,7 @@ export function TabNavigation(props: TabNavigationProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export type TabId =
|
||||
| "feed"
|
||||
| "shows"
|
||||
| "discover"
|
||||
| "search"
|
||||
| "player"
|
||||
| "settings";
|
||||
|
||||
export type TabDefinition = {
|
||||
id: TabId;
|
||||
id: TABS;
|
||||
label: string;
|
||||
};
|
||||
|
||||
@@ -7,121 +7,19 @@ import { useKeyboard } from "@opentui/solid";
|
||||
import { useDiscoverStore, DISCOVER_CATEGORIES } from "@/stores/discover";
|
||||
import { useTheme } from "@/context/ThemeContext";
|
||||
import { PodcastCard } from "./PodcastCard";
|
||||
import { PageProps } from "@/App";
|
||||
|
||||
type DiscoverPageProps = {
|
||||
focused: boolean;
|
||||
onExit?: () => void;
|
||||
};
|
||||
enum DiscoverPagePaneType {
|
||||
CATEGORIES = 1,
|
||||
SHOWS = 2,
|
||||
}
|
||||
export const DiscoverPaneCount = 2;
|
||||
|
||||
type FocusArea = "categories" | "shows";
|
||||
|
||||
export function DiscoverPage(props: DiscoverPageProps) {
|
||||
export function DiscoverPage(props: PageProps) {
|
||||
const discoverStore = useDiscoverStore();
|
||||
const [focusArea, setFocusArea] = createSignal<FocusArea>("shows");
|
||||
const [showIndex, setShowIndex] = createSignal(0);
|
||||
const [categoryIndex, setCategoryIndex] = createSignal(0);
|
||||
|
||||
// Keyboard navigation
|
||||
useKeyboard((key) => {
|
||||
if (!props.focused) return;
|
||||
|
||||
const area = focusArea();
|
||||
|
||||
// Tab switches focus between categories and shows
|
||||
if (key.name === "tab") {
|
||||
if (key.shift) {
|
||||
setFocusArea((a) => (a === "categories" ? "shows" : "categories"));
|
||||
} else {
|
||||
setFocusArea((a) => (a === "categories" ? "shows" : "categories"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.name === "return" && area === "categories") {
|
||||
setFocusArea("shows");
|
||||
return;
|
||||
}
|
||||
|
||||
// Category navigation
|
||||
if (area === "categories") {
|
||||
if (key.name === "left" || key.name === "h") {
|
||||
const nextIndex = Math.max(0, categoryIndex() - 1);
|
||||
setCategoryIndex(nextIndex);
|
||||
const cat = DISCOVER_CATEGORIES[nextIndex];
|
||||
if (cat) discoverStore.setSelectedCategory(cat.id);
|
||||
setShowIndex(0);
|
||||
return;
|
||||
}
|
||||
if (key.name === "right" || key.name === "l") {
|
||||
const nextIndex = Math.min(
|
||||
DISCOVER_CATEGORIES.length - 1,
|
||||
categoryIndex() + 1,
|
||||
);
|
||||
setCategoryIndex(nextIndex);
|
||||
const cat = DISCOVER_CATEGORIES[nextIndex];
|
||||
if (cat) discoverStore.setSelectedCategory(cat.id);
|
||||
setShowIndex(0);
|
||||
return;
|
||||
}
|
||||
if (key.name === "return" || key.name === "enter") {
|
||||
// Select category and move to shows
|
||||
setFocusArea("shows");
|
||||
return;
|
||||
}
|
||||
if (key.name === "down" || key.name === "j") {
|
||||
setFocusArea("shows");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Shows navigation
|
||||
if (area === "shows") {
|
||||
const shows = discoverStore.filteredPodcasts();
|
||||
if (key.name === "down" || key.name === "j") {
|
||||
if (shows.length === 0) return;
|
||||
setShowIndex((i) => Math.min(i + 1, shows.length - 1));
|
||||
return;
|
||||
}
|
||||
if (key.name === "up" || key.name === "k") {
|
||||
if (shows.length === 0) {
|
||||
setFocusArea("categories");
|
||||
return;
|
||||
}
|
||||
const newIndex = showIndex() - 1;
|
||||
if (newIndex < 0) {
|
||||
setFocusArea("categories");
|
||||
} else {
|
||||
setShowIndex(newIndex);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (key.name === "return" || key.name === "enter") {
|
||||
// Subscribe/unsubscribe
|
||||
const podcast = shows[showIndex()];
|
||||
if (podcast) {
|
||||
discoverStore.toggleSubscription(podcast.id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (key.name === "escape") {
|
||||
if (area === "shows") {
|
||||
setFocusArea("categories");
|
||||
key.stopPropagation();
|
||||
} else {
|
||||
props.onExit?.();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Refresh with 'r'
|
||||
if (key.name === "r") {
|
||||
discoverStore.refresh();
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
const handleCategorySelect = (categoryId: string) => {
|
||||
discoverStore.setSelectedCategory(categoryId);
|
||||
const index = DISCOVER_CATEGORIES.findIndex((c) => c.id === categoryId);
|
||||
@@ -131,7 +29,6 @@ export function DiscoverPage(props: DiscoverPageProps) {
|
||||
|
||||
const handleShowSelect = (index: number) => {
|
||||
setShowIndex(index);
|
||||
setFocusArea("shows");
|
||||
};
|
||||
|
||||
const handleSubscribe = (podcast: { id: string }) => {
|
||||
@@ -149,7 +46,13 @@ export function DiscoverPage(props: DiscoverPageProps) {
|
||||
gap={1}
|
||||
width={20}
|
||||
>
|
||||
<text fg={focusArea() === "categories" ? theme.accent : theme.text}>
|
||||
<text
|
||||
fg={
|
||||
props.depth() == DiscoverPagePaneType.CATEGORIES
|
||||
? theme.accent
|
||||
: theme.text
|
||||
}
|
||||
>
|
||||
Categories:
|
||||
</text>
|
||||
<box flexDirection="column" gap={1}>
|
||||
@@ -180,7 +83,9 @@ export function DiscoverPage(props: DiscoverPageProps) {
|
||||
borderColor={theme.border}
|
||||
>
|
||||
<box padding={1}>
|
||||
<text fg={focusArea() === "shows" ? "cyan" : "gray"}>
|
||||
<text
|
||||
fg={props.depth() == DiscoverPagePaneType.SHOWS ? "cyan" : "gray"}
|
||||
>
|
||||
Trending in{" "}
|
||||
{DISCOVER_CATEGORIES.find(
|
||||
(c) => c.id === discoverStore.selectedCategory(),
|
||||
@@ -210,7 +115,8 @@ export function DiscoverPage(props: DiscoverPageProps) {
|
||||
<PodcastCard
|
||||
podcast={podcast}
|
||||
selected={
|
||||
index() === showIndex() && focusArea() === "shows"
|
||||
index() === showIndex() &&
|
||||
props.depth() == DiscoverPagePaneType.SHOWS
|
||||
}
|
||||
onSelect={() => handleShowSelect(index())}
|
||||
onSubscribe={() => handleSubscribe(podcast)}
|
||||
@@ -4,20 +4,16 @@
|
||||
*/
|
||||
|
||||
import { createSignal, For, Show } from "solid-js";
|
||||
import { useKeyboard } from "@opentui/solid";
|
||||
import { useFeedStore } from "@/stores/feed";
|
||||
import { format } from "date-fns";
|
||||
import type { Episode } from "@/types/episode";
|
||||
import type { Feed } from "@/types/feed";
|
||||
import { useTheme } from "@/context/ThemeContext";
|
||||
import { PageProps } from "@/App";
|
||||
|
||||
type FeedPageProps = {
|
||||
focused: boolean;
|
||||
onPlayEpisode?: (episode: Episode, feed: Feed) => void;
|
||||
onExit?: () => void;
|
||||
};
|
||||
export const FeedPaneCount = 1;
|
||||
|
||||
export function FeedPage(props: FeedPageProps) {
|
||||
export function FeedPage(props: PageProps) {
|
||||
const feedStore = useFeedStore();
|
||||
const [selectedIndex, setSelectedIndex] = createSignal(0);
|
||||
const [isRefreshing, setIsRefreshing] = createSignal(false);
|
||||
@@ -41,33 +37,6 @@ export function FeedPage(props: FeedPageProps) {
|
||||
setIsRefreshing(false);
|
||||
};
|
||||
|
||||
useKeyboard((key) => {
|
||||
if (!props.focused) return;
|
||||
|
||||
const episodes = allEpisodes();
|
||||
|
||||
if (key.name === "down" || key.name === "j") {
|
||||
setSelectedIndex((i) => Math.min(episodes.length - 1, i + 1));
|
||||
} else if (key.name === "up" || key.name === "k") {
|
||||
setSelectedIndex((i) => Math.max(0, i - 1));
|
||||
} else if (key.name === "return") {
|
||||
const item = episodes[selectedIndex()];
|
||||
if (item) props.onPlayEpisode?.(item.episode, item.feed);
|
||||
} else if (key.name === "home" || key.name === "g") {
|
||||
setSelectedIndex(0);
|
||||
} else if (key.name === "end") {
|
||||
setSelectedIndex(episodes.length - 1);
|
||||
} else if (key.name === "pageup") {
|
||||
setSelectedIndex((i) => Math.max(0, i - 10));
|
||||
} else if (key.name === "pagedown") {
|
||||
setSelectedIndex((i) => Math.min(episodes.length - 1, i + 10));
|
||||
} else if (key.name === "r") {
|
||||
handleRefresh();
|
||||
} else if (key.name === "escape") {
|
||||
props.onExit?.();
|
||||
}
|
||||
});
|
||||
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<box
|
||||
@@ -80,7 +49,6 @@ export function FeedPage(props: FeedPageProps) {
|
||||
<text fg="yellow">Refreshing feeds...</text>
|
||||
</Show>
|
||||
|
||||
{/* Episode list */}
|
||||
<Show
|
||||
when={allEpisodes().length > 0}
|
||||
fallback={
|
||||
@@ -91,7 +59,8 @@ export function FeedPage(props: FeedPageProps) {
|
||||
</box>
|
||||
}
|
||||
>
|
||||
<scrollbox height="100%" focused={props.focused}>
|
||||
{/**TODO: figure out wtf to do here **/}
|
||||
<scrollbox height="100%" focused={true}>
|
||||
<For each={allEpisodes()}>
|
||||
{(item, index) => (
|
||||
<box
|
||||
@@ -12,19 +12,19 @@ import { DownloadStatus } from "@/types/episode";
|
||||
import { format } from "date-fns";
|
||||
import type { Episode } from "@/types/episode";
|
||||
import type { Feed } from "@/types/feed";
|
||||
import { PageProps } from "@/App";
|
||||
|
||||
type MyShowsPageProps = {
|
||||
focused: boolean;
|
||||
onPlayEpisode?: (episode: Episode, feed: Feed) => void;
|
||||
onExit?: () => void;
|
||||
};
|
||||
enum MyShowsPaneType {
|
||||
SHOWS,
|
||||
EPISODES,
|
||||
}
|
||||
|
||||
type FocusPane = "shows" | "episodes";
|
||||
export const MyShowsPaneCount = 2
|
||||
|
||||
export function MyShowsPage(props: MyShowsPageProps) {
|
||||
export function MyShowsPage(props: PageProps) {
|
||||
const feedStore = useFeedStore();
|
||||
const downloadStore = useDownloadStore();
|
||||
const [focusPane, setFocusPane] = createSignal<FocusPane>("shows");
|
||||
const [focusPane, setFocusPane] = createSignal<>("shows");
|
||||
const [showIndex, setShowIndex] = createSignal(0);
|
||||
const [episodeIndex, setEpisodeIndex] = createSignal(0);
|
||||
const [isRefreshing, setIsRefreshing] = createSignal(false);
|
||||
@@ -128,95 +128,6 @@ export function MyShowsPage(props: MyShowsPageProps) {
|
||||
setEpisodeIndex(0);
|
||||
};
|
||||
|
||||
useKeyboard((key) => {
|
||||
if (!props.focused) return;
|
||||
|
||||
const pane = focusPane();
|
||||
|
||||
// Navigate between panes
|
||||
if (key.name === "right" || key.name === "l") {
|
||||
if (pane === "shows" && selectedShow()) {
|
||||
setFocusPane("episodes");
|
||||
setEpisodeIndex(0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (key.name === "left" || key.name === "h") {
|
||||
if (pane === "episodes") {
|
||||
setFocusPane("shows");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (key.name === "tab") {
|
||||
if (pane === "shows" && selectedShow()) {
|
||||
setFocusPane("episodes");
|
||||
setEpisodeIndex(0);
|
||||
} else {
|
||||
setFocusPane("shows");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (pane === "shows") {
|
||||
const s = shows();
|
||||
if (key.name === "down" || key.name === "j") {
|
||||
setShowIndex((i) => Math.min(s.length - 1, i + 1));
|
||||
setEpisodeIndex(0);
|
||||
} else if (key.name === "up" || key.name === "k") {
|
||||
setShowIndex((i) => Math.max(0, i - 1));
|
||||
setEpisodeIndex(0);
|
||||
} else if (key.name === "return" || key.name === "enter") {
|
||||
if (selectedShow()) {
|
||||
setFocusPane("episodes");
|
||||
setEpisodeIndex(0);
|
||||
}
|
||||
} else if (key.name === "d") {
|
||||
handleUnsubscribe();
|
||||
} else if (key.name === "r") {
|
||||
handleRefresh();
|
||||
} else if (key.name === "escape") {
|
||||
props.onExit?.();
|
||||
}
|
||||
} else if (pane === "episodes") {
|
||||
const eps = episodes();
|
||||
if (key.name === "down" || key.name === "j") {
|
||||
setEpisodeIndex((i) => Math.min(eps.length - 1, i + 1));
|
||||
} else if (key.name === "up" || key.name === "k") {
|
||||
setEpisodeIndex((i) => Math.max(0, i - 1));
|
||||
} else if (key.name === "return" || key.name === "enter") {
|
||||
const ep = eps[episodeIndex()];
|
||||
const show = selectedShow();
|
||||
if (ep && show) props.onPlayEpisode?.(ep, show);
|
||||
} else if (key.name === "d") {
|
||||
const ep = eps[episodeIndex()];
|
||||
const show = selectedShow();
|
||||
if (ep && show) {
|
||||
const status = downloadStore.getDownloadStatus(ep.id);
|
||||
if (
|
||||
status === DownloadStatus.NONE ||
|
||||
status === DownloadStatus.FAILED
|
||||
) {
|
||||
downloadStore.startDownload(ep, show.id);
|
||||
} else if (
|
||||
status === DownloadStatus.DOWNLOADING ||
|
||||
status === DownloadStatus.QUEUED
|
||||
) {
|
||||
downloadStore.cancelDownload(ep.id);
|
||||
}
|
||||
}
|
||||
} else if (key.name === "pageup") {
|
||||
setEpisodeIndex((i) => Math.max(0, i - 10));
|
||||
} else if (key.name === "pagedown") {
|
||||
setEpisodeIndex((i) => Math.min(eps.length - 1, i + 10));
|
||||
} else if (key.name === "r") {
|
||||
handleRefresh();
|
||||
} else if (key.name === "escape") {
|
||||
setFocusPane("shows");
|
||||
key.stopPropagation();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
showsPanel: () => (
|
||||
<box flexDirection="column" height="100%">
|
||||
@@ -233,10 +144,7 @@ export function MyShowsPage(props: MyShowsPageProps) {
|
||||
</box>
|
||||
}
|
||||
>
|
||||
<scrollbox
|
||||
height="100%"
|
||||
focused={props.focused && focusPane() === "shows"}
|
||||
>
|
||||
<scrollbox height="100%" focused={props.depth}>
|
||||
<For each={shows()}>
|
||||
{(feed, index) => (
|
||||
<box
|
||||
73
src/pages/Player/PlayerPage.tsx
Normal file
73
src/pages/Player/PlayerPage.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { PlaybackControls } from "./PlaybackControls";
|
||||
import { RealtimeWaveform } from "./RealtimeWaveform";
|
||||
import { useAudio } from "@/hooks/useAudio";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
|
||||
export function PlayerPage() {
|
||||
const audio = useAudio();
|
||||
|
||||
const progressPercent = () => {
|
||||
const d = audio.duration();
|
||||
if (d <= 0) return 0;
|
||||
return Math.min(100, Math.round((audio.position() / d) * 100));
|
||||
};
|
||||
|
||||
const formatTime = (seconds: number) => {
|
||||
const m = Math.floor(seconds / 60);
|
||||
const s = Math.floor(seconds % 60);
|
||||
return `${m}:${String(s).padStart(2, "0")}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<box flexDirection="column" gap={1}>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<text>
|
||||
<strong>Now Playing</strong>
|
||||
</text>
|
||||
<text fg="gray">
|
||||
{formatTime(audio.position())} / {formatTime(audio.duration())} (
|
||||
{progressPercent()}%)
|
||||
</text>
|
||||
</box>
|
||||
|
||||
{audio.error() && <text fg="red">{audio.error()}</text>}
|
||||
|
||||
<box border padding={1} flexDirection="column" gap={1}>
|
||||
<text fg="white">
|
||||
<strong>{audio.currentEpisode()?.title}</strong>
|
||||
</text>
|
||||
<text fg="gray">{audio.currentEpisode()?.description}</text>
|
||||
|
||||
<RealtimeWaveform
|
||||
visualizerConfig={(() => {
|
||||
const viz = useAppStore().state().settings.visualizer;
|
||||
return {
|
||||
bars: viz.bars,
|
||||
noiseReduction: viz.noiseReduction,
|
||||
lowCutOff: viz.lowCutOff,
|
||||
highCutOff: viz.highCutOff,
|
||||
};
|
||||
})()}
|
||||
/>
|
||||
</box>
|
||||
|
||||
<PlaybackControls
|
||||
isPlaying={audio.isPlaying()}
|
||||
volume={audio.volume()}
|
||||
speed={audio.speed()}
|
||||
backendName={audio.backendName()}
|
||||
hasAudioUrl={!!audio.currentEpisode()?.audioUrl}
|
||||
onToggle={audio.togglePlayback}
|
||||
onPrev={() => audio.seek(0)}
|
||||
onNext={() => audio.seek(audio.currentEpisode()?.duration ?? 0)} //TODO: get next chronological(if feed) or episode(if MyShows)
|
||||
onSpeedChange={(s: number) => audio.setSpeed(s)}
|
||||
onVolumeChange={(v: number) => audio.setVolume(v)}
|
||||
/>
|
||||
|
||||
<text fg="gray">
|
||||
Space play/pause | Left/Right seek 10s | Up/Down volume | S speed | Esc
|
||||
back
|
||||
</text>
|
||||
</box>
|
||||
);
|
||||
}
|
||||
@@ -14,25 +14,11 @@ import {
|
||||
type CavaCoreConfig,
|
||||
} from "@/utils/cavacore";
|
||||
import { AudioStreamReader } from "@/utils/audio-stream-reader";
|
||||
import { useAudio } from "@/hooks/useAudio";
|
||||
|
||||
// ── Types ────────────────────────────────────────────────────────────
|
||||
|
||||
export type RealtimeWaveformProps = {
|
||||
/** Audio URL — used to start the ffmpeg decode stream */
|
||||
audioUrl: string;
|
||||
/** Current playback position in seconds */
|
||||
position: number;
|
||||
/** Total duration in seconds */
|
||||
duration: number;
|
||||
/** Whether audio is currently playing */
|
||||
isPlaying: boolean;
|
||||
/** Playback speed multiplier (default: 1) */
|
||||
speed?: number;
|
||||
/** Number of frequency bars / columns */
|
||||
resolution?: number;
|
||||
/** Callback when user clicks to seek */
|
||||
onSeek?: (seconds: number) => void;
|
||||
/** Visualizer configuration overrides */
|
||||
visualizerConfig?: Partial<CavaCoreConfig>;
|
||||
};
|
||||
|
||||
@@ -58,7 +44,7 @@ const SAMPLES_PER_FRAME = 512;
|
||||
// ── Component ────────────────────────────────────────────────────────
|
||||
|
||||
export function RealtimeWaveform(props: RealtimeWaveformProps) {
|
||||
const resolution = () => props.resolution ?? 32;
|
||||
const audio = useAudio();
|
||||
|
||||
// Frequency bar values (0.0–1.0 per bar)
|
||||
const [barData, setBarData] = createSignal<number[]>([]);
|
||||
@@ -95,7 +81,7 @@ export function RealtimeWaveform(props: RealtimeWaveformProps) {
|
||||
|
||||
// Initialize cavacore with current resolution + any overrides
|
||||
const config: CavaCoreConfig = {
|
||||
bars: resolution(),
|
||||
bars: 32,
|
||||
sampleRate: 44100,
|
||||
channels: 1,
|
||||
...props.visualizerConfig,
|
||||
@@ -151,27 +137,17 @@ export function RealtimeWaveform(props: RealtimeWaveformProps) {
|
||||
setBarData(Array.from(output));
|
||||
};
|
||||
|
||||
// ── Single unified effect: respond to all prop changes ─────────────
|
||||
//
|
||||
// Instead of three competing effects that each independently call
|
||||
// startVisualization() and race against each other, we use ONE effect
|
||||
// that tracks all relevant inputs. Position is read with untrack()
|
||||
// so normal playback drift doesn't trigger restarts.
|
||||
//
|
||||
// SolidJS on() with an array of accessors compares each element
|
||||
// individually, so the effect only fires when a value actually changes.
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
[
|
||||
() => props.isPlaying,
|
||||
() => props.audioUrl,
|
||||
() => props.speed ?? 1,
|
||||
resolution,
|
||||
audio.isPlaying,
|
||||
() => audio.currentEpisode()?.audioUrl ?? "", // may need to fire an error here
|
||||
audio.speed,
|
||||
() => 32,
|
||||
],
|
||||
([playing, url, speed]) => {
|
||||
if (playing && url) {
|
||||
const pos = untrack(() => props.position);
|
||||
const pos = untrack(audio.position);
|
||||
startVisualization(url, pos, speed);
|
||||
} else {
|
||||
stopVisualization();
|
||||
@@ -189,10 +165,8 @@ export function RealtimeWaveform(props: RealtimeWaveformProps) {
|
||||
|
||||
let lastSyncPosition = 0;
|
||||
createEffect(
|
||||
on(
|
||||
() => props.position,
|
||||
(pos) => {
|
||||
if (!props.isPlaying || !reader?.running) {
|
||||
on(audio.position, (pos) => {
|
||||
if (!audio.isPlaying || !reader?.running) {
|
||||
lastSyncPosition = pos;
|
||||
return;
|
||||
}
|
||||
@@ -201,11 +175,9 @@ export function RealtimeWaveform(props: RealtimeWaveformProps) {
|
||||
lastSyncPosition = pos;
|
||||
|
||||
if (delta > 2) {
|
||||
const speed = props.speed ?? 1;
|
||||
reader.restart(pos, speed);
|
||||
reader.restart(pos, audio.speed() ?? 1);
|
||||
}
|
||||
},
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
// Cleanup on unmount
|
||||
@@ -224,11 +196,13 @@ export function RealtimeWaveform(props: RealtimeWaveformProps) {
|
||||
// ── Rendering ──────────────────────────────────────────────────────
|
||||
|
||||
const playedRatio = () =>
|
||||
props.duration <= 0 ? 0 : Math.min(1, props.position / props.duration);
|
||||
audio.duration() <= 0
|
||||
? 0
|
||||
: Math.min(1, audio.position() / audio.duration());
|
||||
|
||||
const renderLine = () => {
|
||||
const bars = barData();
|
||||
const numBars = resolution();
|
||||
const numBars = 32;
|
||||
|
||||
// If no data yet, show empty placeholder
|
||||
if (bars.length === 0) {
|
||||
@@ -241,7 +215,7 @@ export function RealtimeWaveform(props: RealtimeWaveformProps) {
|
||||
}
|
||||
|
||||
const played = Math.floor(numBars * playedRatio());
|
||||
const playedColor = props.isPlaying ? "#6fa8ff" : "#7d8590";
|
||||
const playedColor = audio.isPlaying() ? "#6fa8ff" : "#7d8590";
|
||||
const futureColor = "#3b4252";
|
||||
|
||||
const playedChars = bars
|
||||
@@ -263,13 +237,13 @@ export function RealtimeWaveform(props: RealtimeWaveformProps) {
|
||||
};
|
||||
|
||||
const handleClick = (event: { x: number }) => {
|
||||
const numBars = resolution();
|
||||
const ratio = numBars === 0 ? 0 : event.x / numBars;
|
||||
const numBars = 32;
|
||||
const ratio = event.x / numBars;
|
||||
const next = Math.max(
|
||||
0,
|
||||
Math.min(props.duration, Math.round(props.duration * ratio)),
|
||||
Math.min(audio.duration(), Math.round(audio.duration() * ratio)),
|
||||
);
|
||||
props.onSeek?.(next);
|
||||
audio.seek(next);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -23,7 +23,7 @@ const SECTIONS: Array<{ id: SectionId; label: string }> = [
|
||||
{ id: "account", label: "Account" },
|
||||
];
|
||||
|
||||
export function SettingsScreen(props: SettingsScreenProps) {
|
||||
export function SettingsPage(props: SettingsScreenProps) {
|
||||
const { theme } = useTheme();
|
||||
const [activeSection, setActiveSection] = createSignal<SectionId>("sync");
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
import { useKeyboard } from "@opentui/solid";
|
||||
import { PlaybackControls } from "./PlaybackControls";
|
||||
import { RealtimeWaveform } from "./RealtimeWaveform";
|
||||
import { useAudio } from "@/hooks/useAudio";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import type { Episode } from "@/types/episode";
|
||||
|
||||
type PlayerProps = {
|
||||
focused: boolean;
|
||||
episode?: Episode | null;
|
||||
onExit?: () => void;
|
||||
};
|
||||
|
||||
const SAMPLE_EPISODE: Episode = {
|
||||
id: "sample-ep",
|
||||
podcastId: "sample-podcast",
|
||||
title: "A Tour of the Productive Mind",
|
||||
description: "A short guided session on building creative focus.",
|
||||
audioUrl: "",
|
||||
duration: 2780,
|
||||
pubDate: new Date(),
|
||||
};
|
||||
|
||||
export function Player(props: PlayerProps) {
|
||||
const audio = useAudio();
|
||||
|
||||
// The episode to display — prefer a passed-in episode, then the
|
||||
// currently-playing episode, then fall back to the sample.
|
||||
const episode = () =>
|
||||
props.episode ?? audio.currentEpisode() ?? SAMPLE_EPISODE;
|
||||
const dur = () => audio.duration() || episode().duration || 1;
|
||||
|
||||
useKeyboard((key: { name: string }) => {
|
||||
if (!props.focused) return;
|
||||
if (key.name === "space") {
|
||||
if (audio.currentEpisode()) {
|
||||
audio.togglePlayback();
|
||||
} else {
|
||||
// Nothing loaded yet — start playing the displayed episode
|
||||
const ep = episode();
|
||||
if (ep.audioUrl) {
|
||||
audio.play(ep);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (key.name === "escape") {
|
||||
props.onExit?.();
|
||||
return;
|
||||
}
|
||||
if (key.name === "left") {
|
||||
audio.seekRelative(-10);
|
||||
}
|
||||
if (key.name === "right") {
|
||||
audio.seekRelative(10);
|
||||
}
|
||||
if (key.name === "up") {
|
||||
audio.setVolume(Math.min(1, Number((audio.volume() + 0.05).toFixed(2))));
|
||||
}
|
||||
if (key.name === "down") {
|
||||
audio.setVolume(Math.max(0, Number((audio.volume() - 0.05).toFixed(2))));
|
||||
}
|
||||
if (key.name === "s") {
|
||||
const next =
|
||||
audio.speed() >= 2 ? 0.5 : Number((audio.speed() + 0.25).toFixed(2));
|
||||
audio.setSpeed(next);
|
||||
}
|
||||
});
|
||||
|
||||
const progressPercent = () => {
|
||||
const d = dur();
|
||||
if (d <= 0) return 0;
|
||||
return Math.min(100, Math.round((audio.position() / d) * 100));
|
||||
};
|
||||
|
||||
const formatTime = (seconds: number) => {
|
||||
const m = Math.floor(seconds / 60);
|
||||
const s = Math.floor(seconds % 60);
|
||||
return `${m}:${String(s).padStart(2, "0")}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<box flexDirection="column" gap={1}>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<text>
|
||||
<strong>Now Playing</strong>
|
||||
</text>
|
||||
<text fg="gray">
|
||||
{formatTime(audio.position())} / {formatTime(dur())} (
|
||||
{progressPercent()}%)
|
||||
</text>
|
||||
</box>
|
||||
|
||||
{audio.error() && <text fg="red">{audio.error()}</text>}
|
||||
|
||||
<box border padding={1} flexDirection="column" gap={1}>
|
||||
<text fg="white">
|
||||
<strong>{episode().title}</strong>
|
||||
</text>
|
||||
<text fg="gray">{episode().description}</text>
|
||||
|
||||
<RealtimeWaveform
|
||||
audioUrl={episode().audioUrl}
|
||||
position={audio.position()}
|
||||
duration={dur()}
|
||||
isPlaying={audio.isPlaying()}
|
||||
speed={audio.speed()}
|
||||
onSeek={(next: number) => audio.seek(next)}
|
||||
visualizerConfig={(() => {
|
||||
const viz = useAppStore().state().settings.visualizer;
|
||||
return {
|
||||
bars: viz.bars,
|
||||
noiseReduction: viz.noiseReduction,
|
||||
lowCutOff: viz.lowCutOff,
|
||||
highCutOff: viz.highCutOff,
|
||||
};
|
||||
})()}
|
||||
/>
|
||||
</box>
|
||||
|
||||
<PlaybackControls
|
||||
isPlaying={audio.isPlaying()}
|
||||
volume={audio.volume()}
|
||||
speed={audio.speed()}
|
||||
backendName={audio.backendName()}
|
||||
hasAudioUrl={!!episode().audioUrl}
|
||||
onToggle={() => {
|
||||
if (audio.currentEpisode()) {
|
||||
audio.togglePlayback();
|
||||
} else {
|
||||
const ep = episode();
|
||||
if (ep.audioUrl) audio.play(ep);
|
||||
}
|
||||
}}
|
||||
onPrev={() => audio.seek(0)}
|
||||
onNext={() => audio.seek(dur())}
|
||||
onSpeedChange={(s: number) => audio.setSpeed(s)}
|
||||
onVolumeChange={(v: number) => audio.setVolume(v)}
|
||||
/>
|
||||
|
||||
<text fg="gray">
|
||||
Space play/pause | Left/Right seek 10s | Up/Down volume | S speed | Esc
|
||||
back
|
||||
</text>
|
||||
</box>
|
||||
);
|
||||
}
|
||||
@@ -1,49 +1,9 @@
|
||||
enum FEEDTABTYPE {
|
||||
LATEST,
|
||||
}
|
||||
export const FeedTab = {
|
||||
[FEEDTABTYPE.LATEST]: {
|
||||
size: 1,
|
||||
title: "Feed - Latest Episodes",
|
||||
scrolling: true,
|
||||
},
|
||||
};
|
||||
enum MYSHOWSTYPE {
|
||||
SHOWLIST,
|
||||
EPISODELIST,
|
||||
}
|
||||
export const MyShowsTab = {
|
||||
[MYSHOWSTYPE.SHOWLIST]: { size: 0.3, title: "My Shows", scrolling: true },
|
||||
[MYSHOWSTYPE.EPISODELIST]: {
|
||||
size: 0.7,
|
||||
title: "<SHOW> - Episodes",
|
||||
scrolling: true,
|
||||
},
|
||||
};
|
||||
|
||||
enum DiscoverTab {
|
||||
CATEGORIES,
|
||||
CATEGORYLIST,
|
||||
}
|
||||
|
||||
export enum CATEGORIES {
|
||||
ALL,
|
||||
TECHNOLOGY,
|
||||
SCIENCE,
|
||||
COMEDY,
|
||||
NEWS,
|
||||
BUSINESS,
|
||||
HEALTH,
|
||||
EDUCATION,
|
||||
SPORTS,
|
||||
TRUECRIME,
|
||||
ARTS,
|
||||
}
|
||||
export const SearchTab = [];
|
||||
|
||||
export const PlayerTab = [];
|
||||
|
||||
export const SettingsTab = [];
|
||||
import { DiscoverPage } from "@/pages/Discover/DiscoverPage";
|
||||
import { FeedPage, FeedPaneCount } from "@/pages/Feed/FeedPage";
|
||||
import { MyShowsPage, MyShowsPaneCount } from "@/pages/MyShows/MyShowsPage";
|
||||
import { PlayerPage } from "@/pages/Player/PlayerPage";
|
||||
import { SearchPage } from "@/pages/Search/SearchPage";
|
||||
import { SettingsPage } from "@/pages/Settings/SettingsPage";
|
||||
|
||||
export enum TABS {
|
||||
FEED,
|
||||
@@ -55,10 +15,18 @@ export enum TABS {
|
||||
}
|
||||
|
||||
export const LayerGraph = {
|
||||
[TABS.FEED]: FeedTab,
|
||||
[TABS.MYSHOWS]: MyShowsTab,
|
||||
[TABS.DISCOVER]: DiscoverTab,
|
||||
[TABS.SEARCH]: SearchTab,
|
||||
[TABS.PLAYER]: PlayerTab,
|
||||
[TABS.SETTINGS]: SettingsTab,
|
||||
[TABS.FEED]: FeedPage,
|
||||
[TABS.MYSHOWS]: MyShowsPage,
|
||||
[TABS.DISCOVER]: DiscoverPage,
|
||||
[TABS.SEARCH]: SearchPage,
|
||||
[TABS.PLAYER]: PlayerPage,
|
||||
[TABS.SETTINGS]: SettingsPage,
|
||||
};
|
||||
export const LayerDepths = {
|
||||
[TABS.FEED]: FeedPaneCount,
|
||||
[TABS.MYSHOWS]: MyShowsPaneCount,
|
||||
[TABS.DISCOVER]: DiscoverPaneCount,
|
||||
[TABS.SEARCH]: SearchPaneCount,
|
||||
[TABS.PLAYER]: PlayerPaneCount,
|
||||
[TABS.SETTINGS]: SettingPaneCount,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user