diff --git a/src/App.tsx b/src/App.tsx index 8d5b1b2..6e3b756 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -41,8 +41,9 @@ export function App() { const audioNav = useAudioNavStore(); useMultimediaKeys({ - playerFocused: () => nav.activeTab === TABS.PLAYER && nav.activeDepth > 0, - inputFocused: () => nav.inputFocused, + playerFocused: () => + nav.activeTab() === TABS.PLAYER && nav.activeDepth() > 0, + inputFocused: () => nav.inputFocused(), hasEpisode: () => !!audio.currentEpisode(), }); diff --git a/src/components/Selectable.tsx b/src/components/Selectable.tsx index 69692f4..a8fc5aa 100644 --- a/src/components/Selectable.tsx +++ b/src/components/Selectable.tsx @@ -8,7 +8,8 @@ export const SelectableBox: ParentComponent< selected: () => boolean; } & BoxOptions > = (props) => { - const { theme } = useTheme(); + const themeContext = useTheme(); + const { theme } = themeContext; const child = solidChildren(() => props.children); @@ -16,7 +17,13 @@ export const SelectableBox: ParentComponent< {child()} diff --git a/src/context/NavigationContext.tsx b/src/context/NavigationContext.tsx index bfc9e7e..8ef851a 100644 --- a/src/context/NavigationContext.tsx +++ b/src/context/NavigationContext.tsx @@ -1,4 +1,4 @@ -import { createSignal } from "solid-js"; +import { createEffect, createSignal, on } from "solid-js"; import { createSimpleContext } from "./helper"; import { TABS, TabsCount } from "@/utils/navigation"; @@ -10,6 +10,13 @@ export const { use: useNavigation, provider: NavigationProvider } = const [activeDepth, setActiveDepth] = createSignal(0); const [inputFocused, setInputFocused] = createSignal(false); + createEffect( + on( + () => activeTab, + () => setActiveDepth(0), + ), + ); + //conveniences const nextTab = () => { if (activeTab() >= TabsCount) { diff --git a/src/pages/Feed/FeedDetail.tsx b/src/pages/Feed/FeedDetail.tsx index 6475a23..6b4c958 100644 --- a/src/pages/Feed/FeedDetail.tsx +++ b/src/pages/Feed/FeedDetail.tsx @@ -56,6 +56,11 @@ export function FeedDetail(props: FeedDetailProps) { return; } + if (key.name === "v") { + props.feed.podcast.onToggleVisibility?.(props.feed.id); + return; + } + if (key.name === "up" || key.name === "k") { setSelectedIndex((i) => Math.max(0, i - 1)); } else if (key.name === "down" || key.name === "j") { @@ -91,6 +96,9 @@ export function FeedDetail(props: FeedDetailProps) { setShowInfo((v) => !v)} borderColor={theme.border}> false} primary>[i] {showInfo() ? "Hide" : "Show"} Info + props.feed.podcast.onToggleVisibility?.(props.feed.id)} borderColor={theme.border}> + false} primary>[v] Toggle Visibility + {/* Podcast info section */} @@ -125,6 +133,9 @@ export function FeedDetail(props: FeedDetailProps) { {props.feed.isPinned && false} tertiary>[Pinned]} + + false} tertiary>[v] Toggle Visibility + diff --git a/src/pages/Feed/FeedFilter.tsx b/src/pages/Feed/FeedFilter.tsx index 20135a2..678ec01 100644 --- a/src/pages/Feed/FeedFilter.tsx +++ b/src/pages/Feed/FeedFilter.tsx @@ -14,7 +14,7 @@ interface FeedFilterProps { onFilterChange: (filter: FeedFilter) => void; } -type FilterField = "visibility" | "sort" | "pinned" | "search"; +type FilterField = "visibility" | "sort" | "pinned" | "private" | "search"; export function FeedFilterComponent(props: FeedFilterProps) { const { theme } = useTheme(); @@ -23,7 +23,7 @@ export function FeedFilterComponent(props: FeedFilterProps) { props.filter.searchQuery || "", ); - const fields: FilterField[] = ["visibility", "sort", "pinned", "search"]; + const fields: FilterField[] = ["visibility", "sort", "pinned", "private", "search"]; const handleKeyPress = (key: { name: string; shift?: boolean }) => { if (key.name === "tab") { @@ -39,10 +39,14 @@ export function FeedFilterComponent(props: FeedFilterProps) { cycleSort(); } else if (focusField() === "pinned") { togglePinned(); + } else if (focusField() === "private") { + togglePrivate(); } } else if (key.name === "space") { if (focusField() === "pinned") { togglePinned(); + } else if (focusField() === "private") { + togglePrivate(); } } }; @@ -77,6 +81,13 @@ export function FeedFilterComponent(props: FeedFilterProps) { }); }; + const togglePrivate = () => { + props.onFilterChange({ + ...props.filter, + showPrivate: !props.filter.showPrivate, + }); + }; + const handleSearchInput = (value: string) => { setSearchValue(value); props.onFilterChange({ ...props.filter, searchQuery: value }); @@ -160,6 +171,22 @@ export function FeedFilterComponent(props: FeedFilterProps) { + + {/* Private filter */} + + + + Private: + + + {props.filter.showPrivate ? "Yes" : "No"} + + + {/* Search box */} diff --git a/src/pages/Feed/FeedList.tsx b/src/pages/Feed/FeedList.tsx index 6d9fe4b..59ee128 100644 --- a/src/pages/Feed/FeedList.tsx +++ b/src/pages/Feed/FeedList.tsx @@ -58,6 +58,13 @@ export function FeedList(props: FeedListProps) { if (feed) { feedStore.togglePinned(feed.id); } + } else if (key.name === "v") { + // Toggle visibility on selected feed + const feed = feeds[selectedIndex()]; + if (feed) { + const newVisibility = feed.visibility === FeedVisibility.PUBLIC ? FeedVisibility.PRIVATE : FeedVisibility.PUBLIC; + feedStore.updateFeed(feed.id, { visibility: newVisibility }); + } } else if (key.name === "f") { // Cycle visibility filter cycleVisibilityFilter(); diff --git a/src/stores/feed.ts b/src/stores/feed.ts index b7cfbe8..0b969fe 100644 --- a/src/stores/feed.ts +++ b/src/stores/feed.ts @@ -19,6 +19,7 @@ import { } from "../utils/feeds-persistence"; import { useDownloadStore } from "./download"; import { DownloadStatus } from "../types/episode"; +import { useAuthStore } from "./auth"; /** Max episodes to load per page/chunk */ const MAX_EPISODES_REFRESH = 50; @@ -61,10 +62,14 @@ export function createFeedStore() { const getFilteredFeeds = (): Feed[] => { let result = [...feeds()]; const f = filter(); + const authStore = useAuthStore(); // Filter by visibility if (f.visibility && f.visibility !== "all") { result = result.filter((feed) => feed.visibility === f.visibility); + } else if (f.visibility === "all") { + // Only show private feeds if authenticated + result = result.filter((feed) => feed.visibility === FeedVisibility.PUBLIC || authStore.isAuthenticated); } // Filter by source diff --git a/src/types/feed.ts b/src/types/feed.ts index ab190b6..d99698c 100644 --- a/src/types/feed.ts +++ b/src/types/feed.ts @@ -69,6 +69,8 @@ export interface FeedFilter { sortBy?: FeedSortField /** Sort direction */ sortDirection?: "asc" | "desc" + /** Show private feeds */ + showPrivate?: boolean } /** Feed sort fields */ diff --git a/src/types/podcast.ts b/src/types/podcast.ts index 0cd6f73..3ccd595 100644 --- a/src/types/podcast.ts +++ b/src/types/podcast.ts @@ -26,6 +26,8 @@ export interface Podcast { lastUpdated: Date /** Whether the podcast is currently subscribed */ isSubscribed: boolean + /** Callback to toggle feed visibility */ + onToggleVisibility?: (feedId: string) => void } /** Podcast with episodes included */ diff --git a/src/utils/sync.ts b/src/utils/sync.ts index 398283d..b635aee 100644 --- a/src/utils/sync.ts +++ b/src/utils/sync.ts @@ -2,9 +2,10 @@ import type { SyncData } from "../types/sync-json" import type { SyncDataXML } from "../types/sync-xml" import { validateJSONSync, validateXMLSync } from "./sync-validation" import { syncFormats } from "../constants/sync-formats" +import { FeedVisibility } from "../types/feed" export function exportToJSON(data: SyncData): string { - return `{\n "version": "${data.version}",\n "lastSyncedAt": "${data.lastSyncedAt}",\n "feeds": [],\n "sources": [],\n "settings": {\n "theme": "${data.settings.theme}",\n "playbackSpeed": ${data.settings.playbackSpeed},\n "downloadPath": "${data.settings.downloadPath}"\n },\n "preferences": {\n "showExplicit": ${data.preferences.showExplicit},\n "autoDownload": ${data.preferences.autoDownload}\n }\n}` + return `{\n "version": "${data.version}",\n "lastSyncedAt": "${data.lastSyncedAt}",\n "feeds": [],\n "sources": [],\n "settings": {\n "theme": "${data.settings.theme}",\n "playbackSpeed": ${data.settings.playbackSpeed},\n "downloadPath": "${data.settings.downloadPath}"\n },\n "preferences": {\n "showExplicit": ${data.preferences.showExplicit},\n "autoDownload": ${data.preferences.autoDownload}\n }\}` } export function importFromJSON(json: string): SyncData {