[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.

Move to JS menus to make actions work

Kasper (Aug 30, 2025, 10:37 AM +0200) 3da4c71c e22f1ff7

+248 -239
+11 -14
src-tauri/Cargo.lock
··· 1504 1504 1505 1505 [[package]] 1506 1506 name = "getopts" 1507 - version = "0.2.23" 1507 + version = "0.2.24" 1508 1508 source = "registry+https://github.com/rust-lang/crates.io-index" 1509 - checksum = "cba6ae63eb948698e300f645f87c70f76630d505f23b8907cf1e193ee85048c1" 1509 + checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" 1510 1510 dependencies = [ 1511 1511 "unicode-width", 1512 1512 ] ··· 1542 1542 "cfg-if", 1543 1543 "libc", 1544 1544 "r-efi", 1545 - "wasi 0.14.2+wasi-0.2.4", 1545 + "wasi 0.14.3+wasi-0.2.4", 1546 1546 ] 1547 1547 1548 1548 [[package]] ··· 3451 3451 3452 3452 [[package]] 3453 3453 name = "potential_utf" 3454 - version = "0.1.2" 3454 + version = "0.1.3" 3455 3455 source = "registry+https://github.com/rust-lang/crates.io-index" 3456 - checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" 3456 + checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" 3457 3457 dependencies = [ 3458 3458 "zerovec", 3459 3459 ] ··· 5928 5928 5929 5929 [[package]] 5930 5930 name = "wasi" 5931 - version = "0.14.2+wasi-0.2.4" 5931 + version = "0.14.3+wasi-0.2.4" 5932 5932 source = "registry+https://github.com/rust-lang/crates.io-index" 5933 - checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 5933 + checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95" 5934 5934 dependencies = [ 5935 - "wit-bindgen-rt", 5935 + "wit-bindgen", 5936 5936 ] 5937 5937 5938 5938 [[package]] ··· 6684 6684 ] 6685 6685 6686 6686 [[package]] 6687 - name = "wit-bindgen-rt" 6688 - version = "0.39.0" 6687 + name = "wit-bindgen" 6688 + version = "0.45.0" 6689 6689 source = "registry+https://github.com/rust-lang/crates.io-index" 6690 - checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 6691 - dependencies = [ 6692 - "bitflags 2.9.3", 6693 - ] 6690 + checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814" 6694 6691 6695 6692 [[package]] 6696 6693 name = "writeable"
-3
src-tauri/src/lib.rs
··· 13 13 mod background; 14 14 mod data; 15 15 mod db; 16 - mod menu; 17 16 mod settings; 18 17 19 18 fn error_popup_main_thread(msg: impl AsRef<str>) { ··· 155 154 user_history: UndoHistory::new(), 156 155 }; 157 156 app.manage(ArcData::new(data)); 158 - 159 - menu::manage_menu(app)?; 160 157 161 158 Ok(()) 162 159 })
-146
src-tauri/src/menu.rs
··· 1 - use tauri::{ 2 - menu::{MenuBuilder, MenuItemBuilder, PredefinedMenuItem, SubmenuBuilder}, 3 - App, Manager, 4 - }; 5 - use tauri_plugin_opener::OpenerExt; 6 - 7 - pub fn manage_menu(app: &App) -> Result<(), Box<dyn std::error::Error>> { 8 - #[allow(unused_mut)] 9 - let mut menu = MenuBuilder::new(app); 10 - 11 - #[cfg(target_os = "macos")] 12 - { 13 - let app_entry = SubmenuBuilder::new(app, &app.package_info().name.clone()) 14 - .item(&MenuItemBuilder::new("About").build(app)?) 15 - .item(&PredefinedMenuItem::separator(app)?) 16 - .item(&MenuItemBuilder::new("Preferences...").build(app)?) 17 - .item(&PredefinedMenuItem::separator(app)?) 18 - .item(&MenuItemBuilder::new("Services").build(app)?) 19 - .item(&PredefinedMenuItem::separator(app)?) 20 - .item(&MenuItemBuilder::new("Hide").build(app)?) 21 - .item(&MenuItemBuilder::new("Hide Others").build(app)?) 22 - .item(&MenuItemBuilder::new("Show All").build(app)?) 23 - .item(&PredefinedMenuItem::separator(app)?) 24 - .item(&MenuItemBuilder::new("Quit").build(app)?) 25 - .build()?; 26 - 27 - menu = menu.item(&app_entry); 28 - } 29 - 30 - #[allow(unused_mut)] 31 - let mut file = SubmenuBuilder::new(app, "File") 32 - .item( 33 - &MenuItemBuilder::new("Add Channel...") 34 - .accelerator("CmdOrCtrl+N") 35 - .build(app)?, 36 - ) 37 - .item(&MenuItemBuilder::new("Open").build(app)?) 38 - .item(&MenuItemBuilder::new("Open Channel").build(app)?) 39 - .item( 40 - &MenuItemBuilder::new("Archive") 41 - .accelerator("CmdOrCtrl+Backspace") 42 - .build(app)?, 43 - ) 44 - .item( 45 - &MenuItemBuilder::new("Unarchive") 46 - .accelerator("Shift+CmdOrCtrl+Backspace") 47 - .build(app)?, 48 - ) 49 - .item(&PredefinedMenuItem::separator(app)?) 50 - .item(&PredefinedMenuItem::close_window(app, "Close".into())?); 51 - 52 - #[cfg(not(target_os = "macos"))] 53 - { 54 - file = file 55 - .item(&MenuItemBuilder::new("Options...").build(app)?) 56 - .item(&PredefinedMenuItem::separator(app)?); 57 - } 58 - 59 - let edit = SubmenuBuilder::new(app, "Edit") 60 - .item(&PredefinedMenuItem::undo(app, "Undo".into())?) 61 - .item(&PredefinedMenuItem::redo(app, "Redo".into())?) 62 - .item(&PredefinedMenuItem::separator(app)?) 63 - .item(&PredefinedMenuItem::cut(app, "Cut".into())?) 64 - .item(&PredefinedMenuItem::copy(app, "Copy".into())?) 65 - .item(&PredefinedMenuItem::paste(app, "Paste".into())?) 66 - .item(&PredefinedMenuItem::separator(app)?) 67 - .item(&PredefinedMenuItem::select_all(app, "Select All".into())?) 68 - .item(&PredefinedMenuItem::separator(app)?) 69 - .item( 70 - &MenuItemBuilder::new("Find") 71 - .accelerator("CmdOrCtrl+F") 72 - .build(app)?, 73 - ) 74 - .build()?; 75 - 76 - let view = SubmenuBuilder::new(app, "View") 77 - .item( 78 - &MenuItemBuilder::new("Show New") 79 - .accelerator("Alt+CmdOrCtrl+N") 80 - .build(app)?, 81 - ) 82 - .item( 83 - &MenuItemBuilder::new("Show Archived") 84 - .accelerator("Alt+CmdOrCtrl+E") 85 - .build(app)?, 86 - ) 87 - .item( 88 - &MenuItemBuilder::new("Show All") 89 - .accelerator("Alt+CmdOrCtrl+A") 90 - .build(app)?, 91 - ) 92 - .item(&PredefinedMenuItem::separator(app)?) 93 - .item( 94 - &MenuItemBuilder::new("History") 95 - .accelerator("CmdOrCtrl+Y") 96 - .build(app)?, 97 - ) 98 - .item(&PredefinedMenuItem::separator(app)?) 99 - .item(&PredefinedMenuItem::fullscreen(app, "Fullscreen".into())?) 100 - .build()?; 101 - 102 - let window = SubmenuBuilder::new(app, "Window") 103 - .item(&PredefinedMenuItem::minimize(app, "Minimize".into())?) 104 - // .item(MenuItem::Zoom.into()) 105 - .item(&PredefinedMenuItem::separator(app)?) 106 - .item( 107 - &MenuItemBuilder::new("Videos") 108 - .accelerator("Alt+CmdOrCtrl+1") 109 - .build(app)?, 110 - ) 111 - .item( 112 - &MenuItemBuilder::new("Channels") 113 - .accelerator("Alt+CmdOrCtrl+2") 114 - .build(app)?, 115 - ) 116 - .build()?; 117 - 118 - let help = SubmenuBuilder::new(app, "Help") 119 - .item(&MenuItemBuilder::new("Get Started").build(app)?) 120 - .item(&MenuItemBuilder::new("Learn More").build(app)?) 121 - .build()?; 122 - 123 - let menu_built = menu 124 - .item(&file.build()?) 125 - .item(&edit) 126 - .item(&view) 127 - .item(&window) 128 - .item(&help) 129 - .build()?; 130 - 131 - app.set_menu(menu_built)?; 132 - 133 - app.app_handle() 134 - .on_menu_event(|app, event| match event.id().0.as_str() { 135 - "Learn More" => { 136 - let url = "https://github.com/probablykasper/kadium"; 137 - app.app_handle() 138 - .opener() 139 - .open_url(url, None::<&str>) 140 - .unwrap(); 141 - } 142 - _ => {} 143 - }); 144 - 145 - Ok(()) 146 - }
+12 -40
src/routes/+layout.svelte
··· 1 1 <script lang="ts" context="module"> 2 - let getStartedWasShown = false 2 + import { writable } from 'svelte/store' 3 + 4 + let get_started_was_shown = false 5 + export let show_get_started = writable(false) 3 6 </script> 4 7 5 8 <script lang="ts"> 6 - import { event } from '@tauri-apps/api' 7 9 import { checkShortcut, checkModifiers } from '$lib/general' 8 10 import SettingsModal from '$lib/modals/Settings.svelte' 9 11 import { ··· 15 17 settingsOpen, 16 18 } from '$lib/data' 17 19 import Nav from '$lib/Nav.svelte' 18 - import { onDestroy } from 'svelte' 19 20 import GetStarted from '$lib/modals/GetStarted.svelte' 20 21 import { page } from '$app/stores' 21 22 import { goto } from '$app/navigation' 23 + import { create_menu } from './menu' 22 24 23 25 let error = false 24 26 loadSettings().catch(() => { ··· 48 50 } 49 51 } 50 52 51 - const unlistenFuture = event.listen('tauri://menu', ({ payload }) => { 52 - if (payload === 'Videos') { 53 - goto('/', { replaceState: true }) 54 - } else if (payload === 'Channels') { 55 - goto('/channels', { replaceState: true }) 56 - } else if (payload === 'History') { 57 - goto('/history', { replaceState: true }) 58 - } else if (payload === 'Preferences...' || payload === 'Options...') { 59 - $settingsOpen = true 60 - } else if (payload === 'Add Channel...') { 61 - goto('/channels?add', { replaceState: true }) 62 - } else if (payload === 'Show New') { 63 - goto('/', { replaceState: true }) 64 - $viewOptions.show_all = false 65 - $viewOptions.show_archived = false 66 - } else if (payload === 'Show Archived') { 67 - goto('/', { replaceState: true }) 68 - $viewOptions.show_all = false 69 - $viewOptions.show_archived = true 70 - } else if (payload === 'Show All') { 71 - goto('/', { replaceState: true }) 72 - $viewOptions.show_all = true 73 - $viewOptions.show_archived = false 74 - } else if (payload === 'Get Started') { 75 - showGetStarted = true 76 - } 77 - }) 78 - onDestroy(async () => { 79 - const unlisten = await unlistenFuture 80 - unlisten() 81 - }) 82 - let showGetStarted = false 83 - $: if (showGetStarted) { 84 - getStartedWasShown = true 53 + create_menu() 54 + 55 + $: if ($show_get_started) { 56 + get_started_was_shown = true 85 57 } 86 58 87 - $: if (!getStartedWasShown && $settings?.channels.length === 0 && $settings.api_key === '') { 88 - showGetStarted = true 59 + $: if (!get_started_was_shown && $settings?.channels.length === 0 && $settings.api_key === '') { 60 + $show_get_started = true 89 61 } 90 62 </script> 91 63 ··· 101 73 bind:visible={$settingsOpen} 102 74 /> 103 75 104 - <GetStarted bind:visible={showGetStarted} /> 76 + <GetStarted bind:visible={$show_get_started} /> 105 77 {:else if error} 106 78 Error loading. 107 79
+18 -14
src/routes/+page.svelte
··· 8 8 import { checkModifiers, checkShortcut } from '$lib/general' 9 9 import VideoBar from './_VideoBar.svelte' 10 10 import commands from '$lib/commands' 11 + import { menu_actions } from './menu' 11 12 12 13 let videos: Video[] = [] 13 14 let allLoaded = false ··· 270 271 } 271 272 } 272 273 273 - const unlistenFuture = event.listen('tauri://menu', async ({ payload }) => { 274 - if (payload === 'Open Selected Video' && selectionVisible) { 275 - openVideo(selectedIndex) 276 - } else if (payload === 'Open Selected Channel' && selectionVisible) { 277 - openChannel(selectedIndex) 278 - } else if (payload === 'Archive' && selectionVisible) { 279 - archive(selectedIndex) 280 - } else if (payload === 'Unarchive' && selectionVisible) { 281 - unarchive(selectedIndex) 282 - } 283 - }) 284 - onDestroy(async () => { 285 - const unlisten = await unlistenFuture 286 - unlisten() 274 + menu_actions.Open = () => { 275 + if (selectionVisible) openVideo(selectedIndex) 276 + } 277 + menu_actions['Open Channel'] = () => { 278 + if (selectionVisible) openChannel(selectedIndex) 279 + } 280 + menu_actions.Archive = () => { 281 + if (selectionVisible) archive(selectedIndex) 282 + } 283 + menu_actions.Unarchive = () => { 284 + if (selectionVisible) unarchive(selectedIndex) 285 + } 286 + onDestroy(() => { 287 + menu_actions.Open = undefined 288 + menu_actions['Open Channel'] = undefined 289 + menu_actions.Archive = undefined 290 + menu_actions.Unarchive = undefined 287 291 }) 288 292 289 293 let boxes: HTMLDivElement[] = []
+4 -11
src/routes/_VideoBar.svelte
··· 1 1 <script lang="ts"> 2 - import { onDestroy } from 'svelte' 3 2 import { tags, viewOptions } from '$lib/data' 4 3 import { checkShortcut } from '$lib/general' 5 - import { event } from '@tauri-apps/api' 4 + import { menu_actions } from './menu' 6 5 7 6 export let loadedVideosCount: number 8 7 export let allLoaded: boolean ··· 49 48 } 50 49 51 50 let filterInput: HTMLInputElement 52 - const unlistenFuture = event.listen('tauri://menu', ({ payload }) => { 53 - if (payload === 'Find') { 54 - filterInput.focus() 55 - } 56 - }) 57 - onDestroy(async () => { 58 - const unlisten = await unlistenFuture 59 - unlisten() 60 - }) 51 + menu_actions.Find = () => { 52 + filterInput.focus() 53 + } 61 54 62 55 function blurEscapeKeydown(e: KeyboardEvent) { 63 56 if (checkShortcut(e, 'Escape')) {
+4 -11
src/routes/channels/+page.svelte
··· 4 4 import Tags from '$lib/Tags.svelte' 5 5 import type { Channel } from '../../../bindings' 6 6 import ChannelModal from '$lib/modals/Channel.svelte' 7 - import { event } from '@tauri-apps/api' 8 - import { onDestroy } from 'svelte' 9 7 import commands from '$lib/commands' 10 8 import { page } from '$app/stores' 11 9 import { goto } from '$app/navigation' 10 + import { menu_actions } from '../menu' 12 11 13 12 $: channels = $settings?.channels ?? [] 14 13 ··· 49 48 50 49 let filter = '' 51 50 let filterInput: HTMLInputElement 52 - const unlistenFuture = event.listen('tauri://menu', ({ payload }) => { 53 - if (payload === 'Find') { 54 - filterInput.focus() 55 - } 56 - }) 57 - onDestroy(async () => { 58 - const unlisten = await unlistenFuture 59 - unlisten() 60 - }) 51 + menu_actions.Find = () => { 52 + filterInput.focus() 53 + } 61 54 62 55 let channels_scroll_el: HTMLDivElement 63 56 </script>
+199
src/routes/menu.ts
··· 1 + import { goto } from '$app/navigation' 2 + import { settingsOpen, viewOptions } from '$lib/data' 3 + import { Menu, Submenu, type SubmenuOptions } from '@tauri-apps/api/menu' 4 + import { openUrl } from '@tauri-apps/plugin-opener' 5 + import { show_get_started } from './+layout.svelte' 6 + 7 + export const menu_actions: Partial< 8 + Record<'Find' | 'Open' | 'Open Channel' | 'Archive' | 'Unarchive', () => void> 9 + > = {} 10 + 11 + export async function create_menu() { 12 + const app_menu: SubmenuOptions = { 13 + text: 'Kadium', 14 + items: [ 15 + { 16 + item: { 17 + About: null, 18 + }, 19 + }, 20 + { item: 'Separator' }, 21 + { 22 + text: 'Preferences...', 23 + id: 'Preferences...', 24 + accelerator: 'cmdOrControl+,', 25 + action() { 26 + settingsOpen.set(true) 27 + }, 28 + }, 29 + { item: 'Separator' }, 30 + { item: 'Services' }, 31 + { item: 'Separator' }, 32 + { item: 'Hide' }, 33 + { item: 'HideOthers' }, 34 + { item: 'ShowAll' }, 35 + { item: 'Separator' }, 36 + { item: 'Quit' }, 37 + ], 38 + } 39 + const file_menu: SubmenuOptions = { 40 + text: 'File', 41 + items: [ 42 + { 43 + text: 'Add Channel...', 44 + accelerator: 'cmdOrControl+N', 45 + action() { 46 + goto('/channels?add', { replaceState: true }) 47 + }, 48 + }, 49 + { 50 + text: 'Open', 51 + action: () => menu_actions.Open?.(), 52 + }, 53 + { 54 + text: 'Open Channel', 55 + action: () => menu_actions['Open Channel']?.(), 56 + }, 57 + { 58 + text: 'Archive', 59 + accelerator: 'CmdOrCtrl+Backspace', 60 + action: () => menu_actions.Archive?.(), 61 + }, 62 + { 63 + text: 'Unarchive', 64 + accelerator: 'Shift+CmdOrCtrl+Backspace', 65 + action: () => menu_actions.Unarchive?.(), 66 + }, 67 + { item: 'Separator' }, 68 + { 69 + text: 'Options...', 70 + id: 'Preferences...', 71 + accelerator: 'cmdOrControl+,', 72 + action() { 73 + settingsOpen.set(true) 74 + }, 75 + }, 76 + { item: 'Separator' }, 77 + { item: 'CloseWindow' }, 78 + ], 79 + } 80 + const edit_menu: SubmenuOptions = { 81 + text: 'Edit', 82 + items: [ 83 + { item: 'Undo' }, 84 + { item: 'Redo' }, 85 + { item: 'Separator' }, 86 + { item: 'Cut' }, 87 + { item: 'Copy' }, 88 + { item: 'Paste' }, 89 + // #[cfg(not(target_os = "macos"))] 90 + { item: 'Separator' }, 91 + { item: 'SelectAll' }, 92 + { item: 'Separator' }, 93 + { 94 + text: 'Find', 95 + accelerator: 'CmdOrCtrl+F', 96 + action() { 97 + menu_actions.Find?.() 98 + }, 99 + }, 100 + ], 101 + } 102 + const view_menu: SubmenuOptions = { 103 + text: 'View', 104 + items: [ 105 + { 106 + text: 'Show New', 107 + accelerator: 'Alt+CmdOrCtrl+N', 108 + action() { 109 + goto('/', { replaceState: true }) 110 + viewOptions.update((v) => { 111 + v.show_all = false 112 + v.show_archived = false 113 + return v 114 + }) 115 + }, 116 + }, 117 + { 118 + text: 'Show Archived', 119 + accelerator: 'Alt+CmdOrCtrl+E', 120 + action() { 121 + goto('/', { replaceState: true }) 122 + viewOptions.update((v) => { 123 + v.show_all = false 124 + v.show_archived = true 125 + return v 126 + }) 127 + }, 128 + }, 129 + { 130 + text: 'Show All', 131 + accelerator: 'Alt+CmdOrCtrl+A', 132 + action() { 133 + goto('/', { replaceState: true }) 134 + viewOptions.update((v) => { 135 + v.show_all = true 136 + v.show_archived = false 137 + return v 138 + }) 139 + }, 140 + }, 141 + { item: 'Separator' }, 142 + { 143 + text: 'History', 144 + accelerator: 'CmdOrCtrl+Y', 145 + action() { 146 + goto('/history', { replaceState: true }) 147 + }, 148 + }, 149 + { item: 'Separator' }, 150 + { item: 'Fullscreen' }, 151 + ], 152 + } 153 + const window_menu: SubmenuOptions = { 154 + text: 'Window', 155 + items: [ 156 + // 157 + { item: 'Minimize' }, 158 + { item: 'Maximize' }, 159 + { item: 'Separator' }, 160 + { 161 + text: 'Videos', 162 + accelerator: 'Alt+CmdOrCtrl+1', 163 + action() { 164 + goto('/', { replaceState: true }) 165 + }, 166 + }, 167 + { 168 + text: 'Channels', 169 + accelerator: 'Alt+CmdOrCtrl+2', 170 + action() { 171 + goto('/channels', { replaceState: true }) 172 + }, 173 + }, 174 + ], 175 + } 176 + const help_menu = await Submenu.new({ 177 + text: 'Help', 178 + items: [ 179 + { 180 + text: 'Get Started', 181 + action() { 182 + show_get_started.set(true) 183 + }, 184 + }, 185 + { 186 + text: 'Learn More', 187 + action() { 188 + openUrl('https://github.com/probablykasper/kadium') 189 + }, 190 + }, 191 + ], 192 + }) 193 + const menu = await Menu.new({ 194 + items: [app_menu, file_menu, edit_menu, view_menu, window_menu, help_menu], 195 + }) 196 + menu.setAsAppMenu() 197 + // https://github.com/tauri-apps/tauri/issues/12652 198 + help_menu.setAsHelpMenuForNSApp() 199 + }