From b9f24e07cf77bcc8578dd35a5e5421fc4424bf39 Mon Sep 17 00:00:00 2001 From: Julian Freeman Date: Thu, 16 Apr 2026 23:05:04 -0400 Subject: [PATCH] support history --- src-tauri/src/models.rs | 18 ++ src-tauri/src/scanner.rs | 96 +++++++++- src/App.vue | 7 + .../browser-data/BrowserDataView.vue | 25 ++- .../browser-data/HistoryDomainsList.vue | 165 ++++++++++++++++++ src/composables/useBrowserManager.ts | 32 +++- src/types/browser.ts | 12 +- src/utils/sort.ts | 12 ++ 8 files changed, 361 insertions(+), 6 deletions(-) create mode 100644 src/components/browser-data/HistoryDomainsList.vue diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs index f2d95d1..61e1cae 100644 --- a/src-tauri/src/models.rs +++ b/src-tauri/src/models.rs @@ -20,6 +20,7 @@ pub struct BrowserView { pub extensions: Vec, pub bookmarks: Vec, pub password_sites: Vec, + pub history_domains: Vec, pub stats: BrowserStats, } @@ -30,6 +31,7 @@ pub struct BrowserStats { pub extension_count: usize, pub bookmark_count: usize, pub password_site_count: usize, + pub history_domain_count: usize, } #[derive(Serialize)] @@ -75,6 +77,15 @@ pub struct PasswordSiteSummary { pub profiles: Vec, } +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct HistoryDomainSummary { + pub domain: String, + pub visit_count: i64, + pub profile_ids: Vec, + pub profiles: Vec, +} + #[derive(Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct AssociatedProfileSummary { @@ -183,3 +194,10 @@ pub struct TempPasswordSite { pub profile_ids: BTreeSet, pub profiles: BTreeMap, } + +pub struct TempHistoryDomain { + pub domain: String, + pub visit_count: i64, + pub profile_ids: BTreeSet, + pub profiles: BTreeMap, +} diff --git a/src-tauri/src/scanner.rs b/src-tauri/src/scanner.rs index d528924..9d73313 100644 --- a/src-tauri/src/scanner.rs +++ b/src-tauri/src/scanner.rs @@ -11,8 +11,9 @@ use crate::{ config_store, models::{ AssociatedProfileSummary, BookmarkAssociatedProfileSummary, BookmarkSummary, - BrowserConfigEntry, BrowserStats, BrowserView, ExtensionSummary, PasswordSiteSummary, - ProfileSummary, ScanResponse, TempBookmark, TempExtension, TempPasswordSite, + BrowserConfigEntry, BrowserStats, BrowserView, ExtensionSummary, HistoryDomainSummary, + PasswordSiteSummary, ProfileSummary, ScanResponse, TempBookmark, TempExtension, + TempHistoryDomain, TempPasswordSite, }, utils::{ copy_sqlite_database_to_temp, first_non_empty, load_image_as_data_url, read_json_file, @@ -47,6 +48,7 @@ fn scan_browser(config: BrowserConfigEntry) -> Option { let mut extensions = BTreeMap::::new(); let mut bookmarks = BTreeMap::::new(); let mut password_sites = BTreeMap::::new(); + let mut history_domains = BTreeMap::::new(); for profile_id in profile_ids { let profile_path = root.join(&profile_id); @@ -60,6 +62,7 @@ fn scan_browser(config: BrowserConfigEntry) -> Option { scan_extensions_for_profile(&profile_path, &profile_summary, &mut extensions); scan_bookmarks_for_profile(&profile_path, &profile_summary, &mut bookmarks); scan_password_sites_for_profile(&profile_path, &profile_summary, &mut password_sites); + scan_history_domains_for_profile(&profile_path, &profile_summary, &mut history_domains); profiles.push(profile_summary); } @@ -93,6 +96,15 @@ fn scan_browser(config: BrowserConfigEntry) -> Option { profiles: entry.profiles.into_values().collect(), }) .collect::>(); + let history_domains = history_domains + .into_values() + .map(|entry| HistoryDomainSummary { + domain: entry.domain, + visit_count: entry.visit_count, + profile_ids: entry.profile_ids.into_iter().collect(), + profiles: entry.profiles.into_values().collect(), + }) + .collect::>(); Some(BrowserView { browser_id: config.id, @@ -105,11 +117,13 @@ fn scan_browser(config: BrowserConfigEntry) -> Option { extension_count: extensions.len(), bookmark_count: bookmarks.len(), password_site_count: password_sites.len(), + history_domain_count: history_domains.len(), }, profiles, extensions: sort_extensions(extensions), bookmarks: sort_bookmarks(bookmarks), password_sites: sort_password_sites(password_sites), + history_domains: sort_history_domains(history_domains), }) } @@ -609,6 +623,71 @@ fn scan_password_sites_for_profile( } } +fn scan_history_domains_for_profile( + profile_path: &Path, + profile: &ProfileSummary, + history_domains: &mut BTreeMap, +) { + let history_path = profile_path.join("History"); + if !history_path.is_file() { + return; + } + + let Some(temp_copy) = copy_sqlite_database_to_temp(&history_path) else { + return; + }; + let Ok(connection) = Connection::open_with_flags( + temp_copy.path(), + OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX, + ) else { + return; + }; + + let Ok(mut statement) = connection + .prepare("SELECT url, visit_count FROM urls WHERE hidden = 0 AND visit_count > 0") + else { + return; + }; + let Ok(rows) = statement.query_map([], |row| { + Ok((row.get::<_, Option>(0)?, row.get::<_, i64>(1)?)) + }) else { + return; + }; + + for row in rows.flatten() { + let Some(url) = row.0.as_deref() else { + continue; + }; + let Some(domain) = domain_from_url(url) else { + continue; + }; + + let entry = history_domains + .entry(domain.clone()) + .or_insert_with(|| TempHistoryDomain { + domain: domain.clone(), + visit_count: 0, + profile_ids: BTreeSet::new(), + profiles: BTreeMap::new(), + }); + + entry.visit_count += row.1.max(0); + 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_icon: profile.avatar_icon.clone(), + default_avatar_fill_color: profile.default_avatar_fill_color, + default_avatar_stroke_color: profile.default_avatar_stroke_color, + avatar_label: profile.avatar_label.clone(), + }); + } +} + fn normalize_login_site(origin_url: Option<&str>, signon_realm: Option<&str>) -> Option { let candidate = [signon_realm, origin_url] .into_iter() @@ -640,3 +719,16 @@ fn sort_password_sites(mut password_sites: Vec) -> Vec, +) -> Vec { + history_domains.sort_by(|left, right| { + right + .visit_count + .cmp(&left.visit_count) + .then_with(|| left.domain.to_lowercase().cmp(&right.domain.to_lowercase())) + .then_with(|| left.domain.cmp(&right.domain)) + }); + history_domains +} diff --git a/src/App.vue b/src/App.vue index fd7ea70..2b846d4 100644 --- a/src/App.vue +++ b/src/App.vue @@ -22,6 +22,7 @@ const { error, extensionMonogram, extensionSortKey, + historyDomainSortKey, isDeletingConfig, isOpeningProfile, loading, @@ -38,9 +39,11 @@ const { selectedBrowserId, showBookmarkProfilesModal, showExtensionProfilesModal, + showHistoryDomainProfilesModal, showPasswordSiteProfilesModal, sortedBookmarks, sortedExtensions, + sortedHistoryDomains, sortedPasswordSites, sortedProfiles, closeAssociatedProfilesModal, @@ -106,10 +109,12 @@ const { :extension-sort-key="extensionSortKey" :bookmark-sort-key="bookmarkSortKey" :password-site-sort-key="passwordSiteSortKey" + :history-domain-sort-key="historyDomainSortKey" :sorted-profiles="sortedProfiles" :sorted-extensions="sortedExtensions" :sorted-bookmarks="sortedBookmarks" :sorted-password-sites="sortedPasswordSites" + :sorted-history-domains="sortedHistoryDomains" :open-profile-error="openProfileError" :section-count="sectionCount" :is-opening-profile="isOpeningProfile" @@ -121,10 +126,12 @@ const { @update:extension-sort-key="extensionSortKey = $event" @update:bookmark-sort-key="bookmarkSortKey = $event" @update:password-site-sort-key="passwordSiteSortKey = $event" + @update:history-domain-sort-key="historyDomainSortKey = $event" @open-profile="(browserId, profileId) => openBrowserProfile(browserId, profileId)" @show-extension-profiles="showExtensionProfilesModal" @show-bookmark-profiles="showBookmarkProfilesModal" @show-password-site-profiles="showPasswordSiteProfilesModal" + @show-history-domain-profiles="showHistoryDomainProfilesModal" @close-associated-profiles="closeAssociatedProfilesModal" /> diff --git a/src/components/browser-data/BrowserDataView.vue b/src/components/browser-data/BrowserDataView.vue index e2a4d66..1edd63c 100644 --- a/src/components/browser-data/BrowserDataView.vue +++ b/src/components/browser-data/BrowserDataView.vue @@ -6,12 +6,14 @@ import type { BookmarkSortKey, BrowserView, ExtensionSortKey, + HistoryDomainSortKey, PasswordSiteSortKey, ProfileSortKey, } from "../../types/browser"; import AssociatedProfilesModal from "./AssociatedProfilesModal.vue"; import BookmarksList from "./BookmarksList.vue"; import ExtensionsList from "./ExtensionsList.vue"; +import HistoryDomainsList from "./HistoryDomainsList.vue"; import PasswordSitesList from "./PasswordSitesList.vue"; import ProfilesList from "./ProfilesList.vue"; @@ -22,10 +24,12 @@ defineProps<{ extensionSortKey: ExtensionSortKey; bookmarkSortKey: BookmarkSortKey; passwordSiteSortKey: PasswordSiteSortKey; + historyDomainSortKey: HistoryDomainSortKey; sortedProfiles: BrowserView["profiles"]; sortedExtensions: BrowserView["extensions"]; sortedBookmarks: BrowserView["bookmarks"]; sortedPasswordSites: BrowserView["passwordSites"]; + sortedHistoryDomains: BrowserView["historyDomains"]; openProfileError: string; sectionCount: (section: ActiveSection) => number; isOpeningProfile: (browserId: string, profileId: string) => boolean; @@ -45,10 +49,12 @@ const emit = defineEmits<{ "update:extensionSortKey": [value: ExtensionSortKey]; "update:bookmarkSortKey": [value: BookmarkSortKey]; "update:passwordSiteSortKey": [value: PasswordSiteSortKey]; + "update:historyDomainSortKey": [value: HistoryDomainSortKey]; openProfile: [browserId: string, profileId: string]; showExtensionProfiles: [extensionId: string]; showBookmarkProfiles: [url: string]; showPasswordSiteProfiles: [url: string]; + showHistoryDomainProfiles: [domain: string]; closeAssociatedProfiles: []; }>(); @@ -91,6 +97,15 @@ const emit = defineEmits<{ Saved Logins {{ sectionCount("passwords") }} +
@@ -124,12 +139,20 @@ const emit = defineEmits<{ /> + +
+import type { HistoryDomainSortKey, HistoryDomainSummary } from "../../types/browser"; + +defineProps<{ + historyDomains: HistoryDomainSummary[]; + sortKey: HistoryDomainSortKey; +}>(); + +const emit = defineEmits<{ + "update:sortKey": [value: HistoryDomainSortKey]; + showProfiles: [domain: string]; +}>(); + + + + + diff --git a/src/composables/useBrowserManager.ts b/src/composables/useBrowserManager.ts index f483160..c91ee57 100644 --- a/src/composables/useBrowserManager.ts +++ b/src/composables/useBrowserManager.ts @@ -2,7 +2,13 @@ import { computed, onMounted, ref, watch } from "vue"; import { invoke } from "@tauri-apps/api/core"; import { open } from "@tauri-apps/plugin-dialog"; -import { sortBookmarks, sortExtensions, sortPasswordSites, sortProfiles } from "../utils/sort"; +import { + sortBookmarks, + sortExtensions, + sortHistoryDomains, + sortPasswordSites, + sortProfiles, +} from "../utils/sort"; import type { ActiveSection, AppPage, @@ -14,6 +20,7 @@ import type { BrowserView, CreateCustomBrowserConfigInput, ExtensionSortKey, + HistoryDomainSortKey, PasswordSiteSortKey, ProfileSortKey, ScanResponse, @@ -49,6 +56,7 @@ export function useBrowserManager() { const extensionSortKey = ref("name"); const bookmarkSortKey = ref("title"); const passwordSiteSortKey = ref("domain"); + const historyDomainSortKey = ref("visits"); const browsers = computed(() => response.value.browsers); const currentBrowser = computed( @@ -70,6 +78,9 @@ export function useBrowserManager() { const sortedPasswordSites = computed(() => sortPasswordSites(currentBrowser.value?.passwordSites ?? [], passwordSiteSortKey.value), ); + const sortedHistoryDomains = computed(() => + sortHistoryDomains(currentBrowser.value?.historyDomains ?? [], historyDomainSortKey.value), + ); watch( browsers, @@ -286,7 +297,8 @@ export function useBrowserManager() { if (section === "profiles") return currentBrowser.value.profiles.length; if (section === "extensions") return currentBrowser.value.extensions.length; if (section === "bookmarks") return currentBrowser.value.bookmarks.length; - return currentBrowser.value.passwordSites.length; + if (section === "passwords") return currentBrowser.value.passwordSites.length; + return currentBrowser.value.historyDomains.length; } function showExtensionProfilesModal(extensionId: string) { @@ -322,6 +334,19 @@ export function useBrowserManager() { }; } + function showHistoryDomainProfilesModal(domain: string) { + const historyDomain = currentBrowser.value?.historyDomains.find( + (item) => item.domain === domain, + ); + if (!historyDomain || !currentBrowser.value) return; + associatedProfilesModal.value = { + title: `${historyDomain.domain} Profiles`, + browserId: currentBrowser.value.browserId, + profiles: historyDomain.profiles, + isBookmark: false, + }; + } + function closeAssociatedProfilesModal() { associatedProfilesModal.value = null; } @@ -348,6 +373,7 @@ export function useBrowserManager() { error, extensionMonogram, extensionSortKey, + historyDomainSortKey, isDeletingConfig, isOpeningProfile, loading, @@ -364,9 +390,11 @@ export function useBrowserManager() { selectedBrowserId, showBookmarkProfilesModal, showExtensionProfilesModal, + showHistoryDomainProfilesModal, showPasswordSiteProfilesModal, sortedBookmarks, sortedExtensions, + sortedHistoryDomains, sortedPasswordSites, sortedProfiles, closeAssociatedProfilesModal, diff --git a/src/types/browser.ts b/src/types/browser.ts index 555857f..e47fac6 100644 --- a/src/types/browser.ts +++ b/src/types/browser.ts @@ -3,6 +3,7 @@ export type BrowserStats = { extensionCount: number; bookmarkCount: number; passwordSiteCount: number; + historyDomainCount: number; }; export type ProfileSummary = { @@ -40,6 +41,13 @@ export type PasswordSiteSummary = { profiles: AssociatedProfileSummary[]; }; +export type HistoryDomainSummary = { + domain: string; + visitCount: number; + profileIds: string[]; + profiles: AssociatedProfileSummary[]; +}; + export type AssociatedProfileSummary = { id: string; name: string; @@ -65,8 +73,9 @@ export type ProfileSortKey = "name" | "email" | "id"; export type ExtensionSortKey = "name" | "id"; export type BookmarkSortKey = "title" | "url"; export type PasswordSiteSortKey = "domain" | "url"; +export type HistoryDomainSortKey = "visits" | "domain"; export type AssociatedProfileSortKey = "id" | "name"; -export type ActiveSection = "profiles" | "extensions" | "bookmarks" | "passwords"; +export type ActiveSection = "profiles" | "extensions" | "bookmarks" | "passwords" | "history"; export type AppPage = "browserData" | "configuration"; export type BrowserConfigSource = "default" | "custom"; @@ -102,6 +111,7 @@ export type BrowserView = { extensions: ExtensionSummary[]; bookmarks: BookmarkSummary[]; passwordSites: PasswordSiteSummary[]; + historyDomains: HistoryDomainSummary[]; stats: BrowserStats; }; diff --git a/src/utils/sort.ts b/src/utils/sort.ts index ea52846..01756d8 100644 --- a/src/utils/sort.ts +++ b/src/utils/sort.ts @@ -3,6 +3,8 @@ import type { BookmarkSummary, ExtensionSortKey, ExtensionSummary, + HistoryDomainSortKey, + HistoryDomainSummary, PasswordSiteSortKey, PasswordSiteSummary, AssociatedProfileSortKey, @@ -88,6 +90,16 @@ export function sortPasswordSites(items: PasswordSiteSummary[], sortKey: Passwor }); } +export function sortHistoryDomains(items: HistoryDomainSummary[], sortKey: HistoryDomainSortKey) { + const historyDomains = [...items]; + return historyDomains.sort((left, right) => { + if (sortKey === "domain") { + return compareOptionalText(left.domain, right.domain) || right.visitCount - left.visitCount; + } + return right.visitCount - left.visitCount || compareOptionalText(left.domain, right.domain); + }); +} + export function sortAssociatedProfiles( items: (AssociatedProfileSummary | BookmarkAssociatedProfileSummary)[], sortKey: AssociatedProfileSortKey,