add button to player frame filename

This commit is contained in:
Nikita Galaiko 2023-05-22 15:28:47 +02:00
parent 06a65f862f
commit 0fb7aaede4
15 changed files with 229 additions and 164 deletions

View File

@ -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

View File

@ -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<Option<bookmarks::Bookmark>> {
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<ops::Range<u128>>) -> Result<Vec<bookmarks::Bookmark>> {

View File

@ -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<Option<Bookmark>> {
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<Option<Bookmark>> {
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<F>(&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<rusqlite::CachedStatement<'conn>> {
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<rusqlite::CachedStatement<'conn>> {
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<rusqlite::CachedStatement<'conn>> {
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
",
)?)
}

View File

@ -42,13 +42,6 @@ impl Database {
Ok(())
})?;
log::info!(
"db: inserted {} deltas for file {} for session {}",
deltas.len(),
file_path,
session_id
);
Ok(())
}

View File

@ -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(())
}

View File

@ -492,7 +492,7 @@ async fn upsert_bookmark(
timestamp_ms: u64,
note: String,
deleted: bool
) -> Result<(), Error> {
) -> Result<Option<bookmarks::Bookmark>, Error> {
let app = handle.state::<app::App>();
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!"))]

View File

@ -33,12 +33,6 @@ impl Database {
Ok(())
})?;
log::info!(
"db: inserted {} sessions for project {}",
sessions.len(),
project_id
);
Ok(())
}

View File

@ -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<events::Event>) -> 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(())
}
}

View File

@ -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<events::Event>,
stop: (
crossbeam_channel::Sender<()>,
@ -29,20 +27,11 @@ impl Dispatcher {
project_id: String,
path: P,
proxy_chan: crossbeam_channel::Receiver<events::Event>,
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)) {

View File

@ -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<Vec<events::Event>> {
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![])
}

View File

@ -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)
}
}
}

View File

@ -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(),

View File

@ -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<string, Readable<Loadable<Bookmark[]>>> = {};
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<Loadable<Bookmark[]>>;
};

View File

@ -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);
"
>
<span class="flex-auto overflow-auto font-mono text-[12px] text-zinc-300">
{collapse($frame.filepath)}
</span>
<span class="whitespace-nowrap text-zinc-500">
{new Date(currentDelta.timestampMs).toLocaleString('en-US')}
</span>
<Filename
filename={$frame.filepath}
timestampMs={currentDelta.timestampMs}
projectId={$projectId}
/>
</div>
{/if}

View File

@ -0,0 +1,51 @@
<script lang="ts">
import { api, stores } from '$lib';
import type { Bookmark } from '$lib/api';
import { IconBookmark, IconBookmarkFilled } from '$lib/icons';
import { collapse } from '$lib/paths';
import { writable } from 'svelte/store';
export let projectId: string;
export let filename: string;
export let timestampMs: number;
// TODO: this is stupid, find out why derived stores don't work
$: bookmarks = stores.bookmarks({ projectId });
const bookmark = writable<Bookmark | undefined>(undefined);
$: bookmarks?.subscribe((bookmarks) => {
if (bookmarks.isLoading) return;
bookmark.set(bookmarks.value.find((bookmark) => bookmark.timestampMs === timestampMs));
});
</script>
<div class="flex flex-auto items-center gap-3 overflow-auto">
<span class="font-mono text-[12px] text-zinc-300">
{collapse(filename)}
</span>
{#if $bookmark}
<button
on:click={() =>
$bookmark &&
api.bookmarks.upsert(
$bookmark
? {
...$bookmark,
deleted: !$bookmark.deleted
}
: {
projectId,
timestampMs,
note: '',
deleted: false
}
)}
>
{#if $bookmark.deleted}
<IconBookmark class="h-4 w-4 text-zinc-700" />
{:else}
<IconBookmarkFilled class="h-4 w-4 text-bookmark-selected" />
{/if}
</button>
{/if}
</div>