device switch
This commit is contained in:
@@ -41,8 +41,9 @@ export function App() {
|
|||||||
const audioNav = useAudioNavStore();
|
const audioNav = useAudioNavStore();
|
||||||
|
|
||||||
useMultimediaKeys({
|
useMultimediaKeys({
|
||||||
playerFocused: () => nav.activeTab === TABS.PLAYER && nav.activeDepth > 0,
|
playerFocused: () =>
|
||||||
inputFocused: () => nav.inputFocused,
|
nav.activeTab() === TABS.PLAYER && nav.activeDepth() > 0,
|
||||||
|
inputFocused: () => nav.inputFocused(),
|
||||||
hasEpisode: () => !!audio.currentEpisode(),
|
hasEpisode: () => !!audio.currentEpisode(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ export const SelectableBox: ParentComponent<
|
|||||||
selected: () => boolean;
|
selected: () => boolean;
|
||||||
} & BoxOptions
|
} & BoxOptions
|
||||||
> = (props) => {
|
> = (props) => {
|
||||||
const { theme } = useTheme();
|
const themeContext = useTheme();
|
||||||
|
const { theme } = themeContext;
|
||||||
|
|
||||||
const child = solidChildren(() => props.children);
|
const child = solidChildren(() => props.children);
|
||||||
|
|
||||||
@@ -16,7 +17,13 @@ export const SelectableBox: ParentComponent<
|
|||||||
<box
|
<box
|
||||||
border={!!props.border}
|
border={!!props.border}
|
||||||
borderColor={props.selected() ? theme.surface : theme.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}
|
{...props}
|
||||||
>
|
>
|
||||||
{child()}
|
{child()}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createSignal } from "solid-js";
|
import { createEffect, createSignal, on } from "solid-js";
|
||||||
import { createSimpleContext } from "./helper";
|
import { createSimpleContext } from "./helper";
|
||||||
import { TABS, TabsCount } from "@/utils/navigation";
|
import { TABS, TabsCount } from "@/utils/navigation";
|
||||||
|
|
||||||
@@ -10,6 +10,13 @@ export const { use: useNavigation, provider: NavigationProvider } =
|
|||||||
const [activeDepth, setActiveDepth] = createSignal(0);
|
const [activeDepth, setActiveDepth] = createSignal(0);
|
||||||
const [inputFocused, setInputFocused] = createSignal(false);
|
const [inputFocused, setInputFocused] = createSignal(false);
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on(
|
||||||
|
() => activeTab,
|
||||||
|
() => setActiveDepth(0),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
//conveniences
|
//conveniences
|
||||||
const nextTab = () => {
|
const nextTab = () => {
|
||||||
if (activeTab() >= TabsCount) {
|
if (activeTab() >= TabsCount) {
|
||||||
|
|||||||
@@ -56,6 +56,11 @@ export function FeedDetail(props: FeedDetailProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (key.name === "v") {
|
||||||
|
props.feed.podcast.onToggleVisibility?.(props.feed.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (key.name === "up" || key.name === "k") {
|
if (key.name === "up" || key.name === "k") {
|
||||||
setSelectedIndex((i) => Math.max(0, i - 1));
|
setSelectedIndex((i) => Math.max(0, i - 1));
|
||||||
} else if (key.name === "down" || key.name === "j") {
|
} 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}>
|
<box border padding={0} onMouseDown={() => setShowInfo((v) => !v)} borderColor={theme.border}>
|
||||||
<SelectableText selected={() => false} primary>[i] {showInfo() ? "Hide" : "Show"} Info</SelectableText>
|
<SelectableText selected={() => false} primary>[i] {showInfo() ? "Hide" : "Show"} Info</SelectableText>
|
||||||
</box>
|
</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>
|
</box>
|
||||||
|
|
||||||
{/* Podcast info section */}
|
{/* Podcast info section */}
|
||||||
@@ -125,6 +133,9 @@ export function FeedDetail(props: FeedDetailProps) {
|
|||||||
</SelectableText>
|
</SelectableText>
|
||||||
{props.feed.isPinned && <SelectableText selected={() => false} tertiary>[Pinned]</SelectableText>}
|
{props.feed.isPinned && <SelectableText selected={() => false} tertiary>[Pinned]</SelectableText>}
|
||||||
</box>
|
</box>
|
||||||
|
<box flexDirection="row" gap={1}>
|
||||||
|
<SelectableText selected={() => false} tertiary>[v] Toggle Visibility</SelectableText>
|
||||||
|
</box>
|
||||||
</box>
|
</box>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ interface FeedFilterProps {
|
|||||||
onFilterChange: (filter: FeedFilter) => void;
|
onFilterChange: (filter: FeedFilter) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type FilterField = "visibility" | "sort" | "pinned" | "search";
|
type FilterField = "visibility" | "sort" | "pinned" | "private" | "search";
|
||||||
|
|
||||||
export function FeedFilterComponent(props: FeedFilterProps) {
|
export function FeedFilterComponent(props: FeedFilterProps) {
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
@@ -23,7 +23,7 @@ export function FeedFilterComponent(props: FeedFilterProps) {
|
|||||||
props.filter.searchQuery || "",
|
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 }) => {
|
const handleKeyPress = (key: { name: string; shift?: boolean }) => {
|
||||||
if (key.name === "tab") {
|
if (key.name === "tab") {
|
||||||
@@ -39,10 +39,14 @@ export function FeedFilterComponent(props: FeedFilterProps) {
|
|||||||
cycleSort();
|
cycleSort();
|
||||||
} else if (focusField() === "pinned") {
|
} else if (focusField() === "pinned") {
|
||||||
togglePinned();
|
togglePinned();
|
||||||
|
} else if (focusField() === "private") {
|
||||||
|
togglePrivate();
|
||||||
}
|
}
|
||||||
} else if (key.name === "space") {
|
} else if (key.name === "space") {
|
||||||
if (focusField() === "pinned") {
|
if (focusField() === "pinned") {
|
||||||
togglePinned();
|
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) => {
|
const handleSearchInput = (value: string) => {
|
||||||
setSearchValue(value);
|
setSearchValue(value);
|
||||||
props.onFilterChange({ ...props.filter, searchQuery: value });
|
props.onFilterChange({ ...props.filter, searchQuery: value });
|
||||||
@@ -160,6 +171,22 @@ export function FeedFilterComponent(props: FeedFilterProps) {
|
|||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
</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>
|
</box>
|
||||||
|
|
||||||
{/* Search box */}
|
{/* Search box */}
|
||||||
|
|||||||
@@ -58,6 +58,13 @@ export function FeedList(props: FeedListProps) {
|
|||||||
if (feed) {
|
if (feed) {
|
||||||
feedStore.togglePinned(feed.id);
|
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") {
|
} else if (key.name === "f") {
|
||||||
// Cycle visibility filter
|
// Cycle visibility filter
|
||||||
cycleVisibilityFilter();
|
cycleVisibilityFilter();
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
} from "../utils/feeds-persistence";
|
} from "../utils/feeds-persistence";
|
||||||
import { useDownloadStore } from "./download";
|
import { useDownloadStore } from "./download";
|
||||||
import { DownloadStatus } from "../types/episode";
|
import { DownloadStatus } from "../types/episode";
|
||||||
|
import { useAuthStore } from "./auth";
|
||||||
|
|
||||||
/** Max episodes to load per page/chunk */
|
/** Max episodes to load per page/chunk */
|
||||||
const MAX_EPISODES_REFRESH = 50;
|
const MAX_EPISODES_REFRESH = 50;
|
||||||
@@ -61,10 +62,14 @@ export function createFeedStore() {
|
|||||||
const getFilteredFeeds = (): Feed[] => {
|
const getFilteredFeeds = (): Feed[] => {
|
||||||
let result = [...feeds()];
|
let result = [...feeds()];
|
||||||
const f = filter();
|
const f = filter();
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
// Filter by visibility
|
// Filter by visibility
|
||||||
if (f.visibility && f.visibility !== "all") {
|
if (f.visibility && f.visibility !== "all") {
|
||||||
result = result.filter((feed) => feed.visibility === f.visibility);
|
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
|
// Filter by source
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ export interface FeedFilter {
|
|||||||
sortBy?: FeedSortField
|
sortBy?: FeedSortField
|
||||||
/** Sort direction */
|
/** Sort direction */
|
||||||
sortDirection?: "asc" | "desc"
|
sortDirection?: "asc" | "desc"
|
||||||
|
/** Show private feeds */
|
||||||
|
showPrivate?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Feed sort fields */
|
/** Feed sort fields */
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export interface Podcast {
|
|||||||
lastUpdated: Date
|
lastUpdated: Date
|
||||||
/** Whether the podcast is currently subscribed */
|
/** Whether the podcast is currently subscribed */
|
||||||
isSubscribed: boolean
|
isSubscribed: boolean
|
||||||
|
/** Callback to toggle feed visibility */
|
||||||
|
onToggleVisibility?: (feedId: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Podcast with episodes included */
|
/** Podcast with episodes included */
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import type { SyncData } from "../types/sync-json"
|
|||||||
import type { SyncDataXML } from "../types/sync-xml"
|
import type { SyncDataXML } from "../types/sync-xml"
|
||||||
import { validateJSONSync, validateXMLSync } from "./sync-validation"
|
import { validateJSONSync, validateXMLSync } from "./sync-validation"
|
||||||
import { syncFormats } from "../constants/sync-formats"
|
import { syncFormats } from "../constants/sync-formats"
|
||||||
|
import { FeedVisibility } from "../types/feed"
|
||||||
|
|
||||||
export function exportToJSON(data: SyncData): string {
|
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 {
|
export function importFromJSON(json: string): SyncData {
|
||||||
|
|||||||
Reference in New Issue
Block a user