Compare commits
10 Commits
e041523dbd
...
189d7c02f5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
189d7c02f5 | ||
|
|
83f762435b | ||
|
|
ca649f700f | ||
|
|
81e16a184f | ||
|
|
d2ffdf2954 | ||
|
|
40a49da672 | ||
|
|
5d5d9c3c52 | ||
|
|
309e2219f5 | ||
|
|
16eb25d552 | ||
|
|
c8e1641896 |
@@ -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"]),
|
||||||
|
],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)> {
|
||||||
|
|||||||
@@ -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
BIN
src/assets/chromium.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
BIN
src/assets/vivaldi.png
Normal file
BIN
src/assets/vivaldi.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
BIN
src/assets/yandex.png
Normal file
BIN
src/assets/yandex.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
@@ -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);
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user