From 45662dc642f1d728454013b2bdbd34d3eb322001 Mon Sep 17 00:00:00 2001 From: Julian Freeman Date: Fri, 17 Apr 2026 16:27:57 -0400 Subject: [PATCH] support delete bookmarks --- src-tauri/src/commands.rs | 203 ++++++++++++++- src-tauri/src/lib.rs | 3 +- src-tauri/src/models.rs | 31 +++ src/App.vue | 38 +++ .../browser-data/AssociatedProfilesModal.vue | 25 +- .../browser-data/BookmarkRemovalModal.vue | 151 +++++++++++ src/components/browser-data/BookmarksList.vue | 176 ++++++++++++- .../browser-data/BrowserDataView.vue | 83 +++++- src/composables/useBrowserManager.ts | 237 ++++++++++++++++++ src/types/browser.ts | 23 ++ 10 files changed, 946 insertions(+), 24 deletions(-) create mode 100644 src/components/browser-data/BookmarkRemovalModal.vue diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 44501bb..37e1a1d 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -7,9 +7,11 @@ use std::{ use crate::{ config_store, models::{ - BrowserConfigListResponse, CleanupHistoryInput, CleanupHistoryResponse, - CleanupHistoryResult, CreateCustomBrowserConfigInput, ExtensionInstallSourceSummary, - RemoveExtensionResult, RemoveExtensionsInput, RemoveExtensionsResponse, ScanResponse, + BookmarkRemovalRequest, BrowserConfigListResponse, CleanupHistoryInput, + CleanupHistoryResponse, CleanupHistoryResult, CreateCustomBrowserConfigInput, + ExtensionInstallSourceSummary, RemoveBookmarkResult, RemoveBookmarksInput, + RemoveBookmarksResponse, RemoveExtensionResult, RemoveExtensionsInput, + RemoveExtensionsResponse, ScanResponse, }, scanner, }; @@ -124,6 +126,35 @@ pub fn remove_extensions( Ok(RemoveExtensionsResponse { results }) } +#[tauri::command] +pub fn remove_bookmarks( + app: AppHandle, + input: RemoveBookmarksInput, +) -> Result { + let config = config_store::find_browser_config(&app, &input.browser_id)?; + let user_data_dir = PathBuf::from(&config.user_data_path); + + if !user_data_dir.is_dir() { + return Err(format!( + "User data directory does not exist: {}", + user_data_dir.display() + )); + } + + let mut results = Vec::new(); + for removal in input.removals { + for profile_id in &removal.profile_ids { + results.push(remove_bookmark_from_profile( + &user_data_dir.join(profile_id), + &removal, + profile_id, + )); + } + } + + Ok(RemoveBookmarksResponse { results }) +} + fn spawn_browser_process( executable_path: PathBuf, user_data_dir: PathBuf, @@ -298,6 +329,106 @@ fn remove_extension_from_profile( } } +fn remove_bookmark_from_profile( + profile_path: &Path, + removal: &BookmarkRemovalRequest, + profile_id: &str, +) -> RemoveBookmarkResult { + if !profile_path.is_dir() { + return RemoveBookmarkResult { + url: removal.url.clone(), + profile_id: profile_id.to_string(), + removed_count: 0, + removed_files: Vec::new(), + skipped_files: Vec::new(), + error: Some(format!( + "Profile directory does not exist: {}", + profile_path.display() + )), + }; + } + + let mut removed_files = Vec::new(); + let mut skipped_files = Vec::new(); + + let removed_backup = remove_bookmark_backups(profile_path).map_err(|error| RemoveBookmarkResult { + url: removal.url.clone(), + profile_id: profile_id.to_string(), + removed_count: 0, + removed_files: removed_files.clone(), + skipped_files: skipped_files.clone(), + error: Some(error), + }); + let removed_backup = match removed_backup { + Ok(value) => value, + Err(result) => return result, + }; + if removed_backup { + removed_files.push("Bookmarks.bak".to_string()); + } else { + skipped_files.push("Bookmarks.bak".to_string()); + } + + let Some(bookmarks_path) = resolve_bookmarks_path(profile_path) else { + return RemoveBookmarkResult { + url: removal.url.clone(), + profile_id: profile_id.to_string(), + removed_count: 0, + removed_files, + skipped_files, + error: Some(format!( + "Bookmarks file does not exist in {}", + profile_path.display() + )), + }; + }; + + let mut document = match read_json_document(&bookmarks_path) { + Ok(document) => document, + Err(error) => { + return RemoveBookmarkResult { + url: removal.url.clone(), + profile_id: profile_id.to_string(), + removed_count: 0, + removed_files, + skipped_files, + error: Some(error), + }; + } + }; + + let checksum_removed = document + .as_object_mut() + .and_then(|object| object.remove("checksum")) + .is_some(); + let removed_count = remove_matching_bookmarks(&mut document, &removal.url); + + if checksum_removed || removed_count > 0 { + if let Err(error) = write_json_document(&bookmarks_path, &document) { + return RemoveBookmarkResult { + url: removal.url.clone(), + profile_id: profile_id.to_string(), + removed_count: 0, + removed_files, + skipped_files, + error: Some(error), + }; + } + removed_files.push("Bookmarks".to_string()); + } else { + skipped_files.push("Bookmarks".to_string()); + } + + RemoveBookmarkResult { + url: removal.url.clone(), + profile_id: profile_id.to_string(), + removed_count, + removed_files, + skipped_files, + error: None, + } +} + fn remove_extension_from_secure_preferences( path: &Path, extension_id: &str, @@ -427,3 +558,69 @@ fn cleanup_sessions_directory(path: &Path) -> Result { Ok(deleted_any) } + +fn remove_bookmark_backups(profile_path: &Path) -> Result { + let mut deleted_any = false; + for backup_name in ["Bookmarks.bak", "Bookmark.bak"] { + let backup_path = profile_path.join(backup_name); + if !backup_path.is_file() { + continue; + } + fs::remove_file(&backup_path) + .map_err(|error| format!("Failed to delete {}: {error}", backup_path.display()))?; + deleted_any = true; + } + + Ok(deleted_any) +} + +fn resolve_bookmarks_path(profile_path: &Path) -> Option { + ["Bookmarks", "Bookmark"] + .into_iter() + .map(|name| profile_path.join(name)) + .find(|path| path.is_file()) +} + +fn remove_matching_bookmarks(value: &mut Value, target_url: &str) -> usize { + match value { + Value::Object(object) => { + let mut removed_count = 0; + + if let Some(children) = object.get_mut("children").and_then(Value::as_array_mut) { + let mut index = 0; + while index < children.len() { + let matches_url = children[index] + .as_object() + .map(|child| { + child.get("type").and_then(Value::as_str) == Some("url") + && child.get("url").and_then(Value::as_str) == Some(target_url) + }) + .unwrap_or(false); + + if matches_url { + children.remove(index); + removed_count += 1; + continue; + } + + removed_count += remove_matching_bookmarks(&mut children[index], target_url); + index += 1; + } + } + + for (key, child) in object.iter_mut() { + if key == "children" { + continue; + } + removed_count += remove_matching_bookmarks(child, target_url); + } + + removed_count + } + Value::Array(array) => array + .iter_mut() + .map(|item| remove_matching_bookmarks(item, target_url)) + .sum(), + _ => 0, + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 5419114..b48b75c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -17,7 +17,8 @@ pub fn run() { commands::delete_custom_browser_config, commands::open_browser_profile, commands::cleanup_history_files, - commands::remove_extensions + commands::remove_extensions, + commands::remove_bookmarks ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs index 3995e79..396ac9f 100644 --- a/src-tauri/src/models.rs +++ b/src-tauri/src/models.rs @@ -122,6 +122,13 @@ pub struct RemoveExtensionsInput { pub removals: Vec, } +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RemoveBookmarksInput { + pub browser_id: String, + pub removals: Vec, +} + #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ExtensionRemovalRequest { @@ -129,12 +136,25 @@ pub struct ExtensionRemovalRequest { pub profile_ids: Vec, } +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BookmarkRemovalRequest { + pub url: String, + pub profile_ids: Vec, +} + #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct RemoveExtensionsResponse { pub results: Vec, } +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RemoveBookmarksResponse { + pub results: Vec, +} + #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct RemoveExtensionResult { @@ -145,6 +165,17 @@ pub struct RemoveExtensionResult { pub error: Option, } +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RemoveBookmarkResult { + pub url: String, + pub profile_id: String, + pub removed_count: usize, + pub removed_files: Vec, + pub skipped_files: Vec, + pub error: Option, +} + #[derive(Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct AssociatedProfileSummary { diff --git a/src/App.vue b/src/App.vue index 174d6f9..c513be7 100644 --- a/src/App.vue +++ b/src/App.vue @@ -8,6 +8,14 @@ const { activeSection, associatedProfilesModal, bookmarkSortKey, + bookmarkDeleteBusy, + bookmarkModalSelectedProfileIds, + bookmarkRemovalConfirmBookmarkCount, + bookmarkRemovalConfirmProfileCount, + bookmarkRemovalError, + bookmarkRemovalResultOpen, + bookmarkRemovalResults, + bookmarkSelectedUrls, browserConfigs, browserMonogram, browsers, @@ -16,6 +24,8 @@ const { configsLoading, createConfigForm, createCustomBrowserConfig, + deleteBookmarkFromAllProfiles, + deleteBookmarkFromProfile, cleanupHistoryError, cleanupHistoryForProfile, cleanupHistoryResults, @@ -26,6 +36,8 @@ const { confirmHistoryCleanup, currentBrowser, deleteCustomBrowserConfig, + deleteSelectedBookmarkProfiles, + deleteSelectedBookmarks, deleteExtensionFromAllProfiles, deleteExtensionFromProfile, deleteSelectedExtensionProfiles, @@ -58,6 +70,8 @@ const { savingConfig, sectionCount, selectedBrowserId, + closeBookmarkRemovalConfirm, + closeBookmarkRemovalResult, showBookmarkProfilesModal, showExtensionProfilesModal, showPasswordSiteProfilesModal, @@ -65,10 +79,15 @@ const { sortedExtensions, sortedPasswordSites, sortedProfiles, + confirmBookmarkRemoval, closeExtensionRemovalConfirm, closeExtensionRemovalResult, confirmExtensionRemoval, + toggleAllBookmarks, + toggleAllBookmarkModalProfiles, toggleAllExtensions, + toggleBookmarkModalProfileSelection, + toggleBookmarkSelection, toggleAllExtensionModalProfiles, toggleExtensionModalProfileSelection, toggleExtensionSelection, @@ -147,6 +166,14 @@ const { :history-cleanup-result-open="historyCleanupResultOpen" :cleanup-history-error="cleanupHistoryError" :cleanup-history-results="cleanupHistoryResults" + :bookmark-selected-urls="bookmarkSelectedUrls" + :bookmark-modal-selected-profile-ids="bookmarkModalSelectedProfileIds" + :bookmark-delete-busy="bookmarkDeleteBusy" + :bookmark-removal-confirm-bookmark-count="bookmarkRemovalConfirmBookmarkCount" + :bookmark-removal-confirm-profile-count="bookmarkRemovalConfirmProfileCount" + :bookmark-removal-result-open="bookmarkRemovalResultOpen" + :bookmark-removal-error="bookmarkRemovalError" + :bookmark-removal-results="bookmarkRemovalResults" :extension-selected-ids="extensionSelectedIds" :extension-modal-selected-profile-ids="extensionModalSelectedProfileIds" :extension-delete-busy="extensionDeleteBusy" @@ -169,6 +196,17 @@ const { @show-extension-profiles="showExtensionProfilesModal" @show-bookmark-profiles="showBookmarkProfilesModal" @show-password-site-profiles="showPasswordSiteProfilesModal" + @toggle-bookmark-selection="toggleBookmarkSelection" + @toggle-all-bookmarks="toggleAllBookmarks" + @delete-bookmark-from-all-profiles="deleteBookmarkFromAllProfiles" + @delete-selected-bookmarks="deleteSelectedBookmarks" + @toggle-bookmark-modal-profile-selection="toggleBookmarkModalProfileSelection" + @toggle-all-bookmark-modal-profiles="toggleAllBookmarkModalProfiles" + @delete-bookmark-from-profile="deleteBookmarkFromProfile" + @delete-selected-bookmark-profiles="deleteSelectedBookmarkProfiles" + @confirm-bookmark-removal="confirmBookmarkRemoval" + @close-bookmark-removal-confirm="closeBookmarkRemovalConfirm" + @close-bookmark-removal-result="closeBookmarkRemovalResult" @toggle-extension-selection="toggleExtensionSelection" @toggle-all-extensions="toggleAllExtensions" @delete-extension-from-all-profiles="deleteExtensionFromAllProfiles" diff --git a/src/components/browser-data/AssociatedProfilesModal.vue b/src/components/browser-data/AssociatedProfilesModal.vue index 772b09d..23f4279 100644 --- a/src/components/browser-data/AssociatedProfilesModal.vue +++ b/src/components/browser-data/AssociatedProfilesModal.vue @@ -43,6 +43,7 @@ const allSelected = computed( sortedProfiles.value.length > 0 && sortedProfiles.value.every((profile) => selectedProfileIds.value.includes(profile.id)), ); +const isSelectableMode = computed(() => props.isExtension || props.isBookmark); function hasBookmarkPath(profile: ModalProfile): profile is BookmarkAssociatedProfileSummary { return "bookmarkPath" in profile; @@ -67,7 +68,7 @@ function isSelected(profileId: string) { -