support browser icons

This commit is contained in:
Julian Freeman
2026-04-16 15:25:57 -04:00
parent ac5bedd73f
commit 97d156f606
11 changed files with 140 additions and 26 deletions

View File

@@ -35,6 +35,7 @@ pub fn resolve_browser_configs(app: &AppHandle) -> Result<Vec<BrowserConfigEntry
id: config.id,
source: BrowserConfigSource::Custom,
browser_family_id: None,
icon_key: config.icon_key,
name: config.name,
executable_path: config.executable_path,
user_data_path: config.user_data_path,
@@ -50,6 +51,14 @@ pub fn create_custom_browser_config(
input: CreateCustomBrowserConfigInput,
) -> Result<BrowserConfigListResponse, String> {
let name = input.name.trim();
let icon_key = input.icon_key.and_then(|value| {
let trimmed = value.trim().to_string();
if trimmed.is_empty() {
None
} else {
Some(trimmed)
}
});
let executable_path = input.executable_path.trim();
let user_data_path = input.user_data_path.trim();
@@ -67,6 +76,7 @@ pub fn create_custom_browser_config(
stored.custom_configs.push(CustomBrowserConfigRecord {
id: generate_custom_config_id(),
name: name.to_string(),
icon_key,
executable_path: executable_path.to_string(),
user_data_path: user_data_path.to_string(),
});
@@ -117,6 +127,7 @@ fn default_browser_configs() -> Result<Vec<BrowserConfigEntry>, String> {
id: definition.id.to_string(),
source: BrowserConfigSource::Default,
browser_family_id: Some(definition.id.to_string()),
icon_key: Some(definition.id.to_string()),
name: definition.name.to_string(),
executable_path: resolve_browser_executable(definition.id)
.map(|path| path.display().to_string())

View File

@@ -14,6 +14,7 @@ pub struct BrowserView {
pub browser_id: String,
pub browser_family_id: Option<String>,
pub browser_name: String,
pub icon_key: Option<String>,
pub data_root: String,
pub profiles: Vec<ProfileSummary>,
pub extensions: Vec<ExtensionSummary>,
@@ -70,6 +71,7 @@ pub struct BrowserConfigEntry {
pub id: String,
pub source: BrowserConfigSource,
pub browser_family_id: Option<String>,
pub icon_key: Option<String>,
pub name: String,
pub executable_path: String,
pub user_data_path: String,
@@ -87,6 +89,7 @@ pub enum BrowserConfigSource {
#[serde(rename_all = "camelCase")]
pub struct CreateCustomBrowserConfigInput {
pub name: String,
pub icon_key: Option<String>,
pub executable_path: String,
pub user_data_path: String,
}
@@ -102,6 +105,8 @@ pub struct StoredBrowserConfigs {
pub struct CustomBrowserConfigRecord {
pub id: String,
pub name: String,
#[serde(default)]
pub icon_key: Option<String>,
pub executable_path: String,
pub user_data_path: String,
}

View File

@@ -91,6 +91,7 @@ fn scan_browser(config: BrowserConfigEntry) -> Option<BrowserView> {
browser_id: config.id,
browser_family_id: config.browser_family_id,
browser_name: config.name,
icon_key: config.icon_key,
data_root: root.display().to_string(),
stats: BrowserStats {
profile_count: profiles.len(),

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import SortDropdown from "./components/SortDropdown.vue";
import { browserIconOptions, browserIconSrc } from "./features/browser-assistant/icons";
import { useBrowserAssistant } from "./features/browser-assistant/useBrowserAssistant";
const {
@@ -61,7 +62,14 @@ const {
type="button"
@click="selectedBrowserId = browser.browserId; page = 'browserData'"
>
<div class="browser-nav-icon">{{ browserMonogram(browser.browserId) }}</div>
<div class="browser-nav-icon">
<img
v-if="browserIconSrc(browser.iconKey ?? browser.browserFamilyId)"
:src="browserIconSrc(browser.iconKey ?? browser.browserFamilyId) ?? undefined"
:alt="`${browser.browserName} icon`"
/>
<span v-else>{{ browserMonogram(browser.browserId) }}</span>
</div>
<div class="browser-nav-body">
<strong>{{ browser.browserName }}</strong>
<span>{{ browser.dataRoot }}</span>
@@ -109,6 +117,22 @@ const {
<span>Name</span>
<input v-model="createConfigForm.name" placeholder="Work Chrome" />
</label>
<label class="field-group">
<span>Icon</span>
<div class="icon-option-grid">
<button
v-for="option in browserIconOptions"
:key="option.key"
class="icon-option-button"
:class="{ active: createConfigForm.iconKey === option.key }"
type="button"
@click="createConfigForm.iconKey = option.key"
>
<img :src="option.src" :alt="option.label" />
<span>{{ option.label }}</span>
</button>
</div>
</label>
<label class="field-group">
<span>Executable Path</span>
<div class="path-input-row">
@@ -166,7 +190,12 @@ const {
<div class="config-card-header">
<div class="config-card-lead">
<div class="browser-nav-icon config-icon">
{{ configMonogram(config) }}
<img
v-if="browserIconSrc(config.iconKey ?? config.browserFamilyId)"
:src="browserIconSrc(config.iconKey ?? config.browserFamilyId) ?? undefined"
:alt="`${config.name} icon`"
/>
<span v-else>{{ configMonogram(config) }}</span>
</div>
<div>
<div class="config-title-row">
@@ -175,7 +204,6 @@ const {
{{ config.source === "default" ? "Default" : "Custom" }}
</span>
</div>
<p class="config-id">{{ config.id }}</p>
</div>
</div>
<button

BIN
src/assets/brave.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

View File

@@ -0,0 +1,13 @@
import braveIcon from "../../assets/brave.png";
import chromeIcon from "../../assets/google-chrome.png";
import edgeIcon from "../../assets/microsoft-edge.png";
export const browserIconOptions = [
{ key: "chrome", label: "Google Chrome", src: chromeIcon },
{ key: "edge", label: "Microsoft Edge", src: edgeIcon },
{ key: "brave", label: "Brave", src: braveIcon },
] as const;
export function browserIconSrc(iconKey: string | null | undefined) {
return browserIconOptions.find((option) => option.key === iconKey)?.src ?? null;
}

View File

@@ -38,6 +38,7 @@ export type BrowserConfigEntry = {
id: string;
source: BrowserConfigSource;
browserFamilyId: string | null;
iconKey: string | null;
name: string;
executablePath: string;
userDataPath: string;
@@ -50,6 +51,7 @@ export type BrowserConfigListResponse = {
export type CreateCustomBrowserConfigInput = {
name: string;
iconKey: string | null;
executablePath: string;
userDataPath: string;
};
@@ -58,6 +60,7 @@ export type BrowserView = {
browserId: string;
browserFamilyId: string | null;
browserName: string;
iconKey: string | null;
dataRoot: string;
profiles: ProfileSummary[];
extensions: ExtensionSummary[];

View File

@@ -30,6 +30,7 @@ export function useBrowserAssistant() {
const deletingConfigId = ref("");
const createConfigForm = ref<CreateCustomBrowserConfigInput>({
name: "",
iconKey: "chrome",
executablePath: "",
userDataPath: "",
});
@@ -150,6 +151,7 @@ export function useBrowserAssistant() {
browserConfigs.value = result.configs;
createConfigForm.value = {
name: "",
iconKey: "chrome",
executablePath: "",
userDataPath: "",
};
@@ -218,10 +220,10 @@ export function useBrowserAssistant() {
function browserMonogram(browserId: string) {
const current = browsers.value.find((browser) => browser.browserId === browserId);
const familyId = current?.browserFamilyId;
if (familyId === "chrome") return "CH";
if (familyId === "edge") return "ED";
if (familyId === "brave") return "BR";
const iconKey = current?.iconKey ?? current?.browserFamilyId;
if (iconKey === "chrome") return "CH";
if (iconKey === "edge") return "ED";
if (iconKey === "brave") return "BR";
const name = current?.browserName?.trim() ?? "";
if (name) {
@@ -237,9 +239,10 @@ export function useBrowserAssistant() {
}
function configMonogram(config: BrowserConfigEntry) {
if (config.browserFamilyId === "chrome") return "CH";
if (config.browserFamilyId === "edge") return "ED";
if (config.browserFamilyId === "brave") return "BR";
const iconKey = config.iconKey ?? config.browserFamilyId;
if (iconKey === "chrome") return "CH";
if (iconKey === "edge") return "ED";
if (iconKey === "brave") return "BR";
const letters = config.name
.trim()

View File

@@ -222,6 +222,13 @@ button {
background: linear-gradient(135deg, #10213f, #365f9f);
}
.browser-nav-icon img,
.config-icon img {
width: 100%;
height: 100%;
object-fit: cover;
}
.browser-nav-item.chrome .browser-nav-icon {
--accent: var(--chrome);
}
@@ -315,7 +322,7 @@ button {
.config-form-card,
.config-card {
border-radius: 18px;
padding: 14px;
padding: 12px;
border: 1px solid rgba(148, 163, 184, 0.18);
background: var(--panel-strong);
}
@@ -323,22 +330,61 @@ button {
.config-form-header h3,
.config-title-row h4 {
margin: 0;
font-size: 0.98rem;
font-size: 0.94rem;
}
.config-form-header p,
.config-id,
.config-meta-row p {
margin: 6px 0 0;
color: var(--muted);
font-size: 0.86rem;
font-size: 0.82rem;
}
.config-form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
margin-top: 14px;
gap: 10px;
margin-top: 12px;
}
.icon-option-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
}
.icon-option-button {
display: grid;
justify-items: center;
gap: 6px;
padding: 10px 8px;
border: 1px solid rgba(148, 163, 184, 0.2);
border-radius: 12px;
background: rgba(255, 255, 255, 0.9);
color: var(--text);
cursor: pointer;
transition:
border-color 160ms ease,
background 160ms ease,
box-shadow 160ms ease;
}
.icon-option-button img {
width: 28px;
height: 28px;
object-fit: contain;
}
.icon-option-button span {
font-size: 0.76rem;
font-weight: 600;
text-align: center;
}
.icon-option-button.active {
border-color: rgba(47, 111, 237, 0.3);
background: rgba(232, 240, 255, 0.8);
box-shadow: 0 0 0 3px rgba(47, 111, 237, 0.1);
}
.field-group {
@@ -361,7 +407,7 @@ button {
.field-group input {
width: 100%;
padding: 10px 12px;
padding: 9px 11px;
border: 1px solid rgba(148, 163, 184, 0.24);
border-radius: 12px;
background: rgba(255, 255, 255, 0.94);
@@ -381,7 +427,7 @@ button {
.config-form-actions {
display: flex;
justify-content: flex-end;
margin-top: 14px;
margin-top: 12px;
}
.primary-button,
@@ -423,32 +469,32 @@ button {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
gap: 10px;
}
.config-card-lead {
display: flex;
align-items: flex-start;
gap: 12px;
gap: 10px;
min-width: 0;
}
.config-icon {
width: 40px;
height: 40px;
border-radius: 12px;
width: 36px;
height: 36px;
border-radius: 11px;
font-size: 0.8rem;
}
.config-meta {
display: grid;
gap: 10px;
margin-top: 12px;
gap: 8px;
margin-top: 10px;
}
.config-meta-row {
display: grid;
gap: 4px;
gap: 3px;
}
.config-meta-row p {
@@ -861,6 +907,10 @@ button {
grid-column: auto;
}
.icon-option-grid {
grid-template-columns: 1fr;
}
.sort-bar {
justify-content: stretch;
}