init
This commit is contained in:
328
src/App.vue
Normal file
328
src/App.vue
Normal file
@@ -0,0 +1,328 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
type BrowserStats = {
|
||||
profileCount: number;
|
||||
extensionCount: number;
|
||||
bookmarkCount: number;
|
||||
};
|
||||
|
||||
type ProfileSummary = {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string | null;
|
||||
avatarDataUrl: string | null;
|
||||
avatarLabel: string;
|
||||
path: string;
|
||||
};
|
||||
|
||||
type ExtensionSummary = {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string | null;
|
||||
iconDataUrl: string | null;
|
||||
profileIds: string[];
|
||||
};
|
||||
|
||||
type BookmarkSummary = {
|
||||
url: string;
|
||||
title: string;
|
||||
profileIds: string[];
|
||||
};
|
||||
|
||||
type BrowserView = {
|
||||
browserId: string;
|
||||
browserName: string;
|
||||
dataRoot: string;
|
||||
profiles: ProfileSummary[];
|
||||
extensions: ExtensionSummary[];
|
||||
bookmarks: BookmarkSummary[];
|
||||
stats: BrowserStats;
|
||||
};
|
||||
|
||||
type ScanResponse = {
|
||||
browsers: BrowserView[];
|
||||
};
|
||||
|
||||
const loading = ref(true);
|
||||
const error = ref("");
|
||||
const response = ref<ScanResponse>({ browsers: [] });
|
||||
const selectedBrowserId = ref("");
|
||||
|
||||
const browsers = computed(() => response.value.browsers);
|
||||
const currentBrowser = computed(() =>
|
||||
browsers.value.find((browser) => browser.browserId === selectedBrowserId.value) ??
|
||||
browsers.value[0] ??
|
||||
null,
|
||||
);
|
||||
|
||||
watch(
|
||||
browsers,
|
||||
(items) => {
|
||||
if (!items.length) {
|
||||
selectedBrowserId.value = "";
|
||||
return;
|
||||
}
|
||||
const hasSelected = items.some(
|
||||
(browser) => browser.browserId === selectedBrowserId.value,
|
||||
);
|
||||
if (!hasSelected) {
|
||||
selectedBrowserId.value = items[0].browserId;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
async function scanBrowsers() {
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
|
||||
try {
|
||||
response.value = await invoke<ScanResponse>("scan_browsers");
|
||||
} catch (scanError) {
|
||||
error.value =
|
||||
scanError instanceof Error
|
||||
? scanError.message
|
||||
: "Failed to scan browser data.";
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function browserMonogram(browserId: string) {
|
||||
if (browserId === "chrome") return "CH";
|
||||
if (browserId === "edge") return "ED";
|
||||
if (browserId === "brave") return "BR";
|
||||
return browserId.slice(0, 2).toUpperCase();
|
||||
}
|
||||
|
||||
function extensionMonogram(name: string) {
|
||||
return name.trim().slice(0, 1).toUpperCase() || "?";
|
||||
}
|
||||
|
||||
function domainFromUrl(url: string) {
|
||||
try {
|
||||
return new URL(url).hostname;
|
||||
} catch {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void scanBrowsers();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-shell">
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<p class="eyebrow">Desktop Utility</p>
|
||||
<h1>Browser Assistant</h1>
|
||||
<p class="sidebar-copy">
|
||||
Scan local Chromium profiles, extensions, and bookmarks in one place.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button class="refresh-button" type="button" @click="scanBrowsers">
|
||||
{{ loading ? "Scanning..." : "Refresh Data" }}
|
||||
</button>
|
||||
|
||||
<div v-if="browsers.length" class="browser-nav">
|
||||
<button
|
||||
v-for="browser in browsers"
|
||||
:key="browser.browserId"
|
||||
class="browser-nav-item"
|
||||
:class="[browser.browserId, { active: browser.browserId === currentBrowser?.browserId }]"
|
||||
type="button"
|
||||
@click="selectedBrowserId = browser.browserId"
|
||||
>
|
||||
<div class="browser-nav-icon">{{ browserMonogram(browser.browserId) }}</div>
|
||||
<div class="browser-nav-body">
|
||||
<strong>{{ browser.browserName }}</strong>
|
||||
<span>
|
||||
{{ browser.stats.profileCount }} profiles ·
|
||||
{{ browser.stats.extensionCount }} extensions ·
|
||||
{{ browser.stats.bookmarkCount }} bookmarks
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else class="sidebar-empty">
|
||||
<p>No supported Chromium browser data was found yet.</p>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<section v-else-if="error" class="state-panel error">
|
||||
<p class="eyebrow">Error</p>
|
||||
<h2>Scan failed</h2>
|
||||
<p>{{ error }}</p>
|
||||
</section>
|
||||
|
||||
<template v-else-if="currentBrowser">
|
||||
<header class="hero-card" :class="currentBrowser.browserId">
|
||||
<div class="hero-main">
|
||||
<p class="eyebrow">Current Browser</p>
|
||||
<h2>{{ currentBrowser.browserName }}</h2>
|
||||
<p class="hero-copy">
|
||||
Local data root: <span>{{ currentBrowser.dataRoot }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="hero-stats">
|
||||
<div class="stat-card">
|
||||
<span>Profiles</span>
|
||||
<strong>{{ currentBrowser.stats.profileCount }}</strong>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span>Extensions</span>
|
||||
<strong>{{ currentBrowser.stats.extensionCount }}</strong>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span>Bookmarks</span>
|
||||
<strong>{{ currentBrowser.stats.bookmarkCount }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="content-section">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Profiles</p>
|
||||
<h3>User Profiles</h3>
|
||||
</div>
|
||||
<span class="count-pill">{{ currentBrowser.profiles.length }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="currentBrowser.profiles.length" class="profile-grid">
|
||||
<article
|
||||
v-for="profile in currentBrowser.profiles"
|
||||
:key="profile.id"
|
||||
class="profile-card"
|
||||
>
|
||||
<div class="profile-avatar">
|
||||
<img
|
||||
v-if="profile.avatarDataUrl"
|
||||
:src="profile.avatarDataUrl"
|
||||
:alt="`${profile.name} avatar`"
|
||||
/>
|
||||
<span v-else>{{ profile.avatarLabel }}</span>
|
||||
</div>
|
||||
<div class="profile-body">
|
||||
<div class="profile-topline">
|
||||
<h4>{{ profile.name }}</h4>
|
||||
<span class="badge neutral">{{ profile.id }}</span>
|
||||
</div>
|
||||
<p class="profile-email">{{ profile.email || "No email found" }}</p>
|
||||
<p class="profile-path" :title="profile.path">{{ profile.path }}</p>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div v-else class="empty-card">
|
||||
<p>No profile directories were found for this browser.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Extensions</p>
|
||||
<h3>Installed Extensions</h3>
|
||||
</div>
|
||||
<span class="count-pill">{{ currentBrowser.extensions.length }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="currentBrowser.extensions.length" class="extension-grid">
|
||||
<article
|
||||
v-for="extension in currentBrowser.extensions"
|
||||
:key="extension.id"
|
||||
class="extension-card"
|
||||
>
|
||||
<div class="extension-icon">
|
||||
<img
|
||||
v-if="extension.iconDataUrl"
|
||||
:src="extension.iconDataUrl"
|
||||
:alt="`${extension.name} icon`"
|
||||
/>
|
||||
<span v-else>{{ extensionMonogram(extension.name) }}</span>
|
||||
</div>
|
||||
<div class="extension-body">
|
||||
<div class="extension-topline">
|
||||
<h4>{{ extension.name }}</h4>
|
||||
<span v-if="extension.version" class="badge neutral">
|
||||
v{{ extension.version }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="meta-line">{{ extension.id }}</p>
|
||||
<div class="badge-row">
|
||||
<span
|
||||
v-for="profileId in extension.profileIds"
|
||||
:key="`${extension.id}-${profileId}`"
|
||||
class="badge"
|
||||
>
|
||||
{{ profileId }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div v-else class="empty-card">
|
||||
<p>No extensions were discovered for this browser.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Bookmarks</p>
|
||||
<h3>Saved Bookmarks</h3>
|
||||
</div>
|
||||
<span class="count-pill">{{ currentBrowser.bookmarks.length }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="currentBrowser.bookmarks.length" class="bookmark-list">
|
||||
<article
|
||||
v-for="bookmark in currentBrowser.bookmarks"
|
||||
:key="bookmark.url"
|
||||
class="bookmark-card"
|
||||
>
|
||||
<div class="bookmark-body">
|
||||
<div class="bookmark-topline">
|
||||
<h4>{{ bookmark.title }}</h4>
|
||||
<span class="badge neutral">{{ domainFromUrl(bookmark.url) }}</span>
|
||||
</div>
|
||||
<p class="bookmark-url" :title="bookmark.url">{{ bookmark.url }}</p>
|
||||
<div class="badge-row">
|
||||
<span
|
||||
v-for="profileId in bookmark.profileIds"
|
||||
:key="`${bookmark.url}-${profileId}`"
|
||||
class="badge"
|
||||
>
|
||||
{{ profileId }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div v-else class="empty-card">
|
||||
<p>No bookmarks were discovered for this browser.</p>
|
||||
</div>
|
||||
</section>
|
||||
</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>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user