cooking
This commit is contained in:
269
src/App.tsx
269
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={
|
||||
<TabNavigation activeTab={activeTab()} onTabSelect={setActiveTab} />
|
||||
}
|
||||
panels={getPanels().panels}
|
||||
activePanelIndex={getPanels().activePanelIndex}
|
||||
/>
|
||||
<box
|
||||
flexDirection="row"
|
||||
width="100%"
|
||||
height="100%"
|
||||
backgroundColor={theme.surface}
|
||||
>
|
||||
<TabNavigation activeTab={activeTab()} onTabSelect={setActiveTab} />
|
||||
{LayerGraph[activeTab()]({ depth: activeDepth })}
|
||||
</box>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user