From 0fb7aaede49d4333864ea727d048bc4b1f7bd91e Mon Sep 17 00:00:00 2001 From: Nikita Galaiko Date: Mon, 22 May 2023 15:28:47 +0200 Subject: [PATCH] add button to player frame filename --- src-tauri/Cargo.toml | 2 +- src-tauri/src/app.rs | 14 +- src-tauri/src/bookmarks/database.rs | 139 +++++++++++++++++- src-tauri/src/deltas/database.rs | 7 - src-tauri/src/files/database.rs | 1 - src-tauri/src/main.rs | 6 +- src-tauri/src/sessions/database.rs | 6 - src-tauri/src/watcher/dispatchers/database.rs | 82 ----------- src-tauri/src/watcher/dispatchers/mod.rs | 32 +--- .../src/watcher/handlers/index_handler.rs | 11 +- src-tauri/src/watcher/handlers/mod.rs | 7 +- src-tauri/src/watcher/watcher.rs | 3 - src/lib/stores/bookmarks.ts | 16 +- .../player/[date]/[sessionId]/+page.svelte | 16 +- .../player/[date]/[sessionId]/Filename.svelte | 51 +++++++ 15 files changed, 229 insertions(+), 164 deletions(-) delete mode 100644 src-tauri/src/watcher/dispatchers/database.rs create mode 100644 src/routes/projects/[projectId]/player/[date]/[sessionId]/Filename.svelte diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 4f4feaf8c..f60344ae4 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -53,9 +53,9 @@ sentry-debug-images = "0.31.0" zip = "0.6.5" rusqlite = { version = "0.28.0", features = [ "bundled", "blob", "hooks" ] } refinery = { version = "0.8", features = [ "rusqlite" ] } -r2d2 = "0.8.10" r2d2_sqlite = { version = "0.21.0", features = ["bundled"] } sha1 = "0.10.5" +r2d2 = "0.8.10" [features] # by default Tauri runs in production mode diff --git a/src-tauri/src/app.rs b/src-tauri/src/app.rs index 27b7e5540..4f8da98f1 100644 --- a/src-tauri/src/app.rs +++ b/src-tauri/src/app.rs @@ -302,7 +302,7 @@ impl App { self.files_database.list_by_project_id_session_id(project_id, session_id, paths) } - pub fn upsert_bookmark(&self, bookmark: &bookmarks::Bookmark) -> Result<()> { + pub fn upsert_bookmark(&self, bookmark: &bookmarks::Bookmark) -> Result> { let gb_repository = gb_repository::Repository::open( self.local_data_dir.clone(), bookmark.project_id.to_string(), @@ -311,17 +311,19 @@ impl App { ) .context("failed to open repository")?; + let session = gb_repository.get_or_create_current_session().context("failed to get or create current session")?; let writer = sessions::Writer::open(&gb_repository, &session).context("failed to open session writer")?; writer.write_bookmark(&bookmark).context("failed to write bookmark")?; + let updated = self.bookmarks_database.upsert(bookmark).context("failed to upsert bookmark")?; - self.proxy_watchers.lock().unwrap().get(&bookmark.project_id).map(|proxy_watcher| { - if let Err(e) = proxy_watcher.send(watcher::Event::Bookmark(bookmark.clone())) { - log::error!("failed to send bookmark event to proxy: {}", e); + if let Some(updated) = updated.as_ref() { + if let Err(e) = self.proxy_watchers.lock().unwrap().get(&bookmark.project_id).unwrap().send(watcher::Event::Bookmark(updated.clone())) { + log::error!("failed to send session event: {:#}", e); } - }); + } - Ok(()) + Ok(updated) } pub fn list_bookmarks(&self, project_id: &str, range: Option>) -> Result> { diff --git a/src-tauri/src/bookmarks/database.rs b/src-tauri/src/bookmarks/database.rs index 0d5683bc1..0ac206775 100644 --- a/src-tauri/src/bookmarks/database.rs +++ b/src-tauri/src/bookmarks/database.rs @@ -1,6 +1,7 @@ use std::ops; use anyhow::{Context, Result}; +use rusqlite::hooks; use crate::database; @@ -16,8 +17,63 @@ impl Database { Self { database } } - pub fn upsert(&self, bookmark: &Bookmark) -> Result<()> { - self.database.transaction(|tx| -> Result<()> { + fn get_by_project_id_timestamp_ms( + &self, + project_id: &str, + timestamp_ms: &u128, + ) -> Result> { + self.database.transaction(|tx| { + let mut stmt = get_by_project_id_timestamp_ms_stmt(tx) + .context("Failed to prepare get_by_project_id_timestamp_ms statement")?; + let mut rows = stmt + .query(rusqlite::named_params! { + ":project_id": project_id, + ":timestamp_ms": timestamp_ms.to_string(), + }) + .context("Failed to execute get_by_project_id_timestamp_ms statement")?; + if let Some(row) = rows.next()? { + let bookmark = parse_row(row)?; + Ok(Some(bookmark)) + } else { + Ok(None) + } + }) + } + + pub fn upsert(&self, bookmark: &Bookmark) -> Result> { + let existing = self + .get_by_project_id_timestamp_ms(&bookmark.project_id, &bookmark.timestamp_ms) + .context("Failed to get bookmark")?; + if let Some(existing) = existing { + if existing.note == bookmark.note && existing.deleted == bookmark.deleted { + return Ok(None); + } + self.update(bookmark).context("Failed to update bookmark")?; + Ok(Some(bookmark.clone())) + } else { + self.insert(bookmark).context("Failed to insert bookmark")?; + Ok(Some(bookmark.clone())) + } + } + + fn update(&self, bookmark: &Bookmark) -> Result<()> { + self.database.transaction(|tx| { + let mut stmt = update_bookmark_by_project_id_timestamp_ms_stmt(tx) + .context("Failed to prepare update statement")?; + stmt.execute(rusqlite::named_params! { + ":project_id": &bookmark.project_id, + ":timestamp_ms": &bookmark.timestamp_ms.to_string(), + ":updated_timestamp_ms": &bookmark.updated_timestamp_ms.to_string(), + ":note": &bookmark.note, + ":deleted": &bookmark.deleted, + }) + .context("Failed to execute update statement")?; + Ok(()) + }) + } + + fn insert(&self, bookmark: &Bookmark) -> Result<()> { + self.database.transaction(|tx| { let mut stmt = insert_stmt(tx).context("Failed to prepare insert statement")?; stmt.execute(rusqlite::named_params! { ":project_id": &bookmark.project_id, @@ -81,6 +137,81 @@ impl Database { self.list_by_project_id_all(project_id) } } + + pub fn on(&self, callback: F) -> Result<()> + where + F: Fn(&Bookmark) + Send + 'static, + { + let boxed_database = Box::new(self.database.clone()); + self.database.on_update( + move |action, _database_name, table_name, rowid| match action { + hooks::Action::SQLITE_INSERT | hooks::Action::SQLITE_UPDATE => match table_name { + "bookmarks" => { + if let Err(err) = boxed_database.transaction(|tx| -> Result<()> { + let mut stmt = get_by_rowid_stmt(tx) + .context("Failed to prepare get_by_rowid statement")?; + let mut rows = stmt + .query(rusqlite::named_params! { + ":rowid": rowid, + }) + .context("Failed to execute get_by_rowid statement")?; + + if let Some(row) = rows.next()? { + let bookmark = parse_row(row)?; + callback(&bookmark); + } + + Ok(()) + }) { + log::error!("db: failed to get bookmark by rowid: {}", err); + } + } + _ => {} + }, + _ => {} + }, + ) + } +} + +fn get_by_rowid_stmt<'conn>( + tx: &'conn rusqlite::Transaction, +) -> Result> { + Ok(tx.prepare_cached( + " + SELECT `project_id`, `created_timestamp_ms`, `updated_timestamp_ms`, `note`, `deleted`, `timestamp_ms` + FROM `bookmarks` + WHERE `rowid` = :rowid + ", + )?) +} + +fn get_by_project_id_timestamp_ms_stmt<'conn>( + tx: &'conn rusqlite::Transaction, +) -> Result> { + Ok(tx.prepare_cached( + " + SELECT `project_id`, `created_timestamp_ms`, `updated_timestamp_ms`, `note`, `deleted`, `timestamp_ms` + FROM `bookmarks` + WHERE `project_id` = :project_id + AND `timestamp_ms` = :timestamp_ms + ", + )?) +} + +fn update_bookmark_by_project_id_timestamp_ms_stmt<'conn>( + tx: &'conn rusqlite::Transaction, +) -> Result> { + Ok(tx.prepare_cached( + " + UPDATE `bookmarks` + SET `updated_timestamp_ms` = :updated_timestamp_ms, + `note` = :note, + `deleted` = :deleted + WHERE `project_id` = :project_id + AND `timestamp_ms` = :timestamp_ms + ", + )?) } fn insert_stmt<'conn>( @@ -90,10 +221,6 @@ fn insert_stmt<'conn>( " INSERT INTO `bookmarks` (`project_id`, `created_timestamp_ms`, `updated_timestamp_ms`, `timestamp_ms`, `note`, `deleted`) VALUES (:project_id, :created_timestamp_ms, :updated_timestamp_ms, :timestamp_ms, :note, :deleted) - ON CONFLICT(`project_id`, `timestamp_ms`) DO UPDATE SET - `updated_timestamp_ms` = :updated_timestamp_ms, - `note` = :note, - `deleted` = :deleted ", )?) } diff --git a/src-tauri/src/deltas/database.rs b/src-tauri/src/deltas/database.rs index 167ed3d20..7c2abae38 100644 --- a/src-tauri/src/deltas/database.rs +++ b/src-tauri/src/deltas/database.rs @@ -42,13 +42,6 @@ impl Database { Ok(()) })?; - log::info!( - "db: inserted {} deltas for file {} for session {}", - deltas.len(), - file_path, - session_id - ); - Ok(()) } diff --git a/src-tauri/src/files/database.rs b/src-tauri/src/files/database.rs index f37fb98ec..b4ea5c3f3 100644 --- a/src-tauri/src/files/database.rs +++ b/src-tauri/src/files/database.rs @@ -60,7 +60,6 @@ impl Database { .context("Failed to execute insert statement")?; Ok(()) })?; - log::info!("db: inserted file {} for session {}", file_path, session_id); Ok(()) } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 9dc95f9cf..da1fe46a6 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -492,7 +492,7 @@ async fn upsert_bookmark( timestamp_ms: u64, note: String, deleted: bool -) -> Result<(), Error> { +) -> Result, Error> { let app = handle.state::(); let now = time::UNIX_EPOCH.elapsed().context("failed to get time")?.as_millis(); let bookmark = bookmarks::Bookmark { @@ -503,8 +503,8 @@ async fn upsert_bookmark( note, deleted }; - app.upsert_bookmark(&bookmark).context("failed to upsert bookmark")?; - Ok(()) + let bookmark = app.upsert_bookmark(&bookmark).context("failed to upsert bookmark")?; + Ok(bookmark) } #[timed(duration(printer = "debug!"))] diff --git a/src-tauri/src/sessions/database.rs b/src-tauri/src/sessions/database.rs index 55830fb3e..7a78ebf6b 100644 --- a/src-tauri/src/sessions/database.rs +++ b/src-tauri/src/sessions/database.rs @@ -33,12 +33,6 @@ impl Database { Ok(()) })?; - log::info!( - "db: inserted {} sessions for project {}", - sessions.len(), - project_id - ); - Ok(()) } diff --git a/src-tauri/src/watcher/dispatchers/database.rs b/src-tauri/src/watcher/dispatchers/database.rs deleted file mode 100644 index 97a03caaf..000000000 --- a/src-tauri/src/watcher/dispatchers/database.rs +++ /dev/null @@ -1,82 +0,0 @@ -use anyhow::{Context, Result}; -use crossbeam_channel::{bounded, Receiver, Sender}; - -use crate::{deltas, files, sessions, watcher::events}; - -#[derive(Clone)] -pub struct Dispatcher { - project_id: String, - sessions_database: sessions::Database, - deltas_database: deltas::Database, - files_database: files::Database, - stop: (Sender<()>, Receiver<()>), -} - -impl Dispatcher { - pub fn new( - project_id: String, - sessions_database: sessions::Database, - deltas_database: deltas::Database, - files_database: files::Database, - ) -> Self { - Self { - project_id, - sessions_database, - deltas_database, - files_database, - stop: bounded(1), - } - } - - pub fn stop(&self) -> Result<()> { - self.stop.0.send(())?; - Ok(()) - } - - pub fn start(&self, rtx: crossbeam_channel::Sender) -> Result<()> { - log::info!("{}: database listener started", self.project_id); - - let project_id = self.project_id.clone(); - let boxed_rtx = Box::new(rtx.clone()); - self.sessions_database.on(move |session| { - if let Err(err) = boxed_rtx.send(events::Event::Session(session)) { - log::error!("{}: failed to send db session event: {:#}", project_id, err); - } - })?; - - let project_id = self.project_id.clone(); - let boxed_rtx = Box::new(rtx.clone()); - self.deltas_database - .on(move |session_id, file_path, delta| { - if let Err(err) = boxed_rtx.send(events::Event::Deltas(( - session_id.to_string(), - file_path.into(), - vec![delta], - ))) { - log::error!("{}: failed to send db delta event: {:#}", project_id, err); - } - })?; - - let project_id = self.project_id.clone(); - let boxed_rtx = Box::new(rtx.clone()); - self.files_database - .on(move |session_id, file_path, contents| { - if let Err(err) = boxed_rtx.send(events::Event::File(( - session_id.to_string(), - file_path.into(), - contents.to_string(), - ))) { - log::error!("{}: failed to send db file event: {:#}", project_id, err); - } - })?; - - self.stop - .1 - .recv() - .context("Failed to receive stop signal")?; - - log::info!("{}: database listener stopped", self.project_id); - - Ok(()) - } -} diff --git a/src-tauri/src/watcher/dispatchers/mod.rs b/src-tauri/src/watcher/dispatchers/mod.rs index 01dc59462..7fc4cfe45 100644 --- a/src-tauri/src/watcher/dispatchers/mod.rs +++ b/src-tauri/src/watcher/dispatchers/mod.rs @@ -1,4 +1,3 @@ -mod database; mod file_change; mod tick; @@ -7,7 +6,7 @@ use std::{path, time}; use anyhow::Result; use crossbeam_channel::{bounded, select, unbounded, Sender}; -use crate::{deltas, files, sessions}; +use crate::{bookmarks, deltas, files, sessions}; use super::events; @@ -16,7 +15,6 @@ pub struct Dispatcher { project_id: String, tick_dispatcher: tick::Dispatcher, file_change_dispatcher: file_change::Dispatcher, - database_dispatcher: database::Dispatcher, proxy: crossbeam_channel::Receiver, stop: ( crossbeam_channel::Sender<()>, @@ -29,20 +27,11 @@ impl Dispatcher { project_id: String, path: P, proxy_chan: crossbeam_channel::Receiver, - sessions_database: sessions::Database, - deltas_database: deltas::Database, - files_database: files::Database, ) -> Self { Self { project_id: project_id.clone(), tick_dispatcher: tick::Dispatcher::new(project_id.clone()), file_change_dispatcher: file_change::Dispatcher::new(project_id.clone(), path), - database_dispatcher: database::Dispatcher::new( - project_id.clone(), - sessions_database, - deltas_database, - files_database, - ), stop: bounded(1), proxy: proxy_chan, } @@ -72,27 +61,8 @@ impl Dispatcher { } }); - let (db_tx, db_rx) = unbounded(); - let database_dispatcher = self.database_dispatcher.clone(); - let project_id = self.project_id.clone(); - tauri::async_runtime::spawn_blocking(move || { - if let Err(e) = database_dispatcher.start(db_tx) { - log::error!("{}: failed to start database listener: {:#}", project_id, e); - } - }); - loop { select! { - recv(db_rx) -> event => match event { - Ok(event) => { - if let Err(e) = sender.send(event) { - log::error!("{}: failed to proxy database event: {:#}", self.project_id, e); - } - }, - Err(e) => { - log::error!("{}: failed to receive database event: {:#}", self.project_id, e); - } - }, recv(t_rx) -> ts => match ts{ Ok(ts) => { if let Err(e) = sender.send(events::Event::Tick(ts)) { diff --git a/src-tauri/src/watcher/handlers/index_handler.rs b/src-tauri/src/watcher/handlers/index_handler.rs index c086bf02b..3c2168837 100644 --- a/src-tauri/src/watcher/handlers/index_handler.rs +++ b/src-tauri/src/watcher/handlers/index_handler.rs @@ -1,6 +1,6 @@ use anyhow::{Context, Result}; -use crate::{bookmarks, deltas, files, gb_repository, search, sessions}; +use crate::{bookmarks, deltas, events as app_events, files, gb_repository, search, sessions}; use super::events; @@ -12,6 +12,7 @@ pub struct Handler<'handler> { sessions_database: sessions::Database, deltas_database: deltas::Database, bookmarks_database: bookmarks::Database, + events_sender: app_events::Sender, } impl<'handler> Handler<'handler> { @@ -23,6 +24,7 @@ impl<'handler> Handler<'handler> { sessions_database: sessions::Database, deltas_database: deltas::Database, bookmarks_database: bookmarks::Database, + events_sender: app_events::Sender, ) -> Self { Self { project_id, @@ -32,6 +34,7 @@ impl<'handler> Handler<'handler> { sessions_database, deltas_database, bookmarks_database, + events_sender, } } @@ -60,7 +63,11 @@ impl<'handler> Handler<'handler> { } pub fn index_bookmark(&self, bookmark: &bookmarks::Bookmark) -> Result> { - self.bookmarks_database.upsert(&bookmark)?; + let updated = self.bookmarks_database.upsert(&bookmark)?; + if let Some(updated) = updated { + self.events_sender + .send(app_events::Event::bookmark(&self.project_id, &updated))?; + } Ok(vec![]) } diff --git a/src-tauri/src/watcher/handlers/mod.rs b/src-tauri/src/watcher/handlers/mod.rs index 0ea661303..d7fffa0e5 100644 --- a/src-tauri/src/watcher/handlers/mod.rs +++ b/src-tauri/src/watcher/handlers/mod.rs @@ -49,7 +49,7 @@ impl<'handler> Handler<'handler> { ) -> Self { Self { project_id: project_id.clone(), - events_sender, + events_sender: events_sender.clone(), file_change_handler: file_change::Handler::new(), project_file_handler: project_file_change::Handler::new( @@ -84,6 +84,7 @@ impl<'handler> Handler<'handler> { sessions_database, deltas_database, bookmarks_database, + events_sender, ), } } @@ -179,14 +180,14 @@ impl<'handler> Handler<'handler> { Ok(delta_events) } events::Event::Bookmark(bookmark) => { - let bookmarks_events = self + let bookmark_events = self .index_handler .index_bookmark(&bookmark) .context("failed to index bookmark")?; self.events_sender .send(app_events::Event::bookmark(&self.project_id, &bookmark)) .context("failed to send bookmark event")?; - Ok(bookmarks_events) + Ok(bookmark_events) } } } diff --git a/src-tauri/src/watcher/watcher.rs b/src-tauri/src/watcher/watcher.rs index 6518ba637..21123029f 100644 --- a/src-tauri/src/watcher/watcher.rs +++ b/src-tauri/src/watcher/watcher.rs @@ -32,9 +32,6 @@ impl<'watcher> Watcher<'watcher> { project.id.clone(), project.path.clone(), publisher, - sessions_database.clone(), - deltas_database.clone(), - files_database.clone(), ), handler: handlers::Handler::new( project.id.clone(), diff --git a/src/lib/stores/bookmarks.ts b/src/lib/stores/bookmarks.ts index 7cb5caa5a..b0ab9ea7a 100644 --- a/src/lib/stores/bookmarks.ts +++ b/src/lib/stores/bookmarks.ts @@ -1,16 +1,22 @@ import { writable, type Loadable } from 'svelte-loadable-store'; import { bookmarks, type Bookmark } from '$lib/api'; -import type { Readable } from 'svelte/store'; +import { get, type Readable } from 'svelte/store'; const stores: Record>> = {}; export default (params: { projectId: string }) => { if (params.projectId in stores) return stores[params.projectId]; - const { subscribe } = writable(bookmarks.list(params), (set) => - bookmarks.subscribe(params, () => bookmarks.list(params).then(set)) + const store = writable(bookmarks.list(params), (set) => + bookmarks.subscribe(params, (bookmark) => { + const oldValue = get(store); + if (oldValue.isLoading) { + bookmarks.list(params).then(set); + } else { + set(oldValue.value.filter((b) => b.timestampMs !== bookmark.timestampMs).concat(bookmark)); + } + }) ); - const store = { subscribe }; stores[params.projectId] = store; - return store; + return store as Readable>; }; diff --git a/src/routes/projects/[projectId]/player/[date]/[sessionId]/+page.svelte b/src/routes/projects/[projectId]/player/[date]/[sessionId]/+page.svelte index 3b927f55c..67b04a8cd 100644 --- a/src/routes/projects/[projectId]/player/[date]/[sessionId]/+page.svelte +++ b/src/routes/projects/[projectId]/player/[date]/[sessionId]/+page.svelte @@ -31,7 +31,8 @@ import { format } from 'date-fns'; import { onMount } from 'svelte'; import { unsubscribe } from '$lib/utils'; - import { hotkeys } from '$lib'; + import { hotkeys, stores } from '$lib'; + import Filename from './Filename.svelte'; export let data: PageData; const { currentFilepath, currentTimestamp } = data; @@ -114,6 +115,7 @@ frame.subscribe((frame) => frame?.filepath && currentFilepath.set(frame.filepath)); $: currentDelta = $frame?.deltas[$frame?.deltas.length - 1]; + $: { const timestamp = currentDelta?.timestampMs; if (timestamp) { @@ -237,13 +239,11 @@ background-color: rgba(1, 1, 1, 0.6); " > - - {collapse($frame.filepath)} - - - – - {new Date(currentDelta.timestampMs).toLocaleString('en-US')} - + {/if} diff --git a/src/routes/projects/[projectId]/player/[date]/[sessionId]/Filename.svelte b/src/routes/projects/[projectId]/player/[date]/[sessionId]/Filename.svelte new file mode 100644 index 000000000..82a15fcb6 --- /dev/null +++ b/src/routes/projects/[projectId]/player/[date]/[sessionId]/Filename.svelte @@ -0,0 +1,51 @@ + + +
+ + {collapse(filename)} + + + {#if $bookmark} + + {/if} +