diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs index 27dc5c4..a16a8a4 100644 --- a/src-tauri/src/models.rs +++ b/src-tauri/src/models.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeSet; +use std::collections::{BTreeMap, BTreeSet}; use serde::{Deserialize, Serialize}; @@ -49,6 +49,7 @@ pub struct ExtensionSummary { pub version: Option, pub icon_data_url: Option, pub profile_ids: Vec, + pub profiles: Vec, } #[derive(Serialize)] @@ -57,6 +58,26 @@ pub struct BookmarkSummary { pub url: String, pub title: String, pub profile_ids: Vec, + pub profiles: Vec, +} + +#[derive(Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct AssociatedProfileSummary { + pub id: String, + pub name: String, + pub avatar_data_url: Option, + pub avatar_label: String, +} + +#[derive(Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct BookmarkAssociatedProfileSummary { + pub id: String, + pub name: String, + pub avatar_data_url: Option, + pub avatar_label: String, + pub bookmark_path: String, } #[derive(Serialize)] @@ -124,10 +145,12 @@ pub struct TempExtension { pub version: Option, pub icon_data_url: Option, pub profile_ids: BTreeSet, + pub profiles: BTreeMap, } pub struct TempBookmark { pub url: String, pub title: String, pub profile_ids: BTreeSet, + pub profiles: BTreeMap, } diff --git a/src-tauri/src/scanner.rs b/src-tauri/src/scanner.rs index 78982e7..11c479b 100644 --- a/src-tauri/src/scanner.rs +++ b/src-tauri/src/scanner.rs @@ -10,8 +10,9 @@ use tauri::AppHandle; use crate::{ config_store, models::{ - BookmarkSummary, BrowserConfigEntry, BrowserStats, BrowserView, ExtensionSummary, - ProfileSummary, ScanResponse, TempBookmark, TempExtension, + AssociatedProfileSummary, BookmarkAssociatedProfileSummary, BookmarkSummary, + BrowserConfigEntry, BrowserStats, BrowserView, ExtensionSummary, ProfileSummary, + ScanResponse, TempBookmark, TempExtension, }, utils::{first_non_empty, load_image_as_data_url, pick_latest_subdirectory, read_json_file}, }; @@ -57,14 +58,15 @@ fn scan_browser(config: BrowserConfigEntry) -> Option { } let profile_info = profile_cache.and_then(|cache| cache.get(&profile_id)); - profiles.push(build_profile_summary( + let profile_summary = build_profile_summary( &root, &profile_path, &profile_id, profile_info, - )); - scan_extensions_for_profile(&profile_path, &profile_id, &mut extensions); - scan_bookmarks_for_profile(&profile_path, &profile_id, &mut bookmarks); + ); + scan_extensions_for_profile(&profile_path, &profile_summary, &mut extensions); + scan_bookmarks_for_profile(&profile_path, &profile_summary, &mut bookmarks); + profiles.push(profile_summary); } let profiles = sort_profiles(profiles); @@ -76,6 +78,7 @@ fn scan_browser(config: BrowserConfigEntry) -> Option { version: entry.version, icon_data_url: entry.icon_data_url, profile_ids: entry.profile_ids.into_iter().collect(), + profiles: entry.profiles.into_values().collect(), }) .collect::>(); let bookmarks = bookmarks @@ -84,6 +87,7 @@ fn scan_browser(config: BrowserConfigEntry) -> Option { url: entry.url, title: entry.title, profile_ids: entry.profile_ids.into_iter().collect(), + profiles: entry.profiles.into_values().collect(), }) .collect::>(); @@ -188,7 +192,7 @@ fn resolve_profile_avatar( fn scan_extensions_for_profile( profile_path: &Path, - profile_id: &str, + profile: &ProfileSummary, extensions: &mut BTreeMap, ) { let extensions_root = profile_path.join("Extensions"); @@ -231,6 +235,7 @@ fn scan_extensions_for_profile( version: version.clone(), icon_data_url: icon_data_url.clone(), profile_ids: BTreeSet::new(), + profiles: BTreeMap::new(), }); if entry.name == entry.id && name != extension_id { @@ -242,7 +247,16 @@ fn scan_extensions_for_profile( if entry.icon_data_url.is_none() { entry.icon_data_url = icon_data_url.clone(); } - entry.profile_ids.insert(profile_id.to_string()); + entry.profile_ids.insert(profile.id.clone()); + entry + .profiles + .entry(profile.id.clone()) + .or_insert_with(|| AssociatedProfileSummary { + id: profile.id.clone(), + name: profile.name.clone(), + avatar_data_url: profile.avatar_data_url.clone(), + avatar_label: profile.avatar_label.clone(), + }); } } @@ -337,7 +351,7 @@ fn icon_candidates_from_object(map: &serde_json::Map) -> Vec<(u32 fn scan_bookmarks_for_profile( profile_path: &Path, - profile_id: &str, + profile: &ProfileSummary, bookmarks: &mut BTreeMap, ) { let bookmarks_path = profile_path.join("Bookmarks"); @@ -350,14 +364,15 @@ fn scan_bookmarks_for_profile( }; for root in roots.values() { - collect_bookmarks(root, profile_id, bookmarks); + collect_bookmarks(root, profile, bookmarks, &[]); } } fn collect_bookmarks( node: &Value, - profile_id: &str, + profile: &ProfileSummary, bookmarks: &mut BTreeMap, + ancestors: &[String], ) { match node.get("type").and_then(Value::as_str) { Some("url") => { @@ -381,17 +396,44 @@ fn collect_bookmarks( url: url.to_string(), title: title.clone(), profile_ids: BTreeSet::new(), + profiles: BTreeMap::new(), }); if entry.title == entry.url && title != url { entry.title = title; } - entry.profile_ids.insert(profile_id.to_string()); + entry.profile_ids.insert(profile.id.clone()); + let bookmark_path = if ancestors.is_empty() { + "Root".to_string() + } else { + ancestors.join(" > ") + }; + entry + .profiles + .entry(profile.id.clone()) + .or_insert_with(|| BookmarkAssociatedProfileSummary { + id: profile.id.clone(), + name: profile.name.clone(), + avatar_data_url: profile.avatar_data_url.clone(), + avatar_label: profile.avatar_label.clone(), + bookmark_path, + }); } Some("folder") => { if let Some(children) = node.get("children").and_then(Value::as_array) { + let folder_name = node + .get("name") + .and_then(Value::as_str) + .filter(|value| !value.is_empty()); + let next_ancestors = if let Some(name) = folder_name { + let mut path = ancestors.to_vec(); + path.push(name.to_string()); + path + } else { + ancestors.to_vec() + }; for child in children { - collect_bookmarks(child, profile_id, bookmarks); + collect_bookmarks(child, profile, bookmarks, &next_ancestors); } } } diff --git a/src/App.vue b/src/App.vue index de45128..c06fd8a 100644 --- a/src/App.vue +++ b/src/App.vue @@ -6,7 +6,7 @@ import { useBrowserManager } from "./composables/useBrowserManager"; const { activeSection, - bookmarkProfilesExpanded, + associatedProfilesModal, bookmarkSortKey, browserConfigs, browserMonogram, @@ -21,7 +21,6 @@ const { domainFromUrl, error, extensionMonogram, - extensionProfilesExpanded, extensionSortKey, isDeletingConfig, isOpeningProfile, @@ -36,11 +35,12 @@ const { savingConfig, sectionCount, selectedBrowserId, + showBookmarkProfilesModal, + showExtensionProfilesModal, sortedBookmarks, sortedExtensions, sortedProfiles, - toggleBookmarkProfiles, - toggleExtensionProfiles, + closeAssociatedProfilesModal, } = useBrowserManager(); @@ -109,16 +109,16 @@ const { :section-count="sectionCount" :is-opening-profile="isOpeningProfile" :extension-monogram="extensionMonogram" - :extension-profiles-expanded="extensionProfilesExpanded" - :bookmark-profiles-expanded="bookmarkProfilesExpanded" :domain-from-url="domainFromUrl" + :associated-profiles-modal="associatedProfilesModal" @update:active-section="activeSection = $event" @update:profile-sort-key="profileSortKey = $event" @update:extension-sort-key="extensionSortKey = $event" @update:bookmark-sort-key="bookmarkSortKey = $event" @open-profile="(browserId, profileId) => openBrowserProfile(browserId, profileId)" - @toggle-extension-profiles="toggleExtensionProfiles" - @toggle-bookmark-profiles="toggleBookmarkProfiles" + @show-extension-profiles="showExtensionProfilesModal" + @show-bookmark-profiles="showBookmarkProfilesModal" + @close-associated-profiles="closeAssociatedProfilesModal" />