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

Add basic history page

Kasper (Jan 21, 2024, 7:46 AM +0100) 25fd0093 7ce701bf

+146 -8
+1
CHANGELOG.md
··· 1 1 # Changelog 2 2 3 3 ## Next 4 + - Add basic history page, accessible via `Cmd/Ctrl+Y`. 4 5 - Fix selection staying in the same position when videos update 5 6 - Keep video selected if it moves position 6 7 - Fix settings link in Get Started popup
+6
bindings.ts
··· 38 38 return invoke()<null>("check_now") 39 39 } 40 40 41 + export function getHistory() { 42 + return invoke()<UndoHistory>("get_history") 43 + } 44 + 41 45 export function getVideos(options: Options, after: After | null) { 42 46 return invoke()<Video[]>("get_videos", { options,after }) 43 47 } ··· 55 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 } 56 60 export type After = { publishTimeMs: number; id: string } 57 61 export type Channel = { id: string; name: string; icon: string; uploads_playlist_id: string; from_time: number; refresh_rate_ms: number; tags: string[] } 62 + export type UndoHistory = { entries: ([number, Action])[] } 58 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 }
+48 -2
src-tauri/src/data.rs
··· 3 3 use crate::{api, background, throw}; 4 4 use atomicwrites::{AtomicFile, OverwriteBehavior}; 5 5 use scraper::{Html, Selector}; 6 - use serde::Deserialize; 6 + use serde::{Deserialize, Serialize}; 7 7 use specta::Type; 8 8 use sqlx::SqlitePool; 9 9 use std::collections::HashSet; 10 + use std::convert::TryInto; 10 11 use std::env; 11 12 use std::io::Write; 12 13 use std::path::{Path, PathBuf}; 13 14 use std::sync::Arc; 15 + use std::time::{SystemTime, UNIX_EPOCH}; 14 16 use tauri::{command, Config, State}; 15 17 use tokio::sync::Mutex; 16 18 use url::Url; ··· 41 43 pub versioned_settings: VersionedSettings, 42 44 pub paths: AppPaths, 43 45 pub window: tauri::Window, 46 + pub user_history: UndoHistory, 44 47 } 45 48 impl Data { 46 49 pub fn settings(&mut self) -> &mut Settings { ··· 122 125 pub async fn check_now(data: DataState<'_>) -> Result<(), String> { 123 126 let mut data = data.0.lock().await; 124 127 data.check_now()?; 128 + data.user_history.push(Action::CheckNow); 125 129 Ok(()) 126 130 } 127 131 ··· 227 231 let mut data = data.0.lock().await; 228 232 data.settings().channels = channels; 229 233 data.save_settings()?; 234 + data.user_history 235 + .push(Action::UpdateOrDeleteChannels("".to_string())); 230 236 Ok(()) 231 237 } 232 238 ··· 256 262 }; 257 263 258 264 settings.channels.push(Channel { 259 - id: channel.id, 265 + id: channel.id.clone(), 260 266 name: channel.snippet.title, 261 267 icon: channel.snippet.thumbnails.medium.url, 262 268 uploads_playlist_id: channel.contentDetails.relatedPlaylists.uploads, ··· 265 271 tags: options.tags, 266 272 }); 267 273 data.save_settings()?; 274 + data.user_history.push(Action::AddChannel(channel.id)); 268 275 Ok(()) 269 276 } 270 277 ··· 291 298 Self(Arc::new(Mutex::new(data))) 292 299 } 293 300 } 301 + 302 + #[derive(Serialize, Clone, Type)] 303 + pub struct UndoHistory { 304 + pub entries: Vec<(u32, Action)>, 305 + } 306 + 307 + impl UndoHistory { 308 + pub fn new() -> Self { 309 + Self { entries: vec![] } 310 + } 311 + pub fn push(&mut self, action: Action) { 312 + let time: u32 = SystemTime::now() 313 + .duration_since(UNIX_EPOCH) 314 + .unwrap() 315 + .as_secs() 316 + .try_into() 317 + .unwrap(); 318 + self.entries.push((time, action)); 319 + if self.entries.len() > 100 { 320 + self.entries.remove(0); 321 + } 322 + } 323 + } 324 + 325 + #[derive(Serialize, Clone, Type)] 326 + pub enum Action { 327 + CheckNow, 328 + Archive(String), 329 + Unarchive(String), 330 + AddChannel(String), 331 + UpdateOrDeleteChannels(String), 332 + } 333 + 334 + #[command] 335 + #[specta::specta] 336 + pub async fn get_history(data: DataState<'_>) -> Result<UndoHistory, String> { 337 + let data = data.0.lock().await; 338 + Ok(data.user_history.clone()) 339 + }
+9 -5
src-tauri/src/db.rs
··· 1 1 use crate::api::playlist_items; 2 - use crate::data::{AppPaths, DataState}; 2 + use crate::data::{Action, AppPaths, DataState}; 3 3 use crate::throw; 4 4 use serde::{Deserialize, Serialize}; 5 5 use specta::Type; ··· 225 225 #[command] 226 226 #[specta::specta] 227 227 pub async fn archive(id: String, data: DataState<'_>) -> Result<(), String> { 228 - let data = data.0.lock().await; 228 + let mut data = data.0.lock().await; 229 229 match set_archived(&data.db_pool, &id, true).await { 230 - Ok(()) => Ok(()), 230 + Ok(()) => (), 231 231 Err(e) => throw!("Error archiving video: {}", e), 232 232 } 233 + data.user_history.push(Action::Archive(id)); 234 + Ok(()) 233 235 } 234 236 235 237 #[command] 236 238 #[specta::specta] 237 239 pub async fn unarchive(id: String, data: DataState<'_>) -> Result<(), String> { 238 - let data = data.0.lock().await; 240 + let mut data = data.0.lock().await; 239 241 match set_archived(&data.db_pool, &id, false).await { 240 - Ok(()) => Ok(()), 242 + Ok(()) => (), 241 243 Err(e) => throw!("Error unarchiving video: {}", e), 242 244 } 245 + data.user_history.push(Action::Unarchive(id)); 246 + Ok(()) 243 247 }
+8
src-tauri/src/main.rs
··· 6 6 use crate::data::{AppPaths, ArcData, Data}; 7 7 use crate::settings::yt_email_notifier; 8 8 use crate::settings::VersionedSettings; 9 + use data::UndoHistory; 9 10 use tauri::api::{dialog, shell}; 10 11 #[cfg(target_os = "macos")] 11 12 use tauri::AboutMetadata; ··· 93 94 data::add_channel, 94 95 data::set_general_settings, 95 96 data::check_now, 97 + data::get_history, 96 98 db::get_videos, 97 99 db::archive, 98 100 db::unarchive ··· 137 139 data::add_channel, 138 140 data::set_general_settings, 139 141 data::check_now, 142 + data::get_history, 140 143 db::get_videos, 141 144 db::archive, 142 145 db::unarchive, ··· 184 187 versioned_settings: settings, 185 188 paths: app_paths, 186 189 window: win.clone(), 190 + user_history: UndoHistory::new(), 187 191 }; 188 192 app.manage(ArcData::new(data)); 189 193 ··· 267 271 .into(), 268 272 CustomMenuItem::new("Show All", "Show All") 269 273 .accelerator("Alt+CmdOrCtrl+A") 274 + .into(), 275 + MenuItem::Separator.into(), 276 + CustomMenuItem::new("History", "History") 277 + .accelerator("CmdOrCtrl+Y") 270 278 .into(), 271 279 MenuItem::Separator.into(), 272 280 MenuItem::EnterFullScreen.into(),
+1
src/lib/Link.svelte
··· 17 17 <style lang="sass"> 18 18 button 19 19 font-size: 13px 20 + font-size: inherit 20 21 margin: 0px 21 22 background-color: transparent 22 23 padding: 0px
+1 -1
src/lib/modals/Settings.svelte
··· 33 33 <p class="sub"> 34 34 Kadium has a default API key, but it's vulnerable to abuse and could run out of quota. 35 35 <Link on:click={() => (keyGuideVisible = true)}> 36 - <div>Get your own key</div> 36 + <div style="font-size: 13px;">Get your own key</div> 37 37 </Link> 38 38 </p> 39 39 <input class="textbox" type="text" bind:value={apiKey} placeholder="AIzaSyNq5Y9knL..." />
+2
src/routes/+layout.svelte
··· 53 53 goto('/', { replaceState: true }) 54 54 } else if (payload === 'Channels') { 55 55 goto('/channels', { replaceState: true }) 56 + } else if (payload === 'History') { 57 + goto('/history', { replaceState: true }) 56 58 } else if (payload === 'Preferences...' || payload === 'Options...') { 57 59 $settingsOpen = true 58 60 } else if (payload === 'Add Channel...') {
+1
src/routes/channels/+page.svelte
··· 200 200 color: hsla(231, 20%, 100%, 0.5) 201 201 .edit 202 202 padding: 10px 0px 203 + font-size: 13px 203 204 input 204 205 height: 28px 205 206 box-sizing: border-box
+69
src/routes/history/+page.svelte
··· 1 + <script lang="ts"> 2 + import { getHistory } from '../../../bindings' 3 + import Link from '$lib/Link.svelte' 4 + import { shell } from '@tauri-apps/api' 5 + 6 + const history = getHistory() 7 + </script> 8 + 9 + <main> 10 + <h1>Basic-ass history page</h1> 11 + 12 + <p class="dark">The history is lost when you close the app</p> 13 + 14 + {#await history then history} 15 + {#if history.entries.length === 0} 16 + Empty! When you do things, it will show here. 17 + {/if} 18 + <table> 19 + {#each history.entries.reverse() as [timestamp, action]} 20 + <tr> 21 + <td class="timestamp dark"> 22 + {new Date(timestamp * 1000).toLocaleString()} 23 + </td> 24 + <td> 25 + {#if action === 'CheckNow'} 26 + Manually check for videos 27 + {:else if 'Archive' in action} 28 + {@const id = action.Archive} 29 + Archive video ID <Link 30 + on:click={() => shell.open(`https://www.youtube.com/watch?v=${id}`)} 31 + >{action.Archive}</Link 32 + > 33 + {:else if 'Unarchive' in action} 34 + {@const id = action.Unarchive} 35 + Unarchive video ID <Link 36 + on:click={() => shell.open(`https://www.youtube.com/watch?v=${id}`)} 37 + >{action.Unarchive}</Link 38 + > 39 + {:else if 'AddChannel' in action} 40 + {@const id = action.AddChannel} 41 + Added channel <Link 42 + on:click={() => shell.open(`https://www.youtube.com/channel/${id}`)} 43 + >{action.AddChannel}</Link 44 + > 45 + {:else if 'UpdateOrDeleteChannels' in action} 46 + Updated or deleted channel(s) 47 + {/if} 48 + </td> 49 + </tr> 50 + {/each} 51 + </table> 52 + {#if history.entries.length >= 100} 53 + <p>The end. Only 100 entries are shown.</p> 54 + {/if} 55 + {:catch error} 56 + Error {error} 57 + {/await} 58 + </main> 59 + 60 + <style lang="sass"> 61 + main 62 + padding: 20px 63 + padding-top: 0px 64 + overflow-y: auto 65 + .dark 66 + color: hsl(210, 8%, 80%) 67 + .timestamp 68 + padding-right: 10px 69 + </style>