brows3r_lib/commands/bookmarks_cmd.rs
1//! Tauri commands for bookmarks and recent locations.
2//!
3//! # Commands
4//!
5//! - [`bookmarks_list`] — list all bookmarks for the caller.
6//! - [`bookmark_add`] — add a new bookmark; returns the created record.
7//! - [`bookmark_remove`] — remove a bookmark by id.
8//! - [`bookmark_update`] — rename a bookmark.
9//! - [`recents_list`] — list recent locations (newest first).
10//! - [`recent_track`] — record a navigation (called after every pane move).
11//! - [`recents_clear`] — clear all recent locations.
12//!
13//! # Validation gate
14//!
15//! `bookmarks_list` and `recents_list` return the full unfiltered list. The
16//! **frontend** validation gate (per round-1 finding #9) is applied in the
17//! `<Bookmarks>` and `<Recents>` React components via `useValidatedProfile`.
18//! This keeps the data layer transport-agnostic and lets the UI render a
19//! disabled state for unvalidated profiles without an extra round-trip.
20//!
21//! # OCP
22//!
23//! Adding a new bookmark field = one new arm in `BookmarkPatch` + one line in
24//! `bookmark_update`. No existing commands change.
25
26use tauri::State;
27
28use crate::{
29 bookmarks::{BookmarkInput, BookmarkPatch, BookmarkStoreHandle, RecentsHandle},
30 error::AppError,
31 ids::{BucketId, ProfileId},
32};
33
34// ---------------------------------------------------------------------------
35// bookmarks_list
36// ---------------------------------------------------------------------------
37
38/// Return all persisted bookmarks in insertion order.
39#[tauri::command]
40pub async fn bookmarks_list(
41 store: State<'_, BookmarkStoreHandle>,
42) -> Result<Vec<crate::bookmarks::Bookmark>, AppError> {
43 let guard = store.read().await;
44 Ok(guard.store.list())
45}
46
47// ---------------------------------------------------------------------------
48// bookmark_add
49// ---------------------------------------------------------------------------
50
51/// Add a new bookmark. Returns the created record.
52#[tauri::command]
53pub async fn bookmark_add(
54 profile_id: ProfileId,
55 bucket: BucketId,
56 prefix: String,
57 label: Option<String>,
58 store: State<'_, BookmarkStoreHandle>,
59) -> Result<crate::bookmarks::Bookmark, AppError> {
60 let mut guard = store.write().await;
61 let path = guard.path.clone();
62 guard.store.add(
63 BookmarkInput {
64 profile_id,
65 bucket,
66 prefix,
67 label,
68 },
69 &path,
70 )
71}
72
73// ---------------------------------------------------------------------------
74// bookmark_remove
75// ---------------------------------------------------------------------------
76
77/// Remove a bookmark by `id`.
78///
79/// Returns `Ok(())` even when the id is not found — this matches the pattern
80/// established by `search_cancel` and avoids frontend races.
81#[tauri::command]
82pub async fn bookmark_remove(
83 id: String,
84 store: State<'_, BookmarkStoreHandle>,
85) -> Result<(), AppError> {
86 let mut guard = store.write().await;
87 let path = guard.path.clone();
88 guard.store.remove(&id, &path)?;
89 Ok(())
90}
91
92// ---------------------------------------------------------------------------
93// bookmark_update
94// ---------------------------------------------------------------------------
95
96/// Update mutable fields of a bookmark.
97///
98/// Returns `NotFound` when `id` does not match any stored bookmark.
99#[tauri::command]
100pub async fn bookmark_update(
101 id: String,
102 patch: BookmarkPatch,
103 store: State<'_, BookmarkStoreHandle>,
104) -> Result<crate::bookmarks::Bookmark, AppError> {
105 let mut guard = store.write().await;
106 let path = guard.path.clone();
107 guard.store.update(&id, patch, &path)
108}
109
110// ---------------------------------------------------------------------------
111// recents_list
112// ---------------------------------------------------------------------------
113
114/// Return recent locations, newest first.
115#[tauri::command]
116pub async fn recents_list(
117 handle: State<'_, RecentsHandle>,
118) -> Result<Vec<crate::bookmarks::RecentLocation>, AppError> {
119 let guard = handle.read().await;
120 Ok(guard.list())
121}
122
123// ---------------------------------------------------------------------------
124// recent_track
125// ---------------------------------------------------------------------------
126
127/// Record a navigation. Called after every pane location change.
128///
129/// Always returns `Ok(())` — tracking failures must never surface to the user.
130#[tauri::command]
131pub async fn recent_track(
132 profile_id: ProfileId,
133 bucket: BucketId,
134 prefix: String,
135 handle: State<'_, RecentsHandle>,
136) -> Result<(), AppError> {
137 let mut guard = handle.write().await;
138 guard.track(profile_id, bucket, prefix);
139 Ok(())
140}
141
142// ---------------------------------------------------------------------------
143// recents_clear
144// ---------------------------------------------------------------------------
145
146/// Clear all recent locations and flush the empty list to disk.
147///
148/// Propagates the flush failure so the frontend's `clearMutation.onError`
149/// can surface it. Previously the flush was swallowed and a disk error
150/// would silently leave the on-disk list intact — the next launch would
151/// reload the "cleared" entries.
152#[tauri::command]
153pub async fn recents_clear(handle: State<'_, RecentsHandle>) -> Result<(), AppError> {
154 let mut guard = handle.write().await;
155 guard.clear();
156 guard.flush()
157}