support login data
This commit is contained in:
74
src-tauri/Cargo.lock
generated
74
src-tauri/Cargo.lock
generated
@@ -8,6 +8,18 @@ version = "2.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ahash"
|
||||||
|
version = "0.8.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"version_check",
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.4"
|
version = "1.1.4"
|
||||||
@@ -449,6 +461,7 @@ name = "chrom-tool"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
|
"rusqlite",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
@@ -938,6 +951,18 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fallible-iterator"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fallible-streaming-iterator"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "2.4.1"
|
version = "2.4.1"
|
||||||
@@ -1446,6 +1471,15 @@ version = "0.12.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.14.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||||
|
dependencies = [
|
||||||
|
"ahash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.15.5"
|
version = "0.15.5"
|
||||||
@@ -1461,6 +1495,15 @@ version = "0.17.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
|
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashlink"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
|
||||||
|
dependencies = [
|
||||||
|
"hashbrown 0.14.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
@@ -1990,6 +2033,17 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libsqlite3-sys"
|
||||||
|
version = "0.30.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"pkg-config",
|
||||||
|
"vcpkg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
@@ -3033,6 +3087,20 @@ dependencies = [
|
|||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rusqlite"
|
||||||
|
version = "0.32.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.11.1",
|
||||||
|
"fallible-iterator",
|
||||||
|
"fallible-streaming-iterator",
|
||||||
|
"hashlink",
|
||||||
|
"libsqlite3-sys",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
version = "2.1.2"
|
version = "2.1.2"
|
||||||
@@ -4421,6 +4489,12 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vcpkg"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version-compare"
|
name = "version-compare"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
|||||||
@@ -24,4 +24,4 @@ serde = { version = "1", features = ["derive"] }
|
|||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
tauri-plugin-dialog = "2.7.0"
|
tauri-plugin-dialog = "2.7.0"
|
||||||
|
rusqlite = { version = "0.32", features = ["bundled"] }
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ pub struct BrowserView {
|
|||||||
pub profiles: Vec<ProfileSummary>,
|
pub profiles: Vec<ProfileSummary>,
|
||||||
pub extensions: Vec<ExtensionSummary>,
|
pub extensions: Vec<ExtensionSummary>,
|
||||||
pub bookmarks: Vec<BookmarkSummary>,
|
pub bookmarks: Vec<BookmarkSummary>,
|
||||||
|
pub password_sites: Vec<PasswordSiteSummary>,
|
||||||
pub stats: BrowserStats,
|
pub stats: BrowserStats,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@ pub struct BrowserStats {
|
|||||||
pub profile_count: usize,
|
pub profile_count: usize,
|
||||||
pub extension_count: usize,
|
pub extension_count: usize,
|
||||||
pub bookmark_count: usize,
|
pub bookmark_count: usize,
|
||||||
|
pub password_site_count: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -64,6 +66,15 @@ pub struct BookmarkSummary {
|
|||||||
pub profiles: Vec<BookmarkAssociatedProfileSummary>,
|
pub profiles: Vec<BookmarkAssociatedProfileSummary>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct PasswordSiteSummary {
|
||||||
|
pub url: String,
|
||||||
|
pub domain: String,
|
||||||
|
pub profile_ids: Vec<String>,
|
||||||
|
pub profiles: Vec<AssociatedProfileSummary>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Clone)]
|
#[derive(Serialize, Clone)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct AssociatedProfileSummary {
|
pub struct AssociatedProfileSummary {
|
||||||
@@ -165,3 +176,10 @@ pub struct TempBookmark {
|
|||||||
pub profile_ids: BTreeSet<String>,
|
pub profile_ids: BTreeSet<String>,
|
||||||
pub profiles: BTreeMap<String, BookmarkAssociatedProfileSummary>,
|
pub profiles: BTreeMap<String, BookmarkAssociatedProfileSummary>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct TempPasswordSite {
|
||||||
|
pub url: String,
|
||||||
|
pub domain: String,
|
||||||
|
pub profile_ids: BTreeSet<String>,
|
||||||
|
pub profiles: BTreeMap<String, AssociatedProfileSummary>,
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use std::{
|
|||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use rusqlite::{Connection, OpenFlags};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use tauri::AppHandle;
|
use tauri::AppHandle;
|
||||||
|
|
||||||
@@ -10,10 +11,12 @@ use crate::{
|
|||||||
config_store,
|
config_store,
|
||||||
models::{
|
models::{
|
||||||
AssociatedProfileSummary, BookmarkAssociatedProfileSummary, BookmarkSummary,
|
AssociatedProfileSummary, BookmarkAssociatedProfileSummary, BookmarkSummary,
|
||||||
BrowserConfigEntry, BrowserStats, BrowserView, ExtensionSummary, ProfileSummary,
|
BrowserConfigEntry, BrowserStats, BrowserView, ExtensionSummary, PasswordSiteSummary,
|
||||||
ScanResponse, TempBookmark, TempExtension,
|
ProfileSummary, ScanResponse, TempBookmark, TempExtension, TempPasswordSite,
|
||||||
|
},
|
||||||
|
utils::{
|
||||||
|
copy_sqlite_database_to_temp, first_non_empty, load_image_as_data_url, read_json_file,
|
||||||
},
|
},
|
||||||
utils::{first_non_empty, load_image_as_data_url, read_json_file},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn scan_browsers(app: &AppHandle) -> Result<ScanResponse, String> {
|
pub fn scan_browsers(app: &AppHandle) -> Result<ScanResponse, String> {
|
||||||
@@ -43,6 +46,7 @@ fn scan_browser(config: BrowserConfigEntry) -> Option<BrowserView> {
|
|||||||
let mut profiles = Vec::new();
|
let mut profiles = Vec::new();
|
||||||
let mut extensions = BTreeMap::<String, TempExtension>::new();
|
let mut extensions = BTreeMap::<String, TempExtension>::new();
|
||||||
let mut bookmarks = BTreeMap::<String, TempBookmark>::new();
|
let mut bookmarks = BTreeMap::<String, TempBookmark>::new();
|
||||||
|
let mut password_sites = BTreeMap::<String, TempPasswordSite>::new();
|
||||||
|
|
||||||
for profile_id in profile_ids {
|
for profile_id in profile_ids {
|
||||||
let profile_path = root.join(&profile_id);
|
let profile_path = root.join(&profile_id);
|
||||||
@@ -51,14 +55,11 @@ fn scan_browser(config: BrowserConfigEntry) -> Option<BrowserView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let profile_info = profile_cache.and_then(|cache| cache.get(&profile_id));
|
let profile_info = profile_cache.and_then(|cache| cache.get(&profile_id));
|
||||||
let profile_summary = build_profile_summary(
|
let profile_summary =
|
||||||
&root,
|
build_profile_summary(&root, &profile_path, &profile_id, profile_info);
|
||||||
&profile_path,
|
|
||||||
&profile_id,
|
|
||||||
profile_info,
|
|
||||||
);
|
|
||||||
scan_extensions_for_profile(&profile_path, &profile_summary, &mut extensions);
|
scan_extensions_for_profile(&profile_path, &profile_summary, &mut extensions);
|
||||||
scan_bookmarks_for_profile(&profile_path, &profile_summary, &mut bookmarks);
|
scan_bookmarks_for_profile(&profile_path, &profile_summary, &mut bookmarks);
|
||||||
|
scan_password_sites_for_profile(&profile_path, &profile_summary, &mut password_sites);
|
||||||
profiles.push(profile_summary);
|
profiles.push(profile_summary);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,6 +84,15 @@ fn scan_browser(config: BrowserConfigEntry) -> Option<BrowserView> {
|
|||||||
profiles: entry.profiles.into_values().collect(),
|
profiles: entry.profiles.into_values().collect(),
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
let password_sites = password_sites
|
||||||
|
.into_values()
|
||||||
|
.map(|entry| PasswordSiteSummary {
|
||||||
|
url: entry.url,
|
||||||
|
domain: entry.domain,
|
||||||
|
profile_ids: entry.profile_ids.into_iter().collect(),
|
||||||
|
profiles: entry.profiles.into_values().collect(),
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
Some(BrowserView {
|
Some(BrowserView {
|
||||||
browser_id: config.id,
|
browser_id: config.id,
|
||||||
@@ -94,10 +104,12 @@ fn scan_browser(config: BrowserConfigEntry) -> Option<BrowserView> {
|
|||||||
profile_count: profiles.len(),
|
profile_count: profiles.len(),
|
||||||
extension_count: extensions.len(),
|
extension_count: extensions.len(),
|
||||||
bookmark_count: bookmarks.len(),
|
bookmark_count: bookmarks.len(),
|
||||||
|
password_site_count: password_sites.len(),
|
||||||
},
|
},
|
||||||
profiles,
|
profiles,
|
||||||
extensions: sort_extensions(extensions),
|
extensions: sort_extensions(extensions),
|
||||||
bookmarks: sort_bookmarks(bookmarks),
|
bookmarks: sort_bookmarks(bookmarks),
|
||||||
|
password_sites: sort_password_sites(password_sites),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,7 +303,10 @@ fn resolve_extension_install_dir(
|
|||||||
} else if candidate.is_absolute() {
|
} else if candidate.is_absolute() {
|
||||||
(candidate, ExtensionInstallSource::ExternalAbsolute)
|
(candidate, ExtensionInstallSource::ExternalAbsolute)
|
||||||
} else {
|
} else {
|
||||||
(PathBuf::from(raw_path), ExtensionInstallSource::ExternalAbsolute)
|
(
|
||||||
|
PathBuf::from(raw_path),
|
||||||
|
ExtensionInstallSource::ExternalAbsolute,
|
||||||
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
resolved.is_dir().then_some((resolved, source))
|
resolved.is_dir().then_some((resolved, source))
|
||||||
@@ -451,10 +466,8 @@ fn collect_bookmarks(
|
|||||||
} else {
|
} else {
|
||||||
ancestors.join(" > ")
|
ancestors.join(" > ")
|
||||||
};
|
};
|
||||||
entry
|
entry.profiles.entry(profile.id.clone()).or_insert_with(|| {
|
||||||
.profiles
|
BookmarkAssociatedProfileSummary {
|
||||||
.entry(profile.id.clone())
|
|
||||||
.or_insert_with(|| BookmarkAssociatedProfileSummary {
|
|
||||||
id: profile.id.clone(),
|
id: profile.id.clone(),
|
||||||
name: profile.name.clone(),
|
name: profile.name.clone(),
|
||||||
avatar_data_url: profile.avatar_data_url.clone(),
|
avatar_data_url: profile.avatar_data_url.clone(),
|
||||||
@@ -463,7 +476,8 @@ fn collect_bookmarks(
|
|||||||
default_avatar_stroke_color: profile.default_avatar_stroke_color,
|
default_avatar_stroke_color: profile.default_avatar_stroke_color,
|
||||||
avatar_label: profile.avatar_label.clone(),
|
avatar_label: profile.avatar_label.clone(),
|
||||||
bookmark_path,
|
bookmark_path,
|
||||||
});
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Some("folder") => {
|
Some("folder") => {
|
||||||
if let Some(children) = node.get("children").and_then(Value::as_array) {
|
if let Some(children) = node.get("children").and_then(Value::as_array) {
|
||||||
@@ -526,3 +540,103 @@ fn sort_bookmarks(mut bookmarks: Vec<BookmarkSummary>) -> Vec<BookmarkSummary> {
|
|||||||
});
|
});
|
||||||
bookmarks
|
bookmarks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn scan_password_sites_for_profile(
|
||||||
|
profile_path: &Path,
|
||||||
|
profile: &ProfileSummary,
|
||||||
|
password_sites: &mut BTreeMap<String, TempPasswordSite>,
|
||||||
|
) {
|
||||||
|
let login_data_path = profile_path.join("Login Data");
|
||||||
|
if !login_data_path.is_file() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(temp_copy) = copy_sqlite_database_to_temp(&login_data_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 origin_url, signon_realm FROM logins WHERE blacklisted_by_user = 0")
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(rows) = statement.query_map([], |row| {
|
||||||
|
Ok((
|
||||||
|
row.get::<_, Option<String>>(0)?,
|
||||||
|
row.get::<_, Option<String>>(1)?,
|
||||||
|
))
|
||||||
|
}) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
for row in rows.flatten() {
|
||||||
|
let Some(url) = normalize_login_site(row.0.as_deref(), row.1.as_deref()) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let domain = domain_from_url(&url).unwrap_or_else(|| url.clone());
|
||||||
|
|
||||||
|
let entry = password_sites
|
||||||
|
.entry(url.clone())
|
||||||
|
.or_insert_with(|| TempPasswordSite {
|
||||||
|
url: url.clone(),
|
||||||
|
domain: domain.clone(),
|
||||||
|
profile_ids: BTreeSet::new(),
|
||||||
|
profiles: BTreeMap::new(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if entry.domain == entry.url && domain != entry.url {
|
||||||
|
entry.domain = domain.clone();
|
||||||
|
}
|
||||||
|
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<String> {
|
||||||
|
let candidate = [signon_realm, origin_url]
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.map(str::trim)
|
||||||
|
.find(|value| {
|
||||||
|
!value.is_empty() && (value.starts_with("http://") || value.starts_with("https://"))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Some(candidate.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn domain_from_url(url: &str) -> Option<String> {
|
||||||
|
let (_, remainder) = url.split_once("://")?;
|
||||||
|
let host = remainder.split('/').next()?.trim();
|
||||||
|
if host.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(host.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sort_password_sites(mut password_sites: Vec<PasswordSiteSummary>) -> Vec<PasswordSiteSummary> {
|
||||||
|
password_sites.sort_by(|left, right| {
|
||||||
|
left.domain
|
||||||
|
.to_lowercase()
|
||||||
|
.cmp(&right.domain.to_lowercase())
|
||||||
|
.then_with(|| left.url.cmp(&right.url))
|
||||||
|
});
|
||||||
|
password_sites
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use std::{
|
use std::{
|
||||||
env, fs,
|
env, fs,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
|
process,
|
||||||
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
|
|
||||||
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
||||||
@@ -46,3 +48,48 @@ pub fn first_non_empty<'a>(values: impl IntoIterator<Item = Option<&'a str>>) ->
|
|||||||
.flatten()
|
.flatten()
|
||||||
.find(|value| !value.trim().is_empty())
|
.find(|value| !value.trim().is_empty())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct TempSqliteCopy {
|
||||||
|
path: PathBuf,
|
||||||
|
directory: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TempSqliteCopy {
|
||||||
|
pub fn path(&self) -> &Path {
|
||||||
|
&self.path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for TempSqliteCopy {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = fs::remove_dir_all(&self.directory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn copy_sqlite_database_to_temp(path: &Path) -> Option<TempSqliteCopy> {
|
||||||
|
let file_name = path.file_name()?.to_str()?;
|
||||||
|
let unique_id = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.ok()?
|
||||||
|
.as_nanos();
|
||||||
|
let directory =
|
||||||
|
env::temp_dir().join(format!("chrom-tool-scan-{}-{}", process::id(), unique_id));
|
||||||
|
|
||||||
|
fs::create_dir_all(&directory).ok()?;
|
||||||
|
|
||||||
|
let main_target = directory.join(file_name);
|
||||||
|
fs::copy(path, &main_target).ok()?;
|
||||||
|
|
||||||
|
for suffix in ["-wal", "-shm"] {
|
||||||
|
let source = PathBuf::from(format!("{}{}", path.display(), suffix));
|
||||||
|
if source.is_file() {
|
||||||
|
let target = directory.join(format!("{file_name}{suffix}"));
|
||||||
|
let _ = fs::copy(source, target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(TempSqliteCopy {
|
||||||
|
path: main_target,
|
||||||
|
directory,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ const {
|
|||||||
page,
|
page,
|
||||||
pickExecutablePath,
|
pickExecutablePath,
|
||||||
pickUserDataPath,
|
pickUserDataPath,
|
||||||
|
passwordSiteSortKey,
|
||||||
profileSortKey,
|
profileSortKey,
|
||||||
refreshAll,
|
refreshAll,
|
||||||
savingConfig,
|
savingConfig,
|
||||||
@@ -37,8 +38,10 @@ const {
|
|||||||
selectedBrowserId,
|
selectedBrowserId,
|
||||||
showBookmarkProfilesModal,
|
showBookmarkProfilesModal,
|
||||||
showExtensionProfilesModal,
|
showExtensionProfilesModal,
|
||||||
|
showPasswordSiteProfilesModal,
|
||||||
sortedBookmarks,
|
sortedBookmarks,
|
||||||
sortedExtensions,
|
sortedExtensions,
|
||||||
|
sortedPasswordSites,
|
||||||
sortedProfiles,
|
sortedProfiles,
|
||||||
closeAssociatedProfilesModal,
|
closeAssociatedProfilesModal,
|
||||||
} = useBrowserManager();
|
} = useBrowserManager();
|
||||||
@@ -102,9 +105,11 @@ const {
|
|||||||
:profile-sort-key="profileSortKey"
|
:profile-sort-key="profileSortKey"
|
||||||
:extension-sort-key="extensionSortKey"
|
:extension-sort-key="extensionSortKey"
|
||||||
:bookmark-sort-key="bookmarkSortKey"
|
:bookmark-sort-key="bookmarkSortKey"
|
||||||
|
:password-site-sort-key="passwordSiteSortKey"
|
||||||
:sorted-profiles="sortedProfiles"
|
:sorted-profiles="sortedProfiles"
|
||||||
:sorted-extensions="sortedExtensions"
|
:sorted-extensions="sortedExtensions"
|
||||||
:sorted-bookmarks="sortedBookmarks"
|
:sorted-bookmarks="sortedBookmarks"
|
||||||
|
:sorted-password-sites="sortedPasswordSites"
|
||||||
:open-profile-error="openProfileError"
|
:open-profile-error="openProfileError"
|
||||||
:section-count="sectionCount"
|
:section-count="sectionCount"
|
||||||
:is-opening-profile="isOpeningProfile"
|
:is-opening-profile="isOpeningProfile"
|
||||||
@@ -115,9 +120,11 @@ const {
|
|||||||
@update:profile-sort-key="profileSortKey = $event"
|
@update:profile-sort-key="profileSortKey = $event"
|
||||||
@update:extension-sort-key="extensionSortKey = $event"
|
@update:extension-sort-key="extensionSortKey = $event"
|
||||||
@update:bookmark-sort-key="bookmarkSortKey = $event"
|
@update:bookmark-sort-key="bookmarkSortKey = $event"
|
||||||
|
@update:password-site-sort-key="passwordSiteSortKey = $event"
|
||||||
@open-profile="(browserId, profileId) => openBrowserProfile(browserId, profileId)"
|
@open-profile="(browserId, profileId) => openBrowserProfile(browserId, profileId)"
|
||||||
@show-extension-profiles="showExtensionProfilesModal"
|
@show-extension-profiles="showExtensionProfilesModal"
|
||||||
@show-bookmark-profiles="showBookmarkProfilesModal"
|
@show-bookmark-profiles="showBookmarkProfilesModal"
|
||||||
|
@show-password-site-profiles="showPasswordSiteProfilesModal"
|
||||||
@close-associated-profiles="closeAssociatedProfilesModal"
|
@close-associated-profiles="closeAssociatedProfilesModal"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import type {
|
|||||||
BookmarkSortKey,
|
BookmarkSortKey,
|
||||||
BrowserView,
|
BrowserView,
|
||||||
ExtensionSortKey,
|
ExtensionSortKey,
|
||||||
|
PasswordSiteSortKey,
|
||||||
ProfileSortKey,
|
ProfileSortKey,
|
||||||
} from "../../types/browser";
|
} from "../../types/browser";
|
||||||
import AssociatedProfilesModal from "./AssociatedProfilesModal.vue";
|
import AssociatedProfilesModal from "./AssociatedProfilesModal.vue";
|
||||||
import BookmarksList from "./BookmarksList.vue";
|
import BookmarksList from "./BookmarksList.vue";
|
||||||
import ExtensionsList from "./ExtensionsList.vue";
|
import ExtensionsList from "./ExtensionsList.vue";
|
||||||
|
import PasswordSitesList from "./PasswordSitesList.vue";
|
||||||
import ProfilesList from "./ProfilesList.vue";
|
import ProfilesList from "./ProfilesList.vue";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -19,9 +21,11 @@ defineProps<{
|
|||||||
profileSortKey: ProfileSortKey;
|
profileSortKey: ProfileSortKey;
|
||||||
extensionSortKey: ExtensionSortKey;
|
extensionSortKey: ExtensionSortKey;
|
||||||
bookmarkSortKey: BookmarkSortKey;
|
bookmarkSortKey: BookmarkSortKey;
|
||||||
|
passwordSiteSortKey: PasswordSiteSortKey;
|
||||||
sortedProfiles: BrowserView["profiles"];
|
sortedProfiles: BrowserView["profiles"];
|
||||||
sortedExtensions: BrowserView["extensions"];
|
sortedExtensions: BrowserView["extensions"];
|
||||||
sortedBookmarks: BrowserView["bookmarks"];
|
sortedBookmarks: BrowserView["bookmarks"];
|
||||||
|
sortedPasswordSites: BrowserView["passwordSites"];
|
||||||
openProfileError: string;
|
openProfileError: string;
|
||||||
sectionCount: (section: ActiveSection) => number;
|
sectionCount: (section: ActiveSection) => number;
|
||||||
isOpeningProfile: (browserId: string, profileId: string) => boolean;
|
isOpeningProfile: (browserId: string, profileId: string) => boolean;
|
||||||
@@ -40,9 +44,11 @@ const emit = defineEmits<{
|
|||||||
"update:profileSortKey": [value: ProfileSortKey];
|
"update:profileSortKey": [value: ProfileSortKey];
|
||||||
"update:extensionSortKey": [value: ExtensionSortKey];
|
"update:extensionSortKey": [value: ExtensionSortKey];
|
||||||
"update:bookmarkSortKey": [value: BookmarkSortKey];
|
"update:bookmarkSortKey": [value: BookmarkSortKey];
|
||||||
|
"update:passwordSiteSortKey": [value: PasswordSiteSortKey];
|
||||||
openProfile: [browserId: string, profileId: string];
|
openProfile: [browserId: string, profileId: string];
|
||||||
showExtensionProfiles: [extensionId: string];
|
showExtensionProfiles: [extensionId: string];
|
||||||
showBookmarkProfiles: [url: string];
|
showBookmarkProfiles: [url: string];
|
||||||
|
showPasswordSiteProfiles: [url: string];
|
||||||
closeAssociatedProfiles: [];
|
closeAssociatedProfiles: [];
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
@@ -76,6 +82,15 @@ const emit = defineEmits<{
|
|||||||
<span>Bookmarks</span>
|
<span>Bookmarks</span>
|
||||||
<span class="count-pill">{{ sectionCount("bookmarks") }}</span>
|
<span class="count-pill">{{ sectionCount("bookmarks") }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
class="section-tab"
|
||||||
|
:class="{ active: activeSection === 'passwords' }"
|
||||||
|
type="button"
|
||||||
|
@click="emit('update:activeSection', 'passwords')"
|
||||||
|
>
|
||||||
|
<span>Saved Logins</span>
|
||||||
|
<span class="count-pill">{{ sectionCount("passwords") }}</span>
|
||||||
|
</button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="content-scroll-area">
|
<div class="content-scroll-area">
|
||||||
@@ -101,12 +116,20 @@ const emit = defineEmits<{
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<BookmarksList
|
<BookmarksList
|
||||||
v-else
|
v-else-if="activeSection === 'bookmarks'"
|
||||||
:bookmarks="sortedBookmarks"
|
:bookmarks="sortedBookmarks"
|
||||||
:sort-key="bookmarkSortKey"
|
:sort-key="bookmarkSortKey"
|
||||||
@update:sort-key="emit('update:bookmarkSortKey', $event)"
|
@update:sort-key="emit('update:bookmarkSortKey', $event)"
|
||||||
@show-profiles="emit('showBookmarkProfiles', $event)"
|
@show-profiles="emit('showBookmarkProfiles', $event)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<PasswordSitesList
|
||||||
|
v-else
|
||||||
|
:password-sites="sortedPasswordSites"
|
||||||
|
:sort-key="passwordSiteSortKey"
|
||||||
|
@update:sort-key="emit('update:passwordSiteSortKey', $event)"
|
||||||
|
@show-profiles="emit('showPasswordSiteProfiles', $event)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AssociatedProfilesModal
|
<AssociatedProfilesModal
|
||||||
|
|||||||
174
src/components/browser-data/PasswordSitesList.vue
Normal file
174
src/components/browser-data/PasswordSitesList.vue
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PasswordSiteSortKey, PasswordSiteSummary } from "../../types/browser";
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
passwordSites: PasswordSiteSummary[];
|
||||||
|
sortKey: PasswordSiteSortKey;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
"update:sortKey": [value: PasswordSiteSortKey];
|
||||||
|
showProfiles: [url: string];
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="table-section">
|
||||||
|
<div v-if="passwordSites.length" class="data-table">
|
||||||
|
<div class="data-table-header passwords-grid">
|
||||||
|
<button class="header-cell sortable" :class="{ active: sortKey === 'domain' }" type="button" @click="emit('update:sortKey', 'domain')">Domain</button>
|
||||||
|
<button class="header-cell sortable" :class="{ active: sortKey === 'url' }" type="button" @click="emit('update:sortKey', 'url')">URL</button>
|
||||||
|
<div class="header-cell actions-cell">Profiles</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-table-body styled-scrollbar">
|
||||||
|
<article
|
||||||
|
v-for="passwordSite in passwordSites"
|
||||||
|
:key="passwordSite.url"
|
||||||
|
class="data-table-row passwords-grid"
|
||||||
|
>
|
||||||
|
<div class="row-cell primary-cell">
|
||||||
|
<strong>{{ passwordSite.domain }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="row-cell muted-cell" :title="passwordSite.url">{{ passwordSite.url }}</div>
|
||||||
|
<div class="row-cell actions-cell">
|
||||||
|
<button class="disclosure-button" type="button" @click="emit('showProfiles', passwordSite.url)">
|
||||||
|
<span>View</span>
|
||||||
|
<span class="badge neutral">{{ passwordSite.profileIds.length }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="empty-card">
|
||||||
|
<p>No saved login sites were discovered for this browser.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.table-section {
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||||
|
border-radius: 22px;
|
||||||
|
background: var(--panel);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table-body {
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
}
|
||||||
|
|
||||||
|
.passwords-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(180px, 0.9fr) minmax(280px, 1.2fr) 154px;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 2;
|
||||||
|
padding: 10px 24px 10px 14px;
|
||||||
|
border-bottom: 1px solid rgba(148, 163, 184, 0.14);
|
||||||
|
background: rgba(248, 250, 252, 0.94);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-cell {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.81rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-cell.sortable {
|
||||||
|
padding: 0;
|
||||||
|
text-align: left;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-cell.sortable.active {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table-row {
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-bottom: 1px solid rgba(148, 163, 184, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table-row:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table-row:hover {
|
||||||
|
background: rgba(248, 250, 252, 0.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-cell {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-cell strong {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.93rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.muted-cell {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.87rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disclosure-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
width: fit-content;
|
||||||
|
min-width: 120px;
|
||||||
|
padding: 7px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(241, 245, 249, 0.9);
|
||||||
|
color: var(--badge-text);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-cell {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.passwords-grid {
|
||||||
|
grid-template-columns: minmax(160px, 0.9fr) minmax(200px, 1fr) 148px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.passwords-grid {
|
||||||
|
grid-template-columns: minmax(0, 1fr) 132px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.passwords-grid > :nth-child(2) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,7 +2,7 @@ import { computed, onMounted, ref, watch } from "vue";
|
|||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { open } from "@tauri-apps/plugin-dialog";
|
import { open } from "@tauri-apps/plugin-dialog";
|
||||||
|
|
||||||
import { sortBookmarks, sortExtensions, sortProfiles } from "../utils/sort";
|
import { sortBookmarks, sortExtensions, sortPasswordSites, sortProfiles } from "../utils/sort";
|
||||||
import type {
|
import type {
|
||||||
ActiveSection,
|
ActiveSection,
|
||||||
AppPage,
|
AppPage,
|
||||||
@@ -14,6 +14,7 @@ import type {
|
|||||||
BrowserView,
|
BrowserView,
|
||||||
CreateCustomBrowserConfigInput,
|
CreateCustomBrowserConfigInput,
|
||||||
ExtensionSortKey,
|
ExtensionSortKey,
|
||||||
|
PasswordSiteSortKey,
|
||||||
ProfileSortKey,
|
ProfileSortKey,
|
||||||
ScanResponse,
|
ScanResponse,
|
||||||
} from "../types/browser";
|
} from "../types/browser";
|
||||||
@@ -47,6 +48,7 @@ export function useBrowserManager() {
|
|||||||
const profileSortKey = ref<ProfileSortKey>("name");
|
const profileSortKey = ref<ProfileSortKey>("name");
|
||||||
const extensionSortKey = ref<ExtensionSortKey>("name");
|
const extensionSortKey = ref<ExtensionSortKey>("name");
|
||||||
const bookmarkSortKey = ref<BookmarkSortKey>("title");
|
const bookmarkSortKey = ref<BookmarkSortKey>("title");
|
||||||
|
const passwordSiteSortKey = ref<PasswordSiteSortKey>("domain");
|
||||||
|
|
||||||
const browsers = computed(() => response.value.browsers);
|
const browsers = computed(() => response.value.browsers);
|
||||||
const currentBrowser = computed<BrowserView | null>(
|
const currentBrowser = computed<BrowserView | null>(
|
||||||
@@ -65,6 +67,9 @@ export function useBrowserManager() {
|
|||||||
const sortedBookmarks = computed(() =>
|
const sortedBookmarks = computed(() =>
|
||||||
sortBookmarks(currentBrowser.value?.bookmarks ?? [], bookmarkSortKey.value),
|
sortBookmarks(currentBrowser.value?.bookmarks ?? [], bookmarkSortKey.value),
|
||||||
);
|
);
|
||||||
|
const sortedPasswordSites = computed(() =>
|
||||||
|
sortPasswordSites(currentBrowser.value?.passwordSites ?? [], passwordSiteSortKey.value),
|
||||||
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
browsers,
|
browsers,
|
||||||
@@ -280,7 +285,8 @@ export function useBrowserManager() {
|
|||||||
if (!currentBrowser.value) return 0;
|
if (!currentBrowser.value) return 0;
|
||||||
if (section === "profiles") return currentBrowser.value.profiles.length;
|
if (section === "profiles") return currentBrowser.value.profiles.length;
|
||||||
if (section === "extensions") return currentBrowser.value.extensions.length;
|
if (section === "extensions") return currentBrowser.value.extensions.length;
|
||||||
return currentBrowser.value.bookmarks.length;
|
if (section === "bookmarks") return currentBrowser.value.bookmarks.length;
|
||||||
|
return currentBrowser.value.passwordSites.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
function showExtensionProfilesModal(extensionId: string) {
|
function showExtensionProfilesModal(extensionId: string) {
|
||||||
@@ -305,6 +311,17 @@ export function useBrowserManager() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showPasswordSiteProfilesModal(url: string) {
|
||||||
|
const passwordSite = currentBrowser.value?.passwordSites.find((item) => item.url === url);
|
||||||
|
if (!passwordSite || !currentBrowser.value) return;
|
||||||
|
associatedProfilesModal.value = {
|
||||||
|
title: `${passwordSite.domain} Profiles`,
|
||||||
|
browserId: currentBrowser.value.browserId,
|
||||||
|
profiles: passwordSite.profiles,
|
||||||
|
isBookmark: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function closeAssociatedProfilesModal() {
|
function closeAssociatedProfilesModal() {
|
||||||
associatedProfilesModal.value = null;
|
associatedProfilesModal.value = null;
|
||||||
}
|
}
|
||||||
@@ -339,6 +356,7 @@ export function useBrowserManager() {
|
|||||||
page,
|
page,
|
||||||
pickExecutablePath,
|
pickExecutablePath,
|
||||||
pickUserDataPath,
|
pickUserDataPath,
|
||||||
|
passwordSiteSortKey,
|
||||||
profileSortKey,
|
profileSortKey,
|
||||||
refreshAll,
|
refreshAll,
|
||||||
savingConfig,
|
savingConfig,
|
||||||
@@ -346,8 +364,10 @@ export function useBrowserManager() {
|
|||||||
selectedBrowserId,
|
selectedBrowserId,
|
||||||
showBookmarkProfilesModal,
|
showBookmarkProfilesModal,
|
||||||
showExtensionProfilesModal,
|
showExtensionProfilesModal,
|
||||||
|
showPasswordSiteProfilesModal,
|
||||||
sortedBookmarks,
|
sortedBookmarks,
|
||||||
sortedExtensions,
|
sortedExtensions,
|
||||||
|
sortedPasswordSites,
|
||||||
sortedProfiles,
|
sortedProfiles,
|
||||||
closeAssociatedProfilesModal,
|
closeAssociatedProfilesModal,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export type BrowserStats = {
|
|||||||
profileCount: number;
|
profileCount: number;
|
||||||
extensionCount: number;
|
extensionCount: number;
|
||||||
bookmarkCount: number;
|
bookmarkCount: number;
|
||||||
|
passwordSiteCount: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ProfileSummary = {
|
export type ProfileSummary = {
|
||||||
@@ -32,6 +33,13 @@ export type BookmarkSummary = {
|
|||||||
profiles: BookmarkAssociatedProfileSummary[];
|
profiles: BookmarkAssociatedProfileSummary[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PasswordSiteSummary = {
|
||||||
|
url: string;
|
||||||
|
domain: string;
|
||||||
|
profileIds: string[];
|
||||||
|
profiles: AssociatedProfileSummary[];
|
||||||
|
};
|
||||||
|
|
||||||
export type AssociatedProfileSummary = {
|
export type AssociatedProfileSummary = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -56,8 +64,9 @@ export type BookmarkAssociatedProfileSummary = {
|
|||||||
export type ProfileSortKey = "name" | "email" | "id";
|
export type ProfileSortKey = "name" | "email" | "id";
|
||||||
export type ExtensionSortKey = "name" | "id";
|
export type ExtensionSortKey = "name" | "id";
|
||||||
export type BookmarkSortKey = "title" | "url";
|
export type BookmarkSortKey = "title" | "url";
|
||||||
|
export type PasswordSiteSortKey = "domain" | "url";
|
||||||
export type AssociatedProfileSortKey = "id" | "name";
|
export type AssociatedProfileSortKey = "id" | "name";
|
||||||
export type ActiveSection = "profiles" | "extensions" | "bookmarks";
|
export type ActiveSection = "profiles" | "extensions" | "bookmarks" | "passwords";
|
||||||
export type AppPage = "browserData" | "configuration";
|
export type AppPage = "browserData" | "configuration";
|
||||||
export type BrowserConfigSource = "default" | "custom";
|
export type BrowserConfigSource = "default" | "custom";
|
||||||
|
|
||||||
@@ -92,6 +101,7 @@ export type BrowserView = {
|
|||||||
profiles: ProfileSummary[];
|
profiles: ProfileSummary[];
|
||||||
extensions: ExtensionSummary[];
|
extensions: ExtensionSummary[];
|
||||||
bookmarks: BookmarkSummary[];
|
bookmarks: BookmarkSummary[];
|
||||||
|
passwordSites: PasswordSiteSummary[];
|
||||||
stats: BrowserStats;
|
stats: BrowserStats;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import type {
|
|||||||
BookmarkSummary,
|
BookmarkSummary,
|
||||||
ExtensionSortKey,
|
ExtensionSortKey,
|
||||||
ExtensionSummary,
|
ExtensionSummary,
|
||||||
|
PasswordSiteSortKey,
|
||||||
|
PasswordSiteSummary,
|
||||||
AssociatedProfileSortKey,
|
AssociatedProfileSortKey,
|
||||||
AssociatedProfileSummary,
|
AssociatedProfileSummary,
|
||||||
BookmarkAssociatedProfileSummary,
|
BookmarkAssociatedProfileSummary,
|
||||||
@@ -76,6 +78,16 @@ export function sortBookmarks(items: BookmarkSummary[], sortKey: BookmarkSortKey
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sortPasswordSites(items: PasswordSiteSummary[], sortKey: PasswordSiteSortKey) {
|
||||||
|
const passwordSites = [...items];
|
||||||
|
return passwordSites.sort((left, right) => {
|
||||||
|
if (sortKey === "url") {
|
||||||
|
return compareOptionalText(left.url, right.url) || compareText(left.domain, right.domain);
|
||||||
|
}
|
||||||
|
return compareOptionalText(left.domain, right.domain) || compareText(left.url, right.url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function sortAssociatedProfiles(
|
export function sortAssociatedProfiles(
|
||||||
items: (AssociatedProfileSummary | BookmarkAssociatedProfileSummary)[],
|
items: (AssociatedProfileSummary | BookmarkAssociatedProfileSummary)[],
|
||||||
sortKey: AssociatedProfileSortKey,
|
sortKey: AssociatedProfileSortKey,
|
||||||
|
|||||||
Reference in New Issue
Block a user