support custom userdata
This commit is contained in:
179
src/App.vue
179
src/App.vue
@@ -6,20 +6,30 @@ const {
|
||||
activeSection,
|
||||
bookmarkProfilesExpanded,
|
||||
bookmarkSortKey,
|
||||
browserConfigs,
|
||||
browserMonogram,
|
||||
browsers,
|
||||
configError,
|
||||
configMonogram,
|
||||
configsLoading,
|
||||
createConfigForm,
|
||||
createCustomBrowserConfig,
|
||||
currentBrowser,
|
||||
deleteCustomBrowserConfig,
|
||||
domainFromUrl,
|
||||
error,
|
||||
extensionMonogram,
|
||||
extensionProfilesExpanded,
|
||||
extensionSortKey,
|
||||
isDeletingConfig,
|
||||
isOpeningProfile,
|
||||
loading,
|
||||
openProfileError,
|
||||
openBrowserProfile,
|
||||
page,
|
||||
profileSortKey,
|
||||
scanBrowsers,
|
||||
refreshAll,
|
||||
savingConfig,
|
||||
sectionCount,
|
||||
selectedBrowserId,
|
||||
sortedBookmarks,
|
||||
@@ -45,9 +55,9 @@ const {
|
||||
v-for="browser in browsers"
|
||||
:key="browser.browserId"
|
||||
class="browser-nav-item"
|
||||
:class="[browser.browserId, { active: browser.browserId === currentBrowser?.browserId }]"
|
||||
:class="[browser.browserFamilyId ?? browser.browserId, { active: browser.browserId === currentBrowser?.browserId }]"
|
||||
type="button"
|
||||
@click="selectedBrowserId = browser.browserId"
|
||||
@click="selectedBrowserId = browser.browserId; page = 'browserData'"
|
||||
>
|
||||
<div class="browser-nav-icon">{{ browserMonogram(browser.browserId) }}</div>
|
||||
<div class="browser-nav-body">
|
||||
@@ -61,23 +71,140 @@ const {
|
||||
<p>No supported Chromium browser data was found yet.</p>
|
||||
</div>
|
||||
|
||||
<button class="refresh-button sidebar-refresh" type="button" @click="scanBrowsers">
|
||||
{{ loading ? "Scanning..." : "Refresh" }}
|
||||
<button class="refresh-button sidebar-refresh" type="button" @click="refreshAll">
|
||||
{{ loading || configsLoading ? "Refreshing..." : "Refresh" }}
|
||||
</button>
|
||||
</aside>
|
||||
|
||||
<main class="content-panel">
|
||||
<section v-if="loading" class="state-panel">
|
||||
<p class="eyebrow">Scanning</p>
|
||||
<h2>Reading local browser data</h2>
|
||||
<p>Profiles, installed extensions, and bookmarks are being collected.</p>
|
||||
<section class="page-tabs">
|
||||
<button
|
||||
class="page-tab"
|
||||
:class="{ active: page === 'browserData' }"
|
||||
type="button"
|
||||
@click="page = 'browserData'"
|
||||
>
|
||||
Browser Data
|
||||
</button>
|
||||
<button
|
||||
class="page-tab"
|
||||
:class="{ active: page === 'configuration' }"
|
||||
type="button"
|
||||
@click="page = 'configuration'"
|
||||
>
|
||||
Configuration
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section v-else-if="error" class="state-panel error">
|
||||
<p class="eyebrow">Error</p>
|
||||
<h2>Scan failed</h2>
|
||||
<p>{{ error }}</p>
|
||||
</section>
|
||||
<template v-if="page === 'configuration'">
|
||||
<section class="content-scroll-area">
|
||||
<section class="content-section">
|
||||
<div v-if="configError" class="inline-error">
|
||||
{{ configError }}
|
||||
</div>
|
||||
|
||||
<div class="config-form-card">
|
||||
<div class="config-form-header">
|
||||
<h3>Add Custom Browser</h3>
|
||||
<p>Provide a name, executable path, and Chromium user data path.</p>
|
||||
</div>
|
||||
<div class="config-form-grid">
|
||||
<label class="field-group">
|
||||
<span>Name</span>
|
||||
<input v-model="createConfigForm.name" placeholder="Work Chrome" />
|
||||
</label>
|
||||
<label class="field-group">
|
||||
<span>Executable Path</span>
|
||||
<input
|
||||
v-model="createConfigForm.executablePath"
|
||||
placeholder="C:\Program Files\...\chrome.exe"
|
||||
/>
|
||||
</label>
|
||||
<label class="field-group field-span">
|
||||
<span>User Data Path</span>
|
||||
<input
|
||||
v-model="createConfigForm.userDataPath"
|
||||
placeholder="C:\Users\...\User Data"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="config-form-actions">
|
||||
<button
|
||||
class="primary-button"
|
||||
type="button"
|
||||
:disabled="savingConfig"
|
||||
@click="createCustomBrowserConfig"
|
||||
>
|
||||
{{ savingConfig ? "Saving..." : "Add Config" }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="configsLoading" class="empty-card">
|
||||
<p>Loading browser configs...</p>
|
||||
</div>
|
||||
<div v-else class="stack-list">
|
||||
<article
|
||||
v-for="config in browserConfigs"
|
||||
:key="config.id"
|
||||
class="config-card"
|
||||
>
|
||||
<div class="config-card-header">
|
||||
<div class="config-card-lead">
|
||||
<div class="browser-nav-icon config-icon">
|
||||
{{ configMonogram(config) }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="config-title-row">
|
||||
<h4>{{ config.name }}</h4>
|
||||
<span class="badge neutral">
|
||||
{{ config.source === "default" ? "Default" : "Custom" }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="config-id">{{ config.id }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
v-if="config.deletable"
|
||||
class="danger-button"
|
||||
type="button"
|
||||
:disabled="isDeletingConfig(config.id)"
|
||||
@click="deleteCustomBrowserConfig(config.id)"
|
||||
>
|
||||
{{ isDeletingConfig(config.id) ? "Deleting..." : "Delete" }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="config-meta">
|
||||
<div class="config-meta-row">
|
||||
<span class="config-label">Executable</span>
|
||||
<p :title="config.executablePath">{{ config.executablePath || "Not resolved" }}</p>
|
||||
</div>
|
||||
<div class="config-meta-row">
|
||||
<span class="config-label">User Data</span>
|
||||
<p :title="config.userDataPath">{{ config.userDataPath }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<template v-else-if="loading">
|
||||
<section class="state-panel">
|
||||
<p class="eyebrow">Scanning</p>
|
||||
<h2>Reading local browser data</h2>
|
||||
<p>Profiles, installed extensions, and bookmarks are being collected.</p>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<template v-else-if="error">
|
||||
<section class="state-panel error">
|
||||
<p class="eyebrow">Error</p>
|
||||
<h2>Scan failed</h2>
|
||||
<p>{{ error }}</p>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<template v-else-if="currentBrowser">
|
||||
<section class="section-tabs">
|
||||
@@ -129,11 +256,7 @@ const {
|
||||
</div>
|
||||
|
||||
<div v-if="sortedProfiles.length" class="stack-list">
|
||||
<article
|
||||
v-for="profile in sortedProfiles"
|
||||
:key="profile.id"
|
||||
class="profile-card"
|
||||
>
|
||||
<article v-for="profile in sortedProfiles" :key="profile.id" class="profile-card">
|
||||
<div class="profile-avatar">
|
||||
<img
|
||||
v-if="profile.avatarDataUrl"
|
||||
@@ -247,11 +370,7 @@ const {
|
||||
</div>
|
||||
|
||||
<div v-if="sortedBookmarks.length" class="bookmark-list">
|
||||
<article
|
||||
v-for="bookmark in sortedBookmarks"
|
||||
:key="bookmark.url"
|
||||
class="bookmark-card"
|
||||
>
|
||||
<article v-for="bookmark in sortedBookmarks" :key="bookmark.url" class="bookmark-card">
|
||||
<div class="bookmark-body">
|
||||
<div class="bookmark-topline">
|
||||
<h4>{{ bookmark.title }}</h4>
|
||||
@@ -290,11 +409,13 @@ const {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<section v-else class="state-panel">
|
||||
<p class="eyebrow">No Data</p>
|
||||
<h2>No supported browser was detected</h2>
|
||||
<p>Install or sign in to Chrome, Edge, or Brave and refresh the scan.</p>
|
||||
</section>
|
||||
<template v-else>
|
||||
<section class="state-panel">
|
||||
<p class="eyebrow">No Data</p>
|
||||
<h2>No supported browser was detected</h2>
|
||||
<p>Install or sign in to Chrome, Edge, or Brave and refresh the scan.</p>
|
||||
</section>
|
||||
</template>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -31,9 +31,32 @@ export type ProfileSortKey = "name" | "email" | "id";
|
||||
export type ExtensionSortKey = "name" | "id";
|
||||
export type BookmarkSortKey = "title" | "url";
|
||||
export type ActiveSection = "profiles" | "extensions" | "bookmarks";
|
||||
export type AppPage = "browserData" | "configuration";
|
||||
export type BrowserConfigSource = "default" | "custom";
|
||||
|
||||
export type BrowserConfigEntry = {
|
||||
id: string;
|
||||
source: BrowserConfigSource;
|
||||
browserFamilyId: string | null;
|
||||
name: string;
|
||||
executablePath: string;
|
||||
userDataPath: string;
|
||||
deletable: boolean;
|
||||
};
|
||||
|
||||
export type BrowserConfigListResponse = {
|
||||
configs: BrowserConfigEntry[];
|
||||
};
|
||||
|
||||
export type CreateCustomBrowserConfigInput = {
|
||||
name: string;
|
||||
executablePath: string;
|
||||
userDataPath: string;
|
||||
};
|
||||
|
||||
export type BrowserView = {
|
||||
browserId: string;
|
||||
browserFamilyId: string | null;
|
||||
browserName: string;
|
||||
dataRoot: string;
|
||||
profiles: ProfileSummary[];
|
||||
|
||||
@@ -4,19 +4,34 @@ import { invoke } from "@tauri-apps/api/core";
|
||||
import { sortBookmarks, sortExtensions, sortProfiles } from "./sort";
|
||||
import type {
|
||||
ActiveSection,
|
||||
AppPage,
|
||||
BookmarkSortKey,
|
||||
BrowserConfigEntry,
|
||||
BrowserConfigListResponse,
|
||||
BrowserView,
|
||||
CreateCustomBrowserConfigInput,
|
||||
ExtensionSortKey,
|
||||
ProfileSortKey,
|
||||
ScanResponse,
|
||||
} from "./types";
|
||||
|
||||
export function useBrowserAssistant() {
|
||||
const page = ref<AppPage>("browserData");
|
||||
const loading = ref(true);
|
||||
const error = ref("");
|
||||
const openProfileError = ref("");
|
||||
const openingProfileKey = ref("");
|
||||
const response = ref<ScanResponse>({ browsers: [] });
|
||||
const browserConfigs = ref<BrowserConfigEntry[]>([]);
|
||||
const configsLoading = ref(true);
|
||||
const configError = ref("");
|
||||
const savingConfig = ref(false);
|
||||
const deletingConfigId = ref("");
|
||||
const createConfigForm = ref<CreateCustomBrowserConfigInput>({
|
||||
name: "",
|
||||
executablePath: "",
|
||||
userDataPath: "",
|
||||
});
|
||||
const selectedBrowserId = ref("");
|
||||
const activeSection = ref<ActiveSection>("profiles");
|
||||
const expandedExtensionIds = ref<string[]>([]);
|
||||
@@ -65,8 +80,24 @@ export function useBrowserAssistant() {
|
||||
watch(selectedBrowserId, () => {
|
||||
expandedExtensionIds.value = [];
|
||||
expandedBookmarkUrls.value = [];
|
||||
openProfileError.value = "";
|
||||
});
|
||||
|
||||
async function loadBrowserConfigs() {
|
||||
configsLoading.value = true;
|
||||
configError.value = "";
|
||||
|
||||
try {
|
||||
const result = await invoke<BrowserConfigListResponse>("list_browser_configs");
|
||||
browserConfigs.value = result.configs;
|
||||
} catch (loadError) {
|
||||
configError.value =
|
||||
loadError instanceof Error ? loadError.message : "Failed to load browser configs.";
|
||||
} finally {
|
||||
configsLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function scanBrowsers() {
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
@@ -83,6 +114,10 @@ export function useBrowserAssistant() {
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAll() {
|
||||
await Promise.all([loadBrowserConfigs(), scanBrowsers()]);
|
||||
}
|
||||
|
||||
async function openBrowserProfile(browserId: string, profileId: string) {
|
||||
const profileKey = `${browserId}:${profileId}`;
|
||||
openingProfileKey.value = profileKey;
|
||||
@@ -103,17 +138,89 @@ export function useBrowserAssistant() {
|
||||
}
|
||||
}
|
||||
|
||||
async function createCustomBrowserConfig() {
|
||||
savingConfig.value = true;
|
||||
configError.value = "";
|
||||
|
||||
try {
|
||||
const result = await invoke<BrowserConfigListResponse>("create_custom_browser_config", {
|
||||
input: createConfigForm.value,
|
||||
});
|
||||
browserConfigs.value = result.configs;
|
||||
createConfigForm.value = {
|
||||
name: "",
|
||||
executablePath: "",
|
||||
userDataPath: "",
|
||||
};
|
||||
await scanBrowsers();
|
||||
} catch (saveError) {
|
||||
configError.value =
|
||||
saveError instanceof Error ? saveError.message : "Failed to create browser config.";
|
||||
} finally {
|
||||
savingConfig.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteCustomBrowserConfig(configId: string) {
|
||||
deletingConfigId.value = configId;
|
||||
configError.value = "";
|
||||
|
||||
try {
|
||||
const result = await invoke<BrowserConfigListResponse>("delete_custom_browser_config", {
|
||||
configId,
|
||||
});
|
||||
browserConfigs.value = result.configs;
|
||||
await scanBrowsers();
|
||||
} catch (deleteError) {
|
||||
configError.value =
|
||||
deleteError instanceof Error ? deleteError.message : "Failed to delete browser config.";
|
||||
} finally {
|
||||
deletingConfigId.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
function isDeletingConfig(configId: string) {
|
||||
return deletingConfigId.value === configId;
|
||||
}
|
||||
|
||||
function isOpeningProfile(browserId: string, profileId: string) {
|
||||
return openingProfileKey.value === `${browserId}:${profileId}`;
|
||||
}
|
||||
|
||||
function browserMonogram(browserId: string) {
|
||||
if (browserId === "chrome") return "CH";
|
||||
if (browserId === "edge") return "ED";
|
||||
if (browserId === "brave") return "BR";
|
||||
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 name = current?.browserName?.trim() ?? "";
|
||||
if (name) {
|
||||
const letters = name
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
.map((part) => part[0]);
|
||||
if (letters.length) return letters.join("").toUpperCase();
|
||||
}
|
||||
|
||||
return browserId.slice(0, 2).toUpperCase();
|
||||
}
|
||||
|
||||
function configMonogram(config: BrowserConfigEntry) {
|
||||
if (config.browserFamilyId === "chrome") return "CH";
|
||||
if (config.browserFamilyId === "edge") return "ED";
|
||||
if (config.browserFamilyId === "brave") return "BR";
|
||||
|
||||
const letters = config.name
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
.map((part) => part[0]);
|
||||
return (letters.join("") || config.id.slice(0, 2)).toUpperCase();
|
||||
}
|
||||
|
||||
function extensionMonogram(name: string) {
|
||||
return name.trim().slice(0, 1).toUpperCase() || "?";
|
||||
}
|
||||
@@ -154,27 +261,37 @@ export function useBrowserAssistant() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void scanBrowsers();
|
||||
void refreshAll();
|
||||
});
|
||||
|
||||
return {
|
||||
activeSection,
|
||||
bookmarkProfilesExpanded,
|
||||
bookmarkSortKey,
|
||||
browserConfigs,
|
||||
browserMonogram,
|
||||
browsers,
|
||||
configError,
|
||||
configMonogram,
|
||||
configsLoading,
|
||||
createConfigForm,
|
||||
createCustomBrowserConfig,
|
||||
currentBrowser,
|
||||
deleteCustomBrowserConfig,
|
||||
domainFromUrl,
|
||||
error,
|
||||
extensionMonogram,
|
||||
extensionProfilesExpanded,
|
||||
extensionSortKey,
|
||||
loading,
|
||||
isDeletingConfig,
|
||||
isOpeningProfile,
|
||||
loading,
|
||||
openProfileError,
|
||||
openBrowserProfile,
|
||||
page,
|
||||
profileSortKey,
|
||||
scanBrowsers,
|
||||
refreshAll,
|
||||
savingConfig,
|
||||
sectionCount,
|
||||
selectedBrowserId,
|
||||
sortedBookmarks,
|
||||
|
||||
163
src/styles.css
163
src/styles.css
@@ -276,6 +276,30 @@ button {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.page-tabs {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.page-tab {
|
||||
padding: 10px 14px;
|
||||
border-radius: 14px;
|
||||
background: rgba(255, 255, 255, 0.58);
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 160ms ease,
|
||||
color 160ms ease,
|
||||
box-shadow 160ms ease;
|
||||
}
|
||||
|
||||
.page-tab.active {
|
||||
color: var(--text);
|
||||
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);
|
||||
}
|
||||
|
||||
.section-tabs,
|
||||
.content-section,
|
||||
.state-panel {
|
||||
@@ -299,6 +323,137 @@ button {
|
||||
font-size: 0.86rem;
|
||||
}
|
||||
|
||||
.config-form-card,
|
||||
.config-card {
|
||||
border-radius: 18px;
|
||||
padding: 14px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||
background: var(--panel-strong);
|
||||
}
|
||||
|
||||
.config-form-header h3,
|
||||
.config-title-row h4 {
|
||||
margin: 0;
|
||||
font-size: 0.98rem;
|
||||
}
|
||||
|
||||
.config-form-header p,
|
||||
.config-id,
|
||||
.config-meta-row p {
|
||||
margin: 6px 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 0.86rem;
|
||||
}
|
||||
|
||||
.config-form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.field-group {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.field-group span,
|
||||
.config-label {
|
||||
color: var(--muted);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.field-group input {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.24);
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.field-group input:focus {
|
||||
border-color: rgba(47, 111, 237, 0.42);
|
||||
box-shadow: 0 0 0 3px rgba(47, 111, 237, 0.12);
|
||||
}
|
||||
|
||||
.field-span {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.config-form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.primary-button,
|
||||
.danger-button {
|
||||
padding: 9px 14px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.84rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.primary-button {
|
||||
background: linear-gradient(135deg, #10213f 0%, #213f75 100%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.danger-button {
|
||||
border: 1px solid rgba(239, 68, 68, 0.18);
|
||||
background: rgba(254, 242, 242, 0.96);
|
||||
color: #b42318;
|
||||
}
|
||||
|
||||
.primary-button:disabled,
|
||||
.danger-button:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.config-card-header,
|
||||
.config-title-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.config-card-lead {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.config-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.config-meta {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.config-meta-row {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.config-meta-row p {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.sort-bar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
@@ -695,6 +850,14 @@ button {
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.config-form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.field-span {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
.sort-bar {
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user