[READ-ONLY] Mirror of https://github.com/probablykasper/kadium. App for staying ontop of YouTube channels' uploads kadium.kasper.space
linux macos notifications tauri windows youtube
0

Configure Feed

Select the types of activity you want to include in your feed.

chore: update

GHOST (Aug 30, 2025, 6:51 AM +0100) 4e9e4fa1 bd9d5d9b

+322 -141
+19 -7
.github/workflows/release.yml
··· 19 19 - platform: 'macos-latest' # for Intel based macs. 20 20 target: 'x86_64-apple-darwin' 21 21 args: '--target x86_64-apple-darwin' 22 - - platform: 'ubuntu-22.04' # for Tauri v1 you could replace this with ubuntu-20.04. 22 + - platform: 'ubuntu-24.04' # for Tauri v1 you could replace this with ubuntu-20.04. 23 23 target: '' 24 24 args: '' 25 25 - platform: 'windows-latest' ··· 41 41 targets: ${{ matrix.target }} 42 42 43 43 - name: Install dependencies (ubuntu only) 44 - if: matrix.platform == 'ubuntu-22.04' # This must match the platform value defined above. 44 + if: matrix.platform == 'ubuntu-24.04' 45 + shell: bash 45 46 run: | 46 - sudo apt-get update 47 - sudo apt-get install -y libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf 48 - # webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2. 49 - # You can remove the one that doesn't apply to your app to speed up the workflow a bit. 47 + sudo apt update; 48 + sudo apt install -y \ 49 + build-essential \ 50 + libgtk-3-dev \ 51 + libssl-dev \ 52 + libappindicator3-dev \ 53 + librsvg2-dev patchelf; 54 + sudo apt install -y \ 55 + libwebkit2gtk-4.1-0=2.44.0-2 \ 56 + libwebkit2gtk-4.1-dev=2.44.0-2 \ 57 + libjavascriptcoregtk-4.1-0=2.44.0-2 \ 58 + libjavascriptcoregtk-4.1-dev=2.44.0-2 \ 59 + gir1.2-javascriptcoregtk-4.1=2.44.0-2 \ 60 + gir1.2-webkit2-4.1=2.44.0-2; 50 61 51 - - run: npm install 62 + - name: Install dependencies 63 + run: npm install 52 64 53 65 - name: Build and release 54 66 uses: tauri-apps/tauri-action@v0
+24 -11
.github/workflows/test.yml
··· 18 18 - platform: 'macos-latest' # for Intel based macs. 19 19 target: 'x86_64-apple-darwin' 20 20 args: '--target x86_64-apple-darwin' 21 - - platform: 'ubuntu-latest' 21 + - platform: 'ubuntu-24.04' 22 22 target: '' 23 23 args: '' 24 24 - platform: 'windows-latest' ··· 40 40 targets: ${{ matrix.target }} 41 41 42 42 - name: Install dependencies (ubuntu only) 43 - # This must match the platform value defined above. 44 - if: matrix.platform == 'ubuntu-latest' 43 + if: matrix.platform == 'ubuntu-24.04' 44 + shell: bash 45 45 run: | 46 - sudo apt-get update 47 - sudo apt-get install -y libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf 48 - # webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2. 49 - # You can remove the one that doesn't apply to your app to speed up the workflow a bit. 46 + sudo apt update; 47 + sudo apt install -y \ 48 + build-essential \ 49 + libgtk-3-dev \ 50 + libssl-dev \ 51 + libappindicator3-dev \ 52 + librsvg2-dev patchelf; 53 + sudo apt install -y \ 54 + libwebkit2gtk-4.1-0=2.44.0-2 \ 55 + libwebkit2gtk-4.1-dev=2.44.0-2 \ 56 + libjavascriptcoregtk-4.1-0=2.44.0-2 \ 57 + libjavascriptcoregtk-4.1-dev=2.44.0-2 \ 58 + gir1.2-javascriptcoregtk-4.1=2.44.0-2 \ 59 + gir1.2-webkit2-4.1=2.44.0-2; 50 60 51 - - run: npm install 61 + - name: Install dependencies 62 + run: npm install 52 63 53 - - run: npm run build:web 64 + - name: Build web 65 + run: npm run build:web 54 66 55 - - run: npm run lint 56 - if: matrix.platform == 'ubuntu-latest' 67 + - name: Lint 68 + run: npm run lint 69 + if: matrix.platform == 'ubuntu-24.04' 57 70 58 71 - name: Build 59 72 uses: tauri-apps/tauri-action@v0
+155 -45
bindings.ts
··· 1 - /* eslint-disable */ 1 + 2 2 // This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually. 3 3 4 - declare global { 5 - interface Window { 6 - __TAURI_INVOKE__<T>(cmd: string, args?: Record<string, unknown>): Promise<T>; 7 - } 8 - } 4 + /** user-defined commands **/ 9 5 10 - // Function avoids 'window not defined' in SSR 11 - const invoke = () => window.__TAURI_INVOKE__; 12 6 13 - export function errorPopup(msg: string) { 14 - return invoke()<null>("error_popup", { msg }) 7 + export const commands = { 8 + async errorPopup(msg: string) : Promise<void> { 9 + await TAURI_INVOKE("error_popup", { msg }); 10 + }, 11 + async getSettings() : Promise<Result<Settings, string>> { 12 + try { 13 + return { status: "ok", data: await TAURI_INVOKE("get_settings") }; 14 + } catch (e) { 15 + if(e instanceof Error) throw e; 16 + else return { status: "error", error: e as any }; 15 17 } 16 - 17 - export function getSettings() { 18 - return invoke()<Settings>("get_settings") 18 + }, 19 + async tags() : Promise<Result<string[], string>> { 20 + try { 21 + return { status: "ok", data: await TAURI_INVOKE("tags") }; 22 + } catch (e) { 23 + if(e instanceof Error) throw e; 24 + else return { status: "error", error: e as any }; 19 25 } 20 - 21 - export function tags() { 22 - return invoke()<string[]>("tags") 26 + }, 27 + async setChannels(channels: Channel[]) : Promise<Result<null, string>> { 28 + try { 29 + return { status: "ok", data: await TAURI_INVOKE("set_channels", { channels }) }; 30 + } catch (e) { 31 + if(e instanceof Error) throw e; 32 + else return { status: "error", error: e as any }; 23 33 } 24 - 25 - export function setChannels(channels: Channel[]) { 26 - return invoke()<null>("set_channels", { channels }) 34 + }, 35 + async addChannel(options: AddChannelOptions) : Promise<Result<null, string>> { 36 + try { 37 + return { status: "ok", data: await TAURI_INVOKE("add_channel", { options }) }; 38 + } catch (e) { 39 + if(e instanceof Error) throw e; 40 + else return { status: "error", error: e as any }; 27 41 } 28 - 29 - export function addChannel(options: AddChannelOptions) { 30 - return invoke()<null>("add_channel", { options }) 42 + }, 43 + async setGeneralSettings(apiKey: string, maxConcurrentRequests: number, checkInBackground: boolean) : Promise<Result<null, string>> { 44 + try { 45 + return { status: "ok", data: await TAURI_INVOKE("set_general_settings", { apiKey, maxConcurrentRequests, checkInBackground }) }; 46 + } catch (e) { 47 + if(e instanceof Error) throw e; 48 + else return { status: "error", error: e as any }; 31 49 } 32 - 33 - export function setGeneralSettings(apiKey: string, maxConcurrentRequests: number, checkInBackground: boolean) { 34 - return invoke()<null>("set_general_settings", { apiKey,maxConcurrentRequests,checkInBackground }) 50 + }, 51 + async checkNow() : Promise<Result<null, string>> { 52 + try { 53 + return { status: "ok", data: await TAURI_INVOKE("check_now") }; 54 + } catch (e) { 55 + if(e instanceof Error) throw e; 56 + else return { status: "error", error: e as any }; 35 57 } 36 - 37 - export function checkNow() { 38 - return invoke()<null>("check_now") 58 + }, 59 + async getHistory() : Promise<Result<UndoHistory, string>> { 60 + try { 61 + return { status: "ok", data: await TAURI_INVOKE("get_history") }; 62 + } catch (e) { 63 + if(e instanceof Error) throw e; 64 + else return { status: "error", error: e as any }; 39 65 } 40 - 41 - export function getHistory() { 42 - return invoke()<UndoHistory>("get_history") 66 + }, 67 + async getVideos(options: Options, after: After | null) : Promise<Result<Video[], string>> { 68 + try { 69 + return { status: "ok", data: await TAURI_INVOKE("get_videos", { options, after }) }; 70 + } catch (e) { 71 + if(e instanceof Error) throw e; 72 + else return { status: "error", error: e as any }; 43 73 } 44 - 45 - export function getVideos(options: Options, after: After | null) { 46 - return invoke()<Video[]>("get_videos", { options,after }) 74 + }, 75 + async archive(id: string) : Promise<Result<null, string>> { 76 + try { 77 + return { status: "ok", data: await TAURI_INVOKE("archive", { id }) }; 78 + } catch (e) { 79 + if(e instanceof Error) throw e; 80 + else return { status: "error", error: e as any }; 81 + } 82 + }, 83 + async unarchive(id: string) : Promise<Result<null, string>> { 84 + try { 85 + return { status: "ok", data: await TAURI_INVOKE("unarchive", { id }) }; 86 + } catch (e) { 87 + if(e instanceof Error) throw e; 88 + else return { status: "error", error: e as any }; 89 + } 90 + } 47 91 } 48 92 49 - export function archive(id: string) { 50 - return invoke()<null>("archive", { id }) 51 - } 93 + /** user-defined events **/ 52 94 53 - export function unarchive(id: string) { 54 - return invoke()<null>("unarchive", { id }) 55 - } 56 95 57 - export type Settings = { api_key: string; max_concurrent_requests: number; channels: Channel[]; check_in_background: boolean } 58 - export type Options = { show_all: boolean; show_archived: boolean; channel_filter: string; tag: string | null; limit: number } 59 - export type Video = { id: string; title: string; description: string; publishTimeMs: number; durationMs: number; thumbnailStandard: boolean; thumbnailMaxres: boolean; channelId: string; channelName: string; unread: boolean; archived: boolean } 96 + 97 + /** user-defined constants **/ 98 + 99 + 100 + 101 + /** user-defined types **/ 102 + 103 + export type Action = "CheckNow" | { Archive: string } | { Unarchive: string } | { AddChannel: string } | { UpdateOrDeleteChannels: string } 104 + export type AddChannelOptions = { url: string; from_time: number; refresh_rate_ms: number; tags: string[] } 60 105 export type After = { publishTimeMs: number; id: string } 61 106 export type Channel = { id: string; name: string; icon: string; uploads_playlist_id: string; from_time: number; refresh_rate_ms: number; tags: string[] } 107 + export type Options = { show_all: boolean; show_archived: boolean; channel_filter: string; tag: string | null; limit: number } 108 + export type Settings = { api_key: string; max_concurrent_requests: number; channels: Channel[]; check_in_background: boolean } 62 109 export type UndoHistory = { entries: ([number, Action])[] } 63 - export type AddChannelOptions = { url: string; from_time: number; refresh_rate_ms: number; tags: string[] } 64 - export type Action = "CheckNow" | { Archive: string } | { Unarchive: string } | { AddChannel: string } | { UpdateOrDeleteChannels: string } 110 + export type Video = { id: string; title: string; description: string; publishTimeMs: number; 111 + /** 112 + * SQLite does not support unsigned integers 113 + */ 114 + durationMs: number; thumbnailStandard: boolean; thumbnailMaxres: boolean; channelId: string; channelName: string; unread: boolean; archived: boolean } 115 + 116 + /** tauri-specta globals **/ 117 + 118 + import { 119 + invoke as TAURI_INVOKE, 120 + Channel as TAURI_CHANNEL, 121 + } from "@tauri-apps/api/core"; 122 + import * as TAURI_API_EVENT from "@tauri-apps/api/event"; 123 + import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow"; 124 + 125 + type __EventObj__<T> = { 126 + listen: ( 127 + cb: TAURI_API_EVENT.EventCallback<T>, 128 + ) => ReturnType<typeof TAURI_API_EVENT.listen<T>>; 129 + once: ( 130 + cb: TAURI_API_EVENT.EventCallback<T>, 131 + ) => ReturnType<typeof TAURI_API_EVENT.once<T>>; 132 + emit: null extends T 133 + ? (payload?: T) => ReturnType<typeof TAURI_API_EVENT.emit> 134 + : (payload: T) => ReturnType<typeof TAURI_API_EVENT.emit>; 135 + }; 136 + 137 + export type Result<T, E> = 138 + | { status: "ok"; data: T } 139 + | { status: "error"; error: E }; 140 + 141 + function __makeEvents__<T extends Record<string, any>>( 142 + mappings: Record<keyof T, string>, 143 + ) { 144 + return new Proxy( 145 + {} as unknown as { 146 + [K in keyof T]: __EventObj__<T[K]> & { 147 + (handle: __WebviewWindow__): __EventObj__<T[K]>; 148 + }; 149 + }, 150 + { 151 + get: (_, event) => { 152 + const name = mappings[event as keyof T]; 153 + 154 + return new Proxy((() => {}) as any, { 155 + apply: (_, __, [window]: [__WebviewWindow__]) => ({ 156 + listen: (arg: any) => window.listen(name, arg), 157 + once: (arg: any) => window.once(name, arg), 158 + emit: (arg: any) => window.emit(name, arg), 159 + }), 160 + get: (_, command: keyof __EventObj__<any>) => { 161 + switch (command) { 162 + case "listen": 163 + return (arg: any) => TAURI_API_EVENT.listen(name, arg); 164 + case "once": 165 + return (arg: any) => TAURI_API_EVENT.once(name, arg); 166 + case "emit": 167 + return (arg: any) => TAURI_API_EVENT.emit(name, arg); 168 + } 169 + }, 170 + }); 171 + }, 172 + }, 173 + ); 174 + }
+11 -10
package-lock.json
··· 13 13 "@sveltejs/vite-plugin-svelte": "^3.0.1", 14 14 "@tauri-apps/api": "^2.8.0", 15 15 "@tauri-apps/cli": "^2.8.3", 16 + "@tauri-apps/plugin-opener": "^2.5.0", 16 17 "@typescript-eslint/eslint-plugin": "^6.18.1", 17 18 "@typescript-eslint/parser": "^6.18.1", 18 19 "autoprefixer": "^10.4.16", ··· 27 28 "sass": "^1.69.7", 28 29 "svelte": "^4.2.8", 29 30 "svelte-check": "^3.6.2", 30 - "tauri-specta": "^0.0.2", 31 31 "typescript": "^5.3.3", 32 32 "vite": "^5.0.11" 33 33 } ··· 1319 1319 ], 1320 1320 "engines": { 1321 1321 "node": ">= 10" 1322 + } 1323 + }, 1324 + "node_modules/@tauri-apps/plugin-opener": { 1325 + "version": "2.5.0", 1326 + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.0.tgz", 1327 + "integrity": "sha512-B0LShOYae4CZjN8leiNDbnfjSrTwoZakqKaWpfoH6nXiJwt6Rgj6RnVIffG3DoJiKsffRhMkjmBV9VeilSb4TA==", 1328 + "dev": true, 1329 + "license": "MIT OR Apache-2.0", 1330 + "dependencies": { 1331 + "@tauri-apps/api": "^2.8.0" 1322 1332 } 1323 1333 }, 1324 1334 "node_modules/@types/cookie": { ··· 4496 4506 "peer": true, 4497 4507 "engines": { 4498 4508 "node": ">= 14" 4499 - } 4500 - }, 4501 - "node_modules/tauri-specta": { 4502 - "version": "0.0.2", 4503 - "resolved": "https://registry.npmjs.org/tauri-specta/-/tauri-specta-0.0.2.tgz", 4504 - "integrity": "sha512-ZgbJ/dnqHPn7Jsux7dMx7L8zMsG05Mt6rxR+ZWuSvvfWs5t+dPdHKc/4QEO3gXdQ5h1cJDwGUaNsEFoECbYkTA==", 4505 - "dev": true, 4506 - "peerDependencies": { 4507 - "@tauri-apps/api": "^1.1.0" 4508 4509 } 4509 4510 }, 4510 4511 "node_modules/text-table": {
+1 -1
package.json
··· 19 19 "@sveltejs/vite-plugin-svelte": "^3.0.1", 20 20 "@tauri-apps/api": "^2.8.0", 21 21 "@tauri-apps/cli": "^2.8.3", 22 + "@tauri-apps/plugin-opener": "^2.5.0", 22 23 "@typescript-eslint/eslint-plugin": "^6.18.1", 23 24 "@typescript-eslint/parser": "^6.18.1", 24 25 "autoprefixer": "^10.4.16", ··· 33 34 "sass": "^1.69.7", 34 35 "svelte": "^4.2.8", 35 36 "svelte-check": "^3.6.2", 36 - "tauri-specta": "^0.0.2", 37 37 "typescript": "^5.3.3", 38 38 "vite": "^5.0.11" 39 39 },
+1
src-tauri/Cargo.lock
··· 2302 2302 "atomicwrites", 2303 2303 "chrono", 2304 2304 "cocoa", 2305 + "dirs", 2305 2306 "iso8601-duration", 2306 2307 "macos-app-nap", 2307 2308 "objc",
+2 -1
src-tauri/Cargo.toml
··· 8 8 9 9 [lib] 10 10 name = "app_lib" 11 - crate-type = ["staticlib", "cdylib", "rlib", "lib"] 11 + crate-type = ["staticlib", "cdylib", "rlib"] 12 12 13 13 [build-dependencies] 14 14 tauri-build = { version = "2", features = [] } ··· 32 32 tauri-plugin-notification = "2" 33 33 tauri-plugin-opener = "2" 34 34 tauri-plugin-dialog = "2" 35 + dirs = "6.0.0" 35 36 36 37 [target.'cfg(target_os = "macos")'.dependencies] 37 38 macos-app-nap = "0.0"
+12 -10
src-tauri/capabilities/main.json
··· 1 1 { 2 - "identifier": "migrated", 3 - "description": "permissions that were migrated from v1", 4 - "local": true, 5 - "windows": [ 6 - "main" 7 - ], 8 - "permissions": [ 9 - "core:default" 10 - ] 11 - } 2 + "$schema": "../gen/schemas/desktop-schema.json", 3 + "identifier": "main", 4 + "description": "main window permissions", 5 + "local": true, 6 + "windows": ["main"], 7 + "permissions": [ 8 + "core:default", 9 + "opener:allow-open-url", 10 + "opener:allow-default-urls", 11 + "notification:default" 12 + ] 13 + }
+9 -3
src-tauri/src/background.rs
··· 187 187 } 188 188 189 189 async fn check_channels(options: &IntervalOptions, interval_info: &IntervalInfo) { 190 - let app = options.window.app_handle(); 190 + let app = options.window.app_handle(); 191 191 let window_visible = match options.window.is_visible() { 192 192 Ok(is_visible) => is_visible, 193 193 Err(e) => { 194 194 eprintln!("{}", e); 195 - app.notification().builder().title("Failed to check channels").body(e.to_string()).show().expect("Unable to show notification"); 195 + app.notification() 196 + .builder() 197 + .title("Failed to check channels") 198 + .body(e.to_string()) 199 + .show() 200 + .expect("Unable to show notification"); 196 201 return; 197 202 } 198 203 }; ··· 205 210 Err(e) => { 206 211 let title = format!("Error checking {}", channel.name); 207 212 eprintln!("{}: {}", title, e); 208 - app.notification().builder() 213 + app.notification() 214 + .builder() 209 215 .title(title) 210 216 .body(e) 211 217 .show()
+7 -3
src-tauri/src/data.rs
··· 13 13 use std::path::{Path, PathBuf}; 14 14 use std::sync::Arc; 15 15 use std::time::{SystemTime, UNIX_EPOCH}; 16 - use tauri::{command, App, Config, Manager, State}; 16 + use tauri::{command, Config, Error, State}; 17 17 use tokio::sync::Mutex; 18 18 use url::Url; 19 19 ··· 24 24 pub db: String, 25 25 } 26 26 impl AppPaths { 27 - pub fn from_tauri_config(app: &App) -> Self { 27 + pub fn from_tauri_config(config: &Config) -> Self { 28 28 let app_dir = match env::var("DEVELOPMENT").is_ok() { 29 29 true => env::current_dir().unwrap().join("appdata"), 30 - false => app.path().app_data_dir().unwrap() 30 + false => dirs::data_local_dir() 31 + .ok_or(Error::UnknownPath) 32 + .map(|dir| dir.join(&config.identifier)) 33 + .unwrap(), 31 34 }; 35 + 32 36 AppPaths { 33 37 app_dir: app_dir.clone(), 34 38 settings_file: app_dir.join("Settings.json"),
+22 -19
src-tauri/src/lib.rs
··· 61 61 } 62 62 63 63 #[cfg_attr(mobile, tauri::mobile_entry_point)] 64 - async fn run() { 64 + pub async fn run() { 65 65 let specta_builder = 66 66 tauri_specta::Builder::<tauri::Wry>::new().commands(tauri_specta::collect_commands![ 67 67 error_popup, ··· 89 89 #[cfg(target_os = "macos")] 90 90 macos_app_nap::prevent(); 91 91 92 + let app_paths = AppPaths::from_tauri_config(ctx.config()); 93 + 94 + let mut settings = match load_data(&app_paths) { 95 + Ok(v) => v, 96 + Err(e) => { 97 + error_popup_main_thread(&e); 98 + panic!("{}", e); 99 + } 100 + }; 101 + 102 + let pool = match db::init(&app_paths).await { 103 + Ok(pool) => pool, 104 + Err(e) => { 105 + error_popup_main_thread(&e); 106 + panic!("{}", e); 107 + } 108 + }; 109 + 92 110 let app = tauri::Builder::default() 93 111 .plugin(tauri_plugin_opener::init()) 94 112 .plugin(tauri_plugin_notification::init()) ··· 130 148 } 131 149 } 132 150 133 - let app_paths = AppPaths::from_tauri_config(&app); 134 - 135 - let (mut settings, _note) = match load_data(&app_paths) { 136 - Ok(v) => v, 137 - Err(e) => { 138 - error_popup_main_thread(&e); 139 - panic!("{}", e); 140 - } 141 - }; 142 - 143 - let pool = match db::init(&app_paths).await { 144 - Ok(pool) => pool, 145 - Err(e) => { 146 - error_popup_main_thread(&e); 147 - panic!("{}", e); 148 - } 149 - }; 150 - 151 151 let data = Data { 152 152 bg_handle: background::spawn_bg(settings.unwrap(), &pool, win.clone()), 153 153 db_pool: pool, ··· 162 162 if let Some(note) = _note.clone() { 163 163 dialog::message(Option::Some(&win), note.0, note.1); 164 164 } 165 + 166 + menu::manage_menu(app)?; 167 + 165 168 Ok(()) 166 169 }) 167 170 .build(ctx)
+3 -2
src-tauri/src/main.rs
··· 1 1 #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 2 2 3 - fn main() { 4 - app_lib::run(); 3 + #[tokio::main] 4 + async fn main() { 5 + app_lib::run().await; 5 6 }
+1 -1
src-tauri/src/menu.rs
··· 4 4 }; 5 5 use tauri_plugin_opener::OpenerExt; 6 6 7 - pub fn manage_tray(app: &App) -> Result<(), Box<dyn std::error::Error>> { 7 + pub fn manage_menu(app: &App) -> Result<(), Box<dyn std::error::Error>> { 8 8 #[allow(unused_mut)] 9 9 let mut menu = MenuBuilder::new(app); 10 10
+9 -4
src/lib/commands.ts
··· 1 1 // utils.ts 2 - import * as c from '../../bindings' 2 + import { commands } from '../../bindings' 3 3 4 - export default new Proxy({} as typeof c, { 4 + export default new Proxy({} as typeof commands, { 5 5 get: 6 6 (_, property: string) => 7 7 async (...args: unknown[]) => { 8 8 try { 9 9 // eslint-disable-next-line @typescript-eslint/no-explicit-any 10 - return await (c as any)[property](...args) 10 + const result = await (commands as any)[property](...args) 11 + if (result && 'status' in result && result.status === 'error') { 12 + throw new Error(result.error) 13 + } 14 + 15 + return result 11 16 } catch (e) { 12 - c.errorPopup(String(e)) 17 + commands.errorPopup(String(e)) 13 18 throw e 14 19 } 15 20 },
+9 -2
src/lib/data.ts
··· 24 24 export const settings: Writable<null | Settings> = writable(null) 25 25 export const tags: Writable<string[]> = writable([]) 26 26 export async function loadSettings() { 27 - settings.set(await commands.getSettings()) 28 - tags.set(await commands.tags()) 27 + const settingsResult = await commands.getSettings() 28 + if (settingsResult.status === 'ok') { 29 + settings.set(settingsResult.data) 30 + } 31 + 32 + const tagsResult = await commands.tags() 33 + if (tagsResult.status === 'ok') { 34 + tags.set(tagsResult.data) 35 + } 29 36 } 30 37 31 38 export function enableSampleData() {
+25 -14
src/routes/+page.svelte
··· 1 1 <script lang="ts"> 2 2 import { type ViewOptions, viewOptions } from '$lib/data' 3 + import { openUrl } from '@tauri-apps/plugin-opener' 3 4 import { event } from '@tauri-apps/api' 4 5 import { listen } from '@tauri-apps/api/event' 5 6 import { onDestroy, tick } from 'svelte' ··· 19 20 const oldselectedIndex = selectedIndex 20 21 21 22 const newVideos = await commands.getVideos(options, null) 22 - allLoaded = newVideos.length < $viewOptions.limit 23 - videos = newVideos 23 + if (newVideos.status === 'ok') { 24 + allLoaded = newVideos.data.length < $viewOptions.limit 25 + videos = newVideos.data 26 + } 24 27 25 28 // Update the selection index if the video moves 26 29 const newSelectedIndex = videos.findIndex((v) => v.id === selectedId) ··· 49 52 50 53 if (startIndex === 0) { 51 54 const newVideos = await commands.getVideos($viewOptions, null) 52 - allLoaded = newVideos.length < $viewOptions.limit 53 - videos = newVideos 55 + if (newVideos.status === 'ok') { 56 + allLoaded = newVideos.data.length < $viewOptions.limit 57 + videos = newVideos.data 58 + } 54 59 } else { 55 60 const prevVideo = videos[startIndex - 1] 56 61 const prevVideos = videos.slice(0, startIndex) ··· 65 70 id: prevVideo.id, 66 71 }, 67 72 ) 68 - videos = prevVideos.concat(reloadedVideos) 69 - // Shorten length to a mulpitle of `limit` 70 - if (videos.length > $viewOptions.limit) { 71 - videos = videos.slice(0, maxLength - (maxLength % $viewOptions.limit)) 73 + 74 + if (reloadedVideos.status === 'ok') { 75 + videos = prevVideos.concat(reloadedVideos.data) 76 + // Shorten length to a mulpitle of `limit` 77 + if (videos.length > $viewOptions.limit) { 78 + videos = videos.slice(0, maxLength - (maxLength % $viewOptions.limit)) 79 + } 80 + videos = videos.slice(0, maxLength) 72 81 } 73 - videos = videos.slice(0, maxLength) 74 82 } 75 83 76 84 await tick() ··· 84 92 publishTimeMs: videos[videos.length - 1].publishTimeMs, 85 93 id: videos[videos.length - 1].id, 86 94 }) 87 - allLoaded = newVideos.length < $viewOptions.limit 88 - videos = videos.concat(newVideos) 95 + 96 + if (newVideos.status === 'ok') { 97 + allLoaded = newVideos.data.length < $viewOptions.limit 98 + videos = videos.concat(newVideos.data) 99 + } 89 100 90 101 await tick() 91 102 await autoloadHandler() ··· 162 173 } 163 174 164 175 function openVideo(index: number) { 165 - shell.open('https://youtube.com/watch?v=' + videos[index].id) 176 + openUrl('https://youtube.com/watch?v=' + videos[index].id) 166 177 } 167 178 function openChannel(index: number) { 168 - shell.open('https://www.youtube.com/channel/' + videos[index].channelId) 179 + openUrl('https://www.youtube.com/channel/' + videos[index].channelId) 169 180 } 170 181 function getColumnCount() { 171 182 const gridStyle = window.getComputedStyle(grid) ··· 312 323 class:selected={selectionVisible && i === selectedIndex} 313 324 bind:this={boxes[i]} 314 325 on:mousedown={() => select(i)} 315 - on:dblclick={() => shell.open('https://youtube.com/watch?v=' + video.id)} 326 + on:dblclick={() => openUrl('https://youtube.com/watch?v=' + video.id)} 316 327 on:click={(e) => videoClick(e, i)} 317 328 draggable="true" 318 329 on:dragstart={(e) => dragStartVideo(e, video)}
+12 -8
src/routes/history/+page.svelte
··· 1 1 <script lang="ts"> 2 - import { getHistory } from '../../../bindings' 2 + import { openUrl } from '@tauri-apps/plugin-opener' 3 + import { commands } from '../../../bindings' 3 4 import Link from '$lib/Link.svelte' 4 - import { } from '@tauri-apps/api' 5 - import * as shell from "@tauri-apps/plugin-shell" 6 5 7 - const history = getHistory() 6 + const history = commands.getHistory().then((result) => { 7 + if (result.status == 'ok') { 8 + return result.data 9 + } else { 10 + throw new Error(`Failed to fetch history: ${result.error}`) 11 + } 12 + }) 8 13 </script> 9 14 10 15 <main> ··· 28 33 {:else if 'Archive' in action} 29 34 {@const id = action.Archive} 30 35 Archive video ID <Link 31 - on:click={() => shell.open(`https://www.youtube.com/watch?v=${id}`)} 36 + on:click={() => openUrl(`https://www.youtube.com/watch?v=${id}`)} 32 37 >{action.Archive}</Link 33 38 > 34 39 {:else if 'Unarchive' in action} 35 40 {@const id = action.Unarchive} 36 41 Unarchive video ID <Link 37 - on:click={() => shell.open(`https://www.youtube.com/watch?v=${id}`)} 42 + on:click={() => openUrl(`https://www.youtube.com/watch?v=${id}`)} 38 43 >{action.Unarchive}</Link 39 44 > 40 45 {:else if 'AddChannel' in action} 41 46 {@const id = action.AddChannel} 42 - Added channel <Link 43 - on:click={() => shell.open(`https://www.youtube.com/channel/${id}`)} 47 + Added channel <Link on:click={() => openUrl(`https://www.youtube.com/channel/${id}`)} 44 48 >{action.AddChannel}</Link 45 49 > 46 50 {:else if 'UpdateOrDeleteChannels' in action}