support browser icons
This commit is contained in:
@@ -35,6 +35,7 @@ pub fn resolve_browser_configs(app: &AppHandle) -> Result<Vec<BrowserConfigEntry
|
|||||||
id: config.id,
|
id: config.id,
|
||||||
source: BrowserConfigSource::Custom,
|
source: BrowserConfigSource::Custom,
|
||||||
browser_family_id: None,
|
browser_family_id: None,
|
||||||
|
icon_key: config.icon_key,
|
||||||
name: config.name,
|
name: config.name,
|
||||||
executable_path: config.executable_path,
|
executable_path: config.executable_path,
|
||||||
user_data_path: config.user_data_path,
|
user_data_path: config.user_data_path,
|
||||||
@@ -50,6 +51,14 @@ pub fn create_custom_browser_config(
|
|||||||
input: CreateCustomBrowserConfigInput,
|
input: CreateCustomBrowserConfigInput,
|
||||||
) -> Result<BrowserConfigListResponse, String> {
|
) -> Result<BrowserConfigListResponse, String> {
|
||||||
let name = input.name.trim();
|
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 executable_path = input.executable_path.trim();
|
||||||
let user_data_path = input.user_data_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 {
|
stored.custom_configs.push(CustomBrowserConfigRecord {
|
||||||
id: generate_custom_config_id(),
|
id: generate_custom_config_id(),
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
|
icon_key,
|
||||||
executable_path: executable_path.to_string(),
|
executable_path: executable_path.to_string(),
|
||||||
user_data_path: user_data_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(),
|
id: definition.id.to_string(),
|
||||||
source: BrowserConfigSource::Default,
|
source: BrowserConfigSource::Default,
|
||||||
browser_family_id: Some(definition.id.to_string()),
|
browser_family_id: Some(definition.id.to_string()),
|
||||||
|
icon_key: Some(definition.id.to_string()),
|
||||||
name: definition.name.to_string(),
|
name: definition.name.to_string(),
|
||||||
executable_path: resolve_browser_executable(definition.id)
|
executable_path: resolve_browser_executable(definition.id)
|
||||||
.map(|path| path.display().to_string())
|
.map(|path| path.display().to_string())
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ pub struct BrowserView {
|
|||||||
pub browser_id: String,
|
pub browser_id: String,
|
||||||
pub browser_family_id: Option<String>,
|
pub browser_family_id: Option<String>,
|
||||||
pub browser_name: String,
|
pub browser_name: String,
|
||||||
|
pub icon_key: Option<String>,
|
||||||
pub data_root: String,
|
pub data_root: String,
|
||||||
pub profiles: Vec<ProfileSummary>,
|
pub profiles: Vec<ProfileSummary>,
|
||||||
pub extensions: Vec<ExtensionSummary>,
|
pub extensions: Vec<ExtensionSummary>,
|
||||||
@@ -70,6 +71,7 @@ pub struct BrowserConfigEntry {
|
|||||||
pub id: String,
|
pub id: String,
|
||||||
pub source: BrowserConfigSource,
|
pub source: BrowserConfigSource,
|
||||||
pub browser_family_id: Option<String>,
|
pub browser_family_id: Option<String>,
|
||||||
|
pub icon_key: Option<String>,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub executable_path: String,
|
pub executable_path: String,
|
||||||
pub user_data_path: String,
|
pub user_data_path: String,
|
||||||
@@ -87,6 +89,7 @@ pub enum BrowserConfigSource {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct CreateCustomBrowserConfigInput {
|
pub struct CreateCustomBrowserConfigInput {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
pub icon_key: Option<String>,
|
||||||
pub executable_path: String,
|
pub executable_path: String,
|
||||||
pub user_data_path: String,
|
pub user_data_path: String,
|
||||||
}
|
}
|
||||||
@@ -102,6 +105,8 @@ pub struct StoredBrowserConfigs {
|
|||||||
pub struct CustomBrowserConfigRecord {
|
pub struct CustomBrowserConfigRecord {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub icon_key: Option<String>,
|
||||||
pub executable_path: String,
|
pub executable_path: String,
|
||||||
pub user_data_path: String,
|
pub user_data_path: String,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ fn scan_browser(config: BrowserConfigEntry) -> Option<BrowserView> {
|
|||||||
browser_id: config.id,
|
browser_id: config.id,
|
||||||
browser_family_id: config.browser_family_id,
|
browser_family_id: config.browser_family_id,
|
||||||
browser_name: config.name,
|
browser_name: config.name,
|
||||||
|
icon_key: config.icon_key,
|
||||||
data_root: root.display().to_string(),
|
data_root: root.display().to_string(),
|
||||||
stats: BrowserStats {
|
stats: BrowserStats {
|
||||||
profile_count: profiles.len(),
|
profile_count: profiles.len(),
|
||||||
|
|||||||
34
src/App.vue
34
src/App.vue
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import SortDropdown from "./components/SortDropdown.vue";
|
import SortDropdown from "./components/SortDropdown.vue";
|
||||||
|
import { browserIconOptions, browserIconSrc } from "./features/browser-assistant/icons";
|
||||||
import { useBrowserAssistant } from "./features/browser-assistant/useBrowserAssistant";
|
import { useBrowserAssistant } from "./features/browser-assistant/useBrowserAssistant";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -61,7 +62,14 @@ const {
|
|||||||
type="button"
|
type="button"
|
||||||
@click="selectedBrowserId = browser.browserId; page = 'browserData'"
|
@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">
|
<div class="browser-nav-body">
|
||||||
<strong>{{ browser.browserName }}</strong>
|
<strong>{{ browser.browserName }}</strong>
|
||||||
<span>{{ browser.dataRoot }}</span>
|
<span>{{ browser.dataRoot }}</span>
|
||||||
@@ -109,6 +117,22 @@ const {
|
|||||||
<span>Name</span>
|
<span>Name</span>
|
||||||
<input v-model="createConfigForm.name" placeholder="Work Chrome" />
|
<input v-model="createConfigForm.name" placeholder="Work Chrome" />
|
||||||
</label>
|
</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">
|
<label class="field-group">
|
||||||
<span>Executable Path</span>
|
<span>Executable Path</span>
|
||||||
<div class="path-input-row">
|
<div class="path-input-row">
|
||||||
@@ -166,7 +190,12 @@ const {
|
|||||||
<div class="config-card-header">
|
<div class="config-card-header">
|
||||||
<div class="config-card-lead">
|
<div class="config-card-lead">
|
||||||
<div class="browser-nav-icon config-icon">
|
<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>
|
<div>
|
||||||
<div class="config-title-row">
|
<div class="config-title-row">
|
||||||
@@ -175,7 +204,6 @@ const {
|
|||||||
{{ config.source === "default" ? "Default" : "Custom" }}
|
{{ config.source === "default" ? "Default" : "Custom" }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="config-id">{{ config.id }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
|||||||
BIN
src/assets/brave.png
Normal file
BIN
src/assets/brave.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
BIN
src/assets/google-chrome.png
Normal file
BIN
src/assets/google-chrome.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
BIN
src/assets/microsoft-edge.png
Normal file
BIN
src/assets/microsoft-edge.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
13
src/features/browser-assistant/icons.ts
Normal file
13
src/features/browser-assistant/icons.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -38,6 +38,7 @@ export type BrowserConfigEntry = {
|
|||||||
id: string;
|
id: string;
|
||||||
source: BrowserConfigSource;
|
source: BrowserConfigSource;
|
||||||
browserFamilyId: string | null;
|
browserFamilyId: string | null;
|
||||||
|
iconKey: string | null;
|
||||||
name: string;
|
name: string;
|
||||||
executablePath: string;
|
executablePath: string;
|
||||||
userDataPath: string;
|
userDataPath: string;
|
||||||
@@ -50,6 +51,7 @@ export type BrowserConfigListResponse = {
|
|||||||
|
|
||||||
export type CreateCustomBrowserConfigInput = {
|
export type CreateCustomBrowserConfigInput = {
|
||||||
name: string;
|
name: string;
|
||||||
|
iconKey: string | null;
|
||||||
executablePath: string;
|
executablePath: string;
|
||||||
userDataPath: string;
|
userDataPath: string;
|
||||||
};
|
};
|
||||||
@@ -58,6 +60,7 @@ export type BrowserView = {
|
|||||||
browserId: string;
|
browserId: string;
|
||||||
browserFamilyId: string | null;
|
browserFamilyId: string | null;
|
||||||
browserName: string;
|
browserName: string;
|
||||||
|
iconKey: string | null;
|
||||||
dataRoot: string;
|
dataRoot: string;
|
||||||
profiles: ProfileSummary[];
|
profiles: ProfileSummary[];
|
||||||
extensions: ExtensionSummary[];
|
extensions: ExtensionSummary[];
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export function useBrowserAssistant() {
|
|||||||
const deletingConfigId = ref("");
|
const deletingConfigId = ref("");
|
||||||
const createConfigForm = ref<CreateCustomBrowserConfigInput>({
|
const createConfigForm = ref<CreateCustomBrowserConfigInput>({
|
||||||
name: "",
|
name: "",
|
||||||
|
iconKey: "chrome",
|
||||||
executablePath: "",
|
executablePath: "",
|
||||||
userDataPath: "",
|
userDataPath: "",
|
||||||
});
|
});
|
||||||
@@ -150,6 +151,7 @@ export function useBrowserAssistant() {
|
|||||||
browserConfigs.value = result.configs;
|
browserConfigs.value = result.configs;
|
||||||
createConfigForm.value = {
|
createConfigForm.value = {
|
||||||
name: "",
|
name: "",
|
||||||
|
iconKey: "chrome",
|
||||||
executablePath: "",
|
executablePath: "",
|
||||||
userDataPath: "",
|
userDataPath: "",
|
||||||
};
|
};
|
||||||
@@ -218,10 +220,10 @@ export function useBrowserAssistant() {
|
|||||||
|
|
||||||
function browserMonogram(browserId: string) {
|
function browserMonogram(browserId: string) {
|
||||||
const current = browsers.value.find((browser) => browser.browserId === browserId);
|
const current = browsers.value.find((browser) => browser.browserId === browserId);
|
||||||
const familyId = current?.browserFamilyId;
|
const iconKey = current?.iconKey ?? current?.browserFamilyId;
|
||||||
if (familyId === "chrome") return "CH";
|
if (iconKey === "chrome") return "CH";
|
||||||
if (familyId === "edge") return "ED";
|
if (iconKey === "edge") return "ED";
|
||||||
if (familyId === "brave") return "BR";
|
if (iconKey === "brave") return "BR";
|
||||||
|
|
||||||
const name = current?.browserName?.trim() ?? "";
|
const name = current?.browserName?.trim() ?? "";
|
||||||
if (name) {
|
if (name) {
|
||||||
@@ -237,9 +239,10 @@ export function useBrowserAssistant() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function configMonogram(config: BrowserConfigEntry) {
|
function configMonogram(config: BrowserConfigEntry) {
|
||||||
if (config.browserFamilyId === "chrome") return "CH";
|
const iconKey = config.iconKey ?? config.browserFamilyId;
|
||||||
if (config.browserFamilyId === "edge") return "ED";
|
if (iconKey === "chrome") return "CH";
|
||||||
if (config.browserFamilyId === "brave") return "BR";
|
if (iconKey === "edge") return "ED";
|
||||||
|
if (iconKey === "brave") return "BR";
|
||||||
|
|
||||||
const letters = config.name
|
const letters = config.name
|
||||||
.trim()
|
.trim()
|
||||||
|
|||||||
@@ -222,6 +222,13 @@ button {
|
|||||||
background: linear-gradient(135deg, #10213f, #365f9f);
|
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 {
|
.browser-nav-item.chrome .browser-nav-icon {
|
||||||
--accent: var(--chrome);
|
--accent: var(--chrome);
|
||||||
}
|
}
|
||||||
@@ -315,7 +322,7 @@ button {
|
|||||||
.config-form-card,
|
.config-form-card,
|
||||||
.config-card {
|
.config-card {
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
padding: 14px;
|
padding: 12px;
|
||||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||||
background: var(--panel-strong);
|
background: var(--panel-strong);
|
||||||
}
|
}
|
||||||
@@ -323,22 +330,61 @@ button {
|
|||||||
.config-form-header h3,
|
.config-form-header h3,
|
||||||
.config-title-row h4 {
|
.config-title-row h4 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 0.98rem;
|
font-size: 0.94rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-form-header p,
|
.config-form-header p,
|
||||||
.config-id,
|
|
||||||
.config-meta-row p {
|
.config-meta-row p {
|
||||||
margin: 6px 0 0;
|
margin: 6px 0 0;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 0.86rem;
|
font-size: 0.82rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-form-grid {
|
.config-form-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
gap: 12px;
|
gap: 10px;
|
||||||
margin-top: 14px;
|
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 {
|
.field-group {
|
||||||
@@ -361,7 +407,7 @@ button {
|
|||||||
|
|
||||||
.field-group input {
|
.field-group input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px 12px;
|
padding: 9px 11px;
|
||||||
border: 1px solid rgba(148, 163, 184, 0.24);
|
border: 1px solid rgba(148, 163, 184, 0.24);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: rgba(255, 255, 255, 0.94);
|
background: rgba(255, 255, 255, 0.94);
|
||||||
@@ -381,7 +427,7 @@ button {
|
|||||||
.config-form-actions {
|
.config-form-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
margin-top: 14px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.primary-button,
|
.primary-button,
|
||||||
@@ -423,32 +469,32 @@ button {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 12px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-card-lead {
|
.config-card-lead {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 12px;
|
gap: 10px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-icon {
|
.config-icon {
|
||||||
width: 40px;
|
width: 36px;
|
||||||
height: 40px;
|
height: 36px;
|
||||||
border-radius: 12px;
|
border-radius: 11px;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-meta {
|
.config-meta {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
gap: 8px;
|
||||||
margin-top: 12px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-meta-row {
|
.config-meta-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 4px;
|
gap: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-meta-row p {
|
.config-meta-row p {
|
||||||
@@ -861,6 +907,10 @@ button {
|
|||||||
grid-column: auto;
|
grid-column: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-option-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.sort-bar {
|
.sort-bar {
|
||||||
justify-content: stretch;
|
justify-content: stretch;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user