Compare commits

..

10 Commits

Author SHA1 Message Date
Julian Freeman
189d7c02f5 fix config ui 2026-04-16 21:50:32 -04:00
Julian Freeman
83f762435b support more browsers 2026-04-16 21:47:00 -04:00
Julian Freeman
ca649f700f remove some 2026-04-16 21:11:28 -04:00
Julian Freeman
81e16a184f remove ext versions 2026-04-16 20:59:54 -04:00
Julian Freeman
d2ffdf2954 fix ext bug 2026-04-16 20:56:19 -04:00
Julian Freeman
40a49da672 fix local ext fetch 2026-04-16 20:46:26 -04:00
Julian Freeman
5d5d9c3c52 fix ext fetch 2026-04-16 20:40:27 -04:00
Julian Freeman
309e2219f5 fix scrollbar 2026-04-16 20:25:16 -04:00
Julian Freeman
16eb25d552 fix table ui 2026-04-16 19:38:46 -04:00
Julian Freeman
c8e1641896 fix bug 2026-04-16 19:27:48 -04:00
16 changed files with 371 additions and 297 deletions

View File

@@ -73,6 +73,35 @@ pub fn browser_definitions() -> Vec<BrowserDefinition> {
]), ]),
], ],
}, },
BrowserDefinition {
id: "vivaldi",
name: "Vivaldi",
local_app_data_segments: &["Vivaldi", "User Data"],
executable_candidates: &[
ExecutableCandidate::LocalAppData(&["Vivaldi", "Application", "vivaldi.exe"]),
ExecutableCandidate::ProgramFiles(&["Vivaldi", "Application", "vivaldi.exe"]),
],
},
BrowserDefinition {
id: "yandex",
name: "Yandex Browser",
local_app_data_segments: &["Yandex", "YandexBrowser", "User Data"],
executable_candidates: &[ExecutableCandidate::LocalAppData(&[
"Yandex",
"YandexBrowser",
"Application",
"browser.exe",
])],
},
BrowserDefinition {
id: "chromium",
name: "Chromium",
local_app_data_segments: &["Chromium", "User Data"],
executable_candidates: &[
ExecutableCandidate::LocalAppData(&["Chromium", "Application", "chrome.exe"]),
ExecutableCandidate::ProgramFiles(&["Chromium", "Application", "chrome.exe"]),
],
},
] ]
} }

View File

@@ -205,6 +205,9 @@ fn infer_browser_family_id(icon_key: Option<&str>) -> Option<String> {
Some("chrome") => Some("chrome".to_string()), Some("chrome") => Some("chrome".to_string()),
Some("edge") => Some("edge".to_string()), Some("edge") => Some("edge".to_string()),
Some("brave") => Some("brave".to_string()), Some("brave") => Some("brave".to_string()),
Some("vivaldi") => Some("vivaldi".to_string()),
Some("yandex") => Some("yandex".to_string()),
Some("chromium") => Some("chromium".to_string()),
_ => None, _ => None,
} }
} }

View File

@@ -1,6 +1,5 @@
use std::{ use std::{
collections::{BTreeMap, BTreeSet}, collections::{BTreeMap, BTreeSet},
fs,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
@@ -14,7 +13,7 @@ use crate::{
BrowserConfigEntry, BrowserStats, BrowserView, ExtensionSummary, ProfileSummary, BrowserConfigEntry, BrowserStats, BrowserView, ExtensionSummary, ProfileSummary,
ScanResponse, TempBookmark, TempExtension, ScanResponse, TempBookmark, TempExtension,
}, },
utils::{first_non_empty, load_image_as_data_url, pick_latest_subdirectory, 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> {
@@ -39,13 +38,7 @@ fn scan_browser(config: BrowserConfigEntry) -> Option<BrowserView> {
.and_then(|value| value.get("info_cache")) .and_then(|value| value.get("info_cache"))
.and_then(Value::as_object); .and_then(Value::as_object);
let mut profile_ids = BTreeSet::new(); let profile_ids = collect_profile_ids_from_local_state(profile_cache);
collect_profile_ids_from_fs(&root, &mut profile_ids);
if let Some(cache) = profile_cache {
for profile_id in cache.keys() {
profile_ids.insert(profile_id.to_string());
}
}
let mut profiles = Vec::new(); let mut profiles = Vec::new();
let mut extensions = BTreeMap::<String, TempExtension>::new(); let mut extensions = BTreeMap::<String, TempExtension>::new();
@@ -108,23 +101,12 @@ fn scan_browser(config: BrowserConfigEntry) -> Option<BrowserView> {
}) })
} }
fn collect_profile_ids_from_fs(root: &Path, profile_ids: &mut BTreeSet<String>) { fn collect_profile_ids_from_local_state(
let Ok(entries) = fs::read_dir(root) else { profile_cache: Option<&serde_json::Map<String, Value>>,
return; ) -> BTreeSet<String> {
}; profile_cache
.map(|cache| cache.keys().cloned().collect())
for entry in entries.flatten() { .unwrap_or_default()
let Ok(file_type) = entry.file_type() else {
continue;
};
if !file_type.is_dir() {
continue;
}
let name = entry.file_name().to_string_lossy().to_string();
if name == "Default" || name.starts_with("Profile ") {
profile_ids.insert(name);
}
}
} }
fn build_profile_summary( fn build_profile_summary(
@@ -209,37 +191,48 @@ fn scan_extensions_for_profile(
profile: &ProfileSummary, profile: &ProfileSummary,
extensions: &mut BTreeMap<String, TempExtension>, extensions: &mut BTreeMap<String, TempExtension>,
) { ) {
let extensions_root = profile_path.join("Extensions"); let secure_preferences_path = profile_path.join("Secure Preferences");
let Ok(entries) = fs::read_dir(&extensions_root) else { let Some(secure_preferences) = read_json_file(&secure_preferences_path) else {
return; return;
}; };
for entry in entries.flatten() { let Some(extension_settings) = secure_preferences
let Ok(file_type) = entry.file_type() else { .get("extensions")
.and_then(|value| value.get("settings"))
.and_then(Value::as_object)
else {
return;
};
for (extension_id, extension_value) in extension_settings {
let Some((install_dir, install_source)) =
resolve_extension_install_dir(profile_path, extension_id, extension_value)
else {
continue; continue;
}; };
if !file_type.is_dir() {
continue; let external_manifest = match install_source {
ExtensionInstallSource::ExternalAbsolute => {
read_json_file(&install_dir.join("manifest.json"))
} }
ExtensionInstallSource::StoreRelative => None,
let extension_id = entry.file_name().to_string_lossy().to_string(); };
let Some(version_path) = pick_latest_subdirectory(&entry.path()) else { let manifest = match install_source {
ExtensionInstallSource::StoreRelative => extension_value.get("manifest"),
ExtensionInstallSource::ExternalAbsolute => external_manifest.as_ref(),
};
let Some(manifest) = manifest else {
continue; continue;
}; };
let manifest_path = version_path.join("manifest.json"); let name = resolve_extension_name(manifest, &install_dir)
let Some(manifest) = read_json_file(&manifest_path) else {
continue;
};
let name = resolve_extension_name(&manifest, &version_path)
.filter(|value| !value.is_empty()) .filter(|value| !value.is_empty())
.unwrap_or_else(|| extension_id.clone()); .unwrap_or_else(|| extension_id.clone());
let version = manifest let version = manifest
.get("version") .get("version")
.and_then(Value::as_str) .and_then(Value::as_str)
.map(str::to_string); .map(str::to_string);
let icon_data_url = resolve_extension_icon(&manifest, &version_path); let icon_data_url = resolve_extension_icon(manifest, &install_dir);
let entry = extensions let entry = extensions
.entry(extension_id.clone()) .entry(extension_id.clone())
@@ -252,7 +245,7 @@ fn scan_extensions_for_profile(
profiles: BTreeMap::new(), profiles: BTreeMap::new(),
}); });
if entry.name == entry.id && name != extension_id { if entry.name == entry.id && name != *extension_id {
entry.name = name.clone(); entry.name = name.clone();
} }
if entry.version.is_none() { if entry.version.is_none() {
@@ -277,6 +270,38 @@ fn scan_extensions_for_profile(
} }
} }
fn resolve_extension_install_dir(
profile_path: &Path,
extension_id: &str,
extension_value: &Value,
) -> Option<(PathBuf, ExtensionInstallSource)> {
let raw_path = extension_value
.get("path")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())?;
let normalized_path = raw_path.trim_start_matches('/');
let candidate = PathBuf::from(normalized_path);
let (resolved, source) = if normalized_path.starts_with(extension_id) {
(
profile_path.join("Extensions").join(candidate),
ExtensionInstallSource::StoreRelative,
)
} else if candidate.is_absolute() {
(candidate, ExtensionInstallSource::ExternalAbsolute)
} else {
(PathBuf::from(raw_path), ExtensionInstallSource::ExternalAbsolute)
};
resolved.is_dir().then_some((resolved, source))
}
enum ExtensionInstallSource {
StoreRelative,
ExternalAbsolute,
}
fn resolve_extension_name(manifest: &Value, version_path: &Path) -> Option<String> { fn resolve_extension_name(manifest: &Value, version_path: &Path) -> Option<String> {
let raw_name = manifest.get("name").and_then(Value::as_str)?; let raw_name = manifest.get("name").and_then(Value::as_str)?;
if let Some(localized_name) = resolve_localized_manifest_value(raw_name, manifest, version_path) if let Some(localized_name) = resolve_localized_manifest_value(raw_name, manifest, version_path)
@@ -350,9 +375,10 @@ fn resolve_extension_icon(manifest: &Value, version_path: &Path) -> Option<Strin
} }
candidates.sort_by(|left, right| right.0.cmp(&left.0)); candidates.sort_by(|left, right| right.0.cmp(&left.0));
candidates candidates.into_iter().find_map(|(_, relative_path)| {
.into_iter() let normalized_path = relative_path.trim_start_matches('/');
.find_map(|(_, relative_path)| load_image_as_data_url(&version_path.join(relative_path))) load_image_as_data_url(&version_path.join(normalized_path))
})
} }
fn icon_candidates_from_object(map: &serde_json::Map<String, Value>) -> Vec<(u32, String)> { fn icon_candidates_from_object(map: &serde_json::Map<String, Value>) -> Vec<(u32, String)> {

View File

@@ -14,29 +14,6 @@ pub fn local_app_data_dir() -> Option<PathBuf> {
}) })
} }
pub fn pick_latest_subdirectory(root: &Path) -> Option<PathBuf> {
let entries = fs::read_dir(root).ok()?;
let mut candidates = entries
.flatten()
.filter_map(|entry| {
let file_type = entry.file_type().ok()?;
if !file_type.is_dir() {
return None;
}
let metadata = entry.metadata().ok()?;
let modified = metadata.modified().ok();
Some((
modified,
entry.file_name().to_string_lossy().to_string(),
entry.path(),
))
})
.collect::<Vec<_>>();
candidates.sort_by(|left, right| right.0.cmp(&left.0).then_with(|| right.1.cmp(&left.1)));
candidates.into_iter().next().map(|(_, _, path)| path)
}
pub fn load_image_as_data_url(path: &Path) -> Option<String> { pub fn load_image_as_data_url(path: &Path) -> Option<String> {
let bytes = fs::read(path).ok()?; let bytes = fs::read(path).ok()?;
let extension = path let extension = path

BIN
src/assets/chromium.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
src/assets/vivaldi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
src/assets/yandex.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -50,8 +50,7 @@ function hasBookmarkPath(profile: ModalProfile): profile is BookmarkAssociatedPr
<div v-if="isBookmark" class="header-cell">Bookmark Path</div> <div v-if="isBookmark" class="header-cell">Bookmark Path</div>
<div class="header-cell actions-cell">Action</div> <div class="header-cell actions-cell">Action</div>
</div> </div>
<div class="modal-table-body styled-scrollbar">
<div class="modal-list">
<article <article
v-for="profile in sortedProfiles" v-for="profile in sortedProfiles"
:key="profile.id" :key="profile.id"
@@ -159,7 +158,14 @@ function hasBookmarkPath(profile: ModalProfile): profile is BookmarkAssociatedPr
.modal-table-header { .modal-table-header {
padding: 10px 14px; padding: 10px 14px;
border-bottom: 1px solid rgba(148, 163, 184, 0.14); border-bottom: 1px solid rgba(148, 163, 184, 0.14);
background: rgba(248, 250, 252, 0.82); background: rgba(248, 250, 252, 0.94);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
.modal-table-body {
min-height: 0;
overflow: auto;
} }
.header-cell { .header-cell {
@@ -180,13 +186,6 @@ function hasBookmarkPath(profile: ModalProfile): profile is BookmarkAssociatedPr
color: var(--text); color: var(--text);
} }
.modal-list {
display: flex;
flex-direction: column;
min-height: 0;
overflow: auto;
}
.modal-table-row { .modal-table-row {
padding: 12px 14px; padding: 12px 14px;
border-bottom: 1px solid rgba(148, 163, 184, 0.12); border-bottom: 1px solid rgba(148, 163, 184, 0.12);

View File

@@ -20,7 +20,7 @@ const emit = defineEmits<{
<button class="header-cell sortable" :class="{ active: sortKey === 'url' }" type="button" @click="emit('update:sortKey', 'url')">URL</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 class="header-cell actions-cell">Profiles</div>
</div> </div>
<div class="data-table-body styled-scrollbar">
<article v-for="bookmark in bookmarks" :key="bookmark.url" class="data-table-row bookmarks-grid"> <article v-for="bookmark in bookmarks" :key="bookmark.url" class="data-table-row bookmarks-grid">
<div class="row-cell primary-cell"> <div class="row-cell primary-cell">
<strong>{{ bookmark.title }}</strong> <strong>{{ bookmark.title }}</strong>
@@ -34,6 +34,7 @@ const emit = defineEmits<{
</div> </div>
</article> </article>
</div> </div>
</div>
<div v-else class="empty-card"> <div v-else class="empty-card">
<p>No bookmarks were discovered for this browser.</p> <p>No bookmarks were discovered for this browser.</p>
</div> </div>
@@ -43,17 +44,25 @@ const emit = defineEmits<{
<style scoped> <style scoped>
.table-section { .table-section {
padding: 0; padding: 0;
height: 100%;
min-height: 0;
} }
.data-table { .data-table {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 100%; height: 100%;
min-height: 0;
border: 1px solid rgba(148, 163, 184, 0.18); border: 1px solid rgba(148, 163, 184, 0.18);
border-radius: 22px; border-radius: 22px;
background: var(--panel); background: var(--panel);
box-shadow: var(--shadow); box-shadow: var(--shadow);
overflow: clip; overflow: hidden;
}
.data-table-body {
min-height: 0;
overflow: auto;
} }
.bookmarks-grid { .bookmarks-grid {

View File

@@ -163,4 +163,13 @@ const emit = defineEmits<{
background: linear-gradient(135deg, rgba(255, 255, 255, 0.96), rgba(232, 240, 255, 0.92)); background: linear-gradient(135deg, rgba(255, 255, 255, 0.96), rgba(232, 240, 255, 0.92));
box-shadow: 0 12px 24px rgba(15, 23, 42, 0.08); box-shadow: 0 12px 24px rgba(15, 23, 42, 0.08);
} }
.content-scroll-area {
display: flex;
}
.content-scroll-area > * {
flex: 1;
min-height: 0;
}
</style> </style>

View File

@@ -20,10 +20,9 @@ const emit = defineEmits<{
<div class="header-cell icon-cell">Icon</div> <div class="header-cell icon-cell">Icon</div>
<button class="header-cell sortable" :class="{ active: sortKey === 'name' }" type="button" @click="emit('update:sortKey', 'name')">Name</button> <button class="header-cell sortable" :class="{ active: sortKey === 'name' }" type="button" @click="emit('update:sortKey', 'name')">Name</button>
<button class="header-cell sortable" :class="{ active: sortKey === 'id' }" type="button" @click="emit('update:sortKey', 'id')">Extension ID</button> <button class="header-cell sortable" :class="{ active: sortKey === 'id' }" type="button" @click="emit('update:sortKey', 'id')">Extension ID</button>
<div class="header-cell">Version</div>
<div class="header-cell actions-cell">Profiles</div> <div class="header-cell actions-cell">Profiles</div>
</div> </div>
<div class="data-table-body styled-scrollbar">
<article v-for="extension in extensions" :key="extension.id" class="data-table-row extensions-grid"> <article v-for="extension in extensions" :key="extension.id" class="data-table-row extensions-grid">
<div class="extension-icon table-icon" :class="{ filled: Boolean(extension.iconDataUrl) }"> <div class="extension-icon table-icon" :class="{ filled: Boolean(extension.iconDataUrl) }">
<img v-if="extension.iconDataUrl" :src="extension.iconDataUrl" :alt="`${extension.name} icon`" /> <img v-if="extension.iconDataUrl" :src="extension.iconDataUrl" :alt="`${extension.name} icon`" />
@@ -33,10 +32,6 @@ const emit = defineEmits<{
<strong>{{ extension.name }}</strong> <strong>{{ extension.name }}</strong>
</div> </div>
<div class="row-cell muted-cell" :title="extension.id">{{ extension.id }}</div> <div class="row-cell muted-cell" :title="extension.id">{{ extension.id }}</div>
<div class="row-cell">
<span v-if="extension.version" class="badge neutral">v{{ extension.version }}</span>
<span v-else class="muted-cell">-</span>
</div>
<div class="row-cell actions-cell"> <div class="row-cell actions-cell">
<button class="disclosure-button" type="button" @click="emit('showProfiles', extension.id)"> <button class="disclosure-button" type="button" @click="emit('showProfiles', extension.id)">
<span>View</span> <span>View</span>
@@ -45,6 +40,7 @@ const emit = defineEmits<{
</div> </div>
</article> </article>
</div> </div>
</div>
<div v-else class="empty-card"> <div v-else class="empty-card">
<p>No extensions were discovered for this browser.</p> <p>No extensions were discovered for this browser.</p>
</div> </div>
@@ -54,22 +50,30 @@ const emit = defineEmits<{
<style scoped> <style scoped>
.table-section { .table-section {
padding: 0; padding: 0;
height: 100%;
min-height: 0;
} }
.data-table { .data-table {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 100%; height: 100%;
min-height: 0;
border: 1px solid rgba(148, 163, 184, 0.18); border: 1px solid rgba(148, 163, 184, 0.18);
border-radius: 22px; border-radius: 22px;
background: var(--panel); background: var(--panel);
box-shadow: var(--shadow); box-shadow: var(--shadow);
overflow: clip; overflow: hidden;
}
.data-table-body {
min-height: 0;
overflow: auto;
} }
.extensions-grid { .extensions-grid {
display: grid; display: grid;
grid-template-columns: 60px minmax(180px, 1.1fr) minmax(220px, 1fr) 108px 154px; grid-template-columns: 60px minmax(180px, 1.1fr) minmax(220px, 1fr) 154px;
gap: 12px; gap: 12px;
align-items: center; align-items: center;
} }
@@ -185,7 +189,7 @@ const emit = defineEmits<{
@media (max-width: 900px) { @media (max-width: 900px) {
.extensions-grid { .extensions-grid {
grid-template-columns: 56px minmax(160px, 1fr) minmax(160px, 1fr) 96px 148px; grid-template-columns: 56px minmax(160px, 1fr) minmax(160px, 1fr) 148px;
} }
} }
@@ -194,8 +198,7 @@ const emit = defineEmits<{
grid-template-columns: 56px minmax(0, 1fr) 132px; grid-template-columns: 56px minmax(0, 1fr) 132px;
} }
.extensions-grid > :nth-child(3), .extensions-grid > :nth-child(3) {
.extensions-grid > :nth-child(4) {
display: none; display: none;
} }
} }

View File

@@ -31,7 +31,7 @@ const emit = defineEmits<{
<button class="header-cell sortable" :class="{ active: sortKey === 'id' }" type="button" @click="emit('update:sortKey', 'id')">Profile ID</button> <button class="header-cell sortable" :class="{ active: sortKey === 'id' }" type="button" @click="emit('update:sortKey', 'id')">Profile ID</button>
<div class="header-cell actions-cell">Action</div> <div class="header-cell actions-cell">Action</div>
</div> </div>
<div class="data-table-body styled-scrollbar">
<article v-for="profile in profiles" :key="profile.id" class="data-table-row profiles-grid"> <article v-for="profile in profiles" :key="profile.id" class="data-table-row profiles-grid">
<div class="profile-avatar table-avatar"> <div class="profile-avatar table-avatar">
<img <img
@@ -62,6 +62,7 @@ const emit = defineEmits<{
</div> </div>
</article> </article>
</div> </div>
</div>
<div v-else class="empty-card"> <div v-else class="empty-card">
<p>No profile directories were found for this browser.</p> <p>No profile directories were found for this browser.</p>
</div> </div>
@@ -71,17 +72,25 @@ const emit = defineEmits<{
<style scoped> <style scoped>
.table-section { .table-section {
padding: 0; padding: 0;
height: 100%;
min-height: 0;
} }
.data-table { .data-table {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 100%; height: 100%;
min-height: 0;
border: 1px solid rgba(148, 163, 184, 0.18); border: 1px solid rgba(148, 163, 184, 0.18);
border-radius: 22px; border-radius: 22px;
background: var(--panel); background: var(--panel);
box-shadow: var(--shadow); box-shadow: var(--shadow);
overflow: clip; overflow: hidden;
}
.data-table-body {
min-height: 0;
overflow: auto;
} }
.profiles-grid { .profiles-grid {

View File

@@ -37,8 +37,7 @@ const iconOptions = computed(() =>
</script> </script>
<template> <template>
<section class="content-scroll-area"> <section class="config-page styled-scrollbar">
<section class="content-section">
<div v-if="configError" class="inline-error"> <div v-if="configError" class="inline-error">
{{ configError }} {{ configError }}
</div> </div>
@@ -166,10 +165,18 @@ const iconOptions = computed(() =>
</article> </article>
</div> </div>
</section> </section>
</section>
</template> </template>
<style scoped> <style scoped>
.config-page {
display: flex;
flex-direction: column;
gap: 10px;
min-height: 0;
overflow: auto;
padding: 0 2px 0 0;
}
.config-form-card, .config-form-card,
.config-card { .config-card {
border-radius: 18px; border-radius: 18px;

View File

@@ -229,6 +229,9 @@ export function useBrowserManager() {
if (iconKey === "chrome") return "CH"; if (iconKey === "chrome") return "CH";
if (iconKey === "edge") return "ED"; if (iconKey === "edge") return "ED";
if (iconKey === "brave") return "BR"; if (iconKey === "brave") return "BR";
if (iconKey === "vivaldi") return "VI";
if (iconKey === "yandex") return "YA";
if (iconKey === "chromium") return "CR";
const name = current?.browserName?.trim() ?? ""; const name = current?.browserName?.trim() ?? "";
if (name) { if (name) {
@@ -248,6 +251,9 @@ export function useBrowserManager() {
if (iconKey === "chrome") return "CH"; if (iconKey === "chrome") return "CH";
if (iconKey === "edge") return "ED"; if (iconKey === "edge") return "ED";
if (iconKey === "brave") return "BR"; if (iconKey === "brave") return "BR";
if (iconKey === "vivaldi") return "VI";
if (iconKey === "yandex") return "YA";
if (iconKey === "chromium") return "CR";
const letters = config.name const letters = config.name
.trim() .trim()

View File

@@ -86,8 +86,8 @@
.content-scroll-area { .content-scroll-area {
min-height: 0; min-height: 0;
overflow: auto; overflow: hidden;
padding-right: 2px; padding-right: 0;
} }
.count-pill, .count-pill,
@@ -175,23 +175,24 @@
} }
.browser-nav, .browser-nav,
.content-scroll-area { .styled-scrollbar {
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: rgba(100, 116, 139, 0.42) transparent; scrollbar-color: rgba(100, 116, 139, 0.42) transparent;
} }
.content-scroll-area::-webkit-scrollbar { .styled-scrollbar::-webkit-scrollbar {
width: 10px; width: 10px;
height: 10px;
} }
.content-scroll-area::-webkit-scrollbar-track { .styled-scrollbar::-webkit-scrollbar-track {
background: transparent; background: transparent;
} }
.content-scroll-area::-webkit-scrollbar-thumb { .styled-scrollbar::-webkit-scrollbar-thumb {
border: 3px solid transparent; border: 3px solid transparent;
border-radius: 999px; border-radius: 999px;
background: linear-gradient(180deg, rgba(148, 163, 184, 0.72), rgba(100, 116, 139, 0.58)); background: linear-gradient(180deg, rgba(148, 163, 184, 0.72), rgba(100, 116, 139, 0.58));
background-clip: padding-box; background-clip: padding-box;
} }
.content-scroll-area::-webkit-scrollbar-thumb:hover { .styled-scrollbar::-webkit-scrollbar-thumb:hover {
background: linear-gradient(180deg, rgba(100, 116, 139, 0.82), rgba(71, 85, 105, 0.72)); background: linear-gradient(180deg, rgba(100, 116, 139, 0.82), rgba(71, 85, 105, 0.72));
background-clip: padding-box; background-clip: padding-box;
} }
@@ -200,25 +201,6 @@
background: linear-gradient(180deg, rgba(254, 242, 242, 0.92), rgba(255, 255, 255, 0.86)); background: linear-gradient(180deg, rgba(254, 242, 242, 0.92), rgba(255, 255, 255, 0.86));
} }
@media (max-width: 1180px) {
.app-shell {
grid-template-columns: 1fr;
height: auto;
min-height: 100vh;
padding: 12px;
overflow: visible;
}
.content-panel {
padding: 0;
overflow: visible;
}
.content-scroll-area {
overflow: visible;
}
}
@media (max-width: 720px) { @media (max-width: 720px) {
.sort-bar { .sort-bar {
justify-content: stretch; justify-content: stretch;

View File

@@ -1,7 +1,10 @@
import braveIcon from "../assets/brave.png"; import braveIcon from "../assets/brave.png";
import chromiumIcon from "../assets/chromium.png";
import chromeIcon from "../assets/google-chrome.png"; import chromeIcon from "../assets/google-chrome.png";
import edgeIcon from "../assets/microsoft-edge.png"; import edgeIcon from "../assets/microsoft-edge.png";
import settingsIcon from "../assets/settings.png"; import settingsIcon from "../assets/settings.png";
import vivaldiIcon from "../assets/vivaldi.png";
import yandexIcon from "../assets/yandex.png";
import type { import type {
AssociatedProfileSummary, AssociatedProfileSummary,
BookmarkAssociatedProfileSummary, BookmarkAssociatedProfileSummary,
@@ -12,6 +15,9 @@ export const browserIconOptions = [
{ key: "chrome", label: "Google Chrome", src: chromeIcon }, { key: "chrome", label: "Google Chrome", src: chromeIcon },
{ key: "edge", label: "Microsoft Edge", src: edgeIcon }, { key: "edge", label: "Microsoft Edge", src: edgeIcon },
{ key: "brave", label: "Brave", src: braveIcon }, { key: "brave", label: "Brave", src: braveIcon },
{ key: "vivaldi", label: "Vivaldi", src: vivaldiIcon },
{ key: "yandex", label: "Yandex Browser", src: yandexIcon },
{ key: "chromium", label: "Chromium", src: chromiumIcon },
] as const; ] as const;
export function browserIconSrc(iconKey: string | null | undefined) { export function browserIconSrc(iconKey: string | null | undefined) {
@@ -83,15 +89,16 @@ export function profileAvatarSrc(
return profile.avatarDataUrl; return profile.avatarDataUrl;
} }
const avatarFamilyId = resolveAvatarFamilyId(browserFamilyId);
const avatarKey = normalizeAvatarIcon(profile.avatarIcon); const avatarKey = normalizeAvatarIcon(profile.avatarIcon);
if (avatarKey) { if (avatarKey) {
const familyMap = browserFamilyId ? avatarMap[browserFamilyId] : undefined; const familyMap = avatarFamilyId ? avatarMap[avatarFamilyId] : undefined;
if (familyMap?.[avatarKey]) { if (familyMap?.[avatarKey]) {
return familyMap[avatarKey]; return familyMap[avatarKey];
} }
} }
if (browserFamilyId === "chrome") { if (avatarFamilyId === "chrome") {
return createChromeGeneratedAvatar( return createChromeGeneratedAvatar(
profile.defaultAvatarFillColor, profile.defaultAvatarFillColor,
profile.defaultAvatarStrokeColor, profile.defaultAvatarStrokeColor,
@@ -110,6 +117,14 @@ function normalizeAvatarIcon(value: string | null | undefined) {
return lastSegment.replace(/\.png$/i, ""); return lastSegment.replace(/\.png$/i, "");
} }
function resolveAvatarFamilyId(browserFamilyId: string | null | undefined) {
if (browserFamilyId === "chromium") {
return "chrome";
}
return browserFamilyId ?? null;
}
function createChromeGeneratedAvatar( function createChromeGeneratedAvatar(
backgroundArgb: number | null | undefined, backgroundArgb: number | null | undefined,
foregroundArgb: number | null | undefined, foregroundArgb: number | null | undefined,