device switch
This commit is contained in:
@@ -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(),
|
||||
});
|
||||
|
||||
|
||||
@@ -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<
|
||||
<box
|
||||
border={!!props.border}
|
||||
borderColor={props.selected() ? theme.surface : theme.border}
|
||||
backgroundColor={props.selected() ? theme.primary : theme.surface}
|
||||
backgroundColor={
|
||||
props.selected()
|
||||
? theme.primary
|
||||
: themeContext.selected === "system"
|
||||
? "transparent"
|
||||
: themeContext.theme.surface
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
{child()}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
<box border padding={0} onMouseDown={() => setShowInfo((v) => !v)} borderColor={theme.border}>
|
||||
<SelectableText selected={() => false} primary>[i] {showInfo() ? "Hide" : "Show"} Info</SelectableText>
|
||||
</box>
|
||||
<box border padding={0} onMouseDown={() => props.feed.podcast.onToggleVisibility?.(props.feed.id)} borderColor={theme.border}>
|
||||
<SelectableText selected={() => false} primary>[v] Toggle Visibility</SelectableText>
|
||||
</box>
|
||||
</box>
|
||||
|
||||
{/* Podcast info section */}
|
||||
@@ -125,6 +133,9 @@ export function FeedDetail(props: FeedDetailProps) {
|
||||
</SelectableText>
|
||||
{props.feed.isPinned && <SelectableText selected={() => false} tertiary>[Pinned]</SelectableText>}
|
||||
</box>
|
||||
<box flexDirection="row" gap={1}>
|
||||
<SelectableText selected={() => false} tertiary>[v] Toggle Visibility</SelectableText>
|
||||
</box>
|
||||
</box>
|
||||
</Show>
|
||||
|
||||
|
||||
@@ -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) {
|
||||
</text>
|
||||
</box>
|
||||
</box>
|
||||
|
||||
{/* Private filter */}
|
||||
<box
|
||||
border
|
||||
padding={0}
|
||||
backgroundColor={focusField() === "private" ? theme.backgroundElement : undefined}
|
||||
>
|
||||
<box flexDirection="row" gap={1}>
|
||||
<text fg={focusField() === "private" ? theme.primary : theme.textMuted}>
|
||||
Private:
|
||||
</text>
|
||||
<text fg={props.filter.showPrivate ? theme.warning : theme.textMuted}>
|
||||
{props.filter.showPrivate ? "Yes" : "No"}
|
||||
</text>
|
||||
</box>
|
||||
</box>
|
||||
</box>
|
||||
|
||||
{/* Search box */}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -69,6 +69,8 @@ export interface FeedFilter {
|
||||
sortBy?: FeedSortField
|
||||
/** Sort direction */
|
||||
sortDirection?: "asc" | "desc"
|
||||
/** Show private feeds */
|
||||
showPrivate?: boolean
|
||||
}
|
||||
|
||||
/** Feed sort fields */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user