feat: feed UI, in-app player, offline mode, settings

Adds an end-to-end integration test that runs the real pipeline against
live YouTube Atom feeds, verifying the merged feed is newest-first
across channels.
This commit is contained in:
vincent
2026-08-29 02:34:14 +02:00
parent a30423e4b5
commit e75d896933
16 changed files with 1059 additions and 2 deletions
+79
View File
@@ -0,0 +1,79 @@
import { invoke, convertFileSrc } from "@tauri-apps/api/core";
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import { open } from "@tauri-apps/plugin-dialog";
import type {
ChannelWithCount,
DownloadProgressEvent,
DownloadStateEvent,
FeedFilter,
FeedItem,
Prereqs,
RefreshProgress,
RefreshSummary,
} from "./types";
export const checkPrereqs = () => invoke<Prereqs>("check_prereqs");
export const listChannels = () => invoke<ChannelWithCount[]>("list_channels");
export const listFeed = (filter: FeedFilter) =>
invoke<FeedItem[]>("list_feed", { filter });
export const refreshFeeds = () => invoke<RefreshSummary>("refresh_feeds");
export const downloadVideo = (videoId: string) =>
invoke<void>("download_video", { videoId });
export const cancelDownload = (videoId: string) =>
invoke<void>("cancel_download", { videoId });
export const deleteDownload = (videoId: string) =>
invoke<void>("delete_download", { videoId });
export const getConnectivity = () => invoke<boolean>("get_connectivity");
export const setLibraryPath = (path: string) =>
invoke<string>("set_library_path", { path });
export const openExternal = (url: string) =>
invoke<void>("open_external", { url });
/** Opens the native file picker for a Takeout subscriptions.csv. */
export async function pickAndImportTakeout(): Promise<number | null> {
const path = await open({
multiple: false,
directory: false,
filters: [{ name: "Takeout subscriptions", extensions: ["csv"] }],
});
if (typeof path !== "string") return null;
return invoke<number>("import_takeout_csv", { path });
}
export async function pickLibraryFolder(): Promise<string | null> {
const path = await open({ directory: true, multiple: false });
if (typeof path !== "string") return null;
return setLibraryPath(path);
}
export const onRefreshProgress = (cb: (p: RefreshProgress) => void) =>
listen<RefreshProgress>("refresh:progress", (e) => cb(e.payload));
export const onDownloadProgress = (cb: (p: DownloadProgressEvent) => void) =>
listen<DownloadProgressEvent>("download:progress", (e) => cb(e.payload));
export const onDownloadState = (cb: (p: DownloadStateEvent) => void) =>
listen<DownloadStateEvent>("download:state", (e) => cb(e.payload));
export type { UnlistenFn };
/** Local file path -> a URL the webview is allowed to load. */
export const fileUrl = (path: string) => convertFileSrc(path);
/**
* Prefers the on-disk thumbnail so the feed still renders with no network,
* falling back to the remote URL for videos not yet cached.
*/
export const thumbSrc = (item: FeedItem, online: boolean) => {
if (item.thumb_path) return convertFileSrc(item.thumb_path);
return online ? item.thumb_url : "";
};