mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-23 09:33:01 +03:00
add button to player frame filename
This commit is contained in:
parent
06a65f862f
commit
0fb7aaede4
@ -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
|
||||
|
@ -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>> {
|
||||
|
@ -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
|
||||
",
|
||||
)?)
|
||||
}
|
||||
|
@ -42,13 +42,6 @@ impl Database {
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
log::info!(
|
||||
"db: inserted {} deltas for file {} for session {}",
|
||||
deltas.len(),
|
||||
file_path,
|
||||
session_id
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -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(())
|
||||
}
|
||||
|
||||
|
@ -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!"))]
|
||||
|
@ -33,12 +33,6 @@ impl Database {
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
log::info!(
|
||||
"db: inserted {} sessions for project {}",
|
||||
sessions.len(),
|
||||
project_id
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -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(())
|
||||
}
|
||||
}
|
@ -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)) {
|
||||
|
@ -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![])
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
|
@ -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[]>>;
|
||||
};
|
||||
|
@ -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}
|
||||
|
||||
|
@ -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>
|
Loading…
Reference in New Issue
Block a user