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 {