refactor frontend

This commit is contained in:
Julian Freeman
2026-04-16 13:59:52 -04:00
parent 0a7632c931
commit a12c734aa5
4 changed files with 303 additions and 215 deletions

View File

@@ -1,221 +1,29 @@
<script setup lang="ts">
import { computed, onMounted, ref, watch } from "vue";
import { invoke } from "@tauri-apps/api/core";
import { useBrowserAssistant } from "./features/browser-assistant/useBrowserAssistant";
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 ProfileSortKey = "name" | "email" | "id";
type ExtensionSortKey = "name" | "id";
type BookmarkSortKey = "title" | "url";
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 activeSection = ref<"profiles" | "extensions" | "bookmarks">("profiles");
const expandedExtensionIds = ref<string[]>([]);
const expandedBookmarkUrls = ref<string[]>([]);
const profileSortKey = ref<ProfileSortKey>("name");
const extensionSortKey = ref<ExtensionSortKey>("name");
const bookmarkSortKey = ref<BookmarkSortKey>("title");
const browsers = computed(() => response.value.browsers);
const currentBrowser = computed(() =>
browsers.value.find((browser) => browser.browserId === selectedBrowserId.value) ??
browsers.value[0] ??
null,
);
const sortedProfiles = computed(() => {
const profiles = [...(currentBrowser.value?.profiles ?? [])];
return profiles.sort((left, right) => {
if (profileSortKey.value === "email") {
return (
compareText(left.email ?? "", right.email ?? "") ||
compareText(left.name, right.name) ||
compareProfileId(left.id, right.id)
);
}
if (profileSortKey.value === "id") {
return compareProfileId(left.id, right.id);
}
return compareText(left.name, right.name) || compareProfileId(left.id, right.id);
});
});
const sortedExtensions = computed(() => {
const extensions = [...(currentBrowser.value?.extensions ?? [])];
return extensions.sort((left, right) => {
if (extensionSortKey.value === "id") {
return compareText(left.id, right.id);
}
return compareText(left.name, right.name) || compareText(left.id, right.id);
});
});
const sortedBookmarks = computed(() => {
const bookmarks = [...(currentBrowser.value?.bookmarks ?? [])];
return bookmarks.sort((left, right) => {
if (bookmarkSortKey.value === "url") {
return compareText(left.url, right.url);
}
return compareText(left.title, right.title) || compareText(left.url, right.url);
});
});
watch(
const {
activeSection,
bookmarkProfilesExpanded,
bookmarkSortKey,
browserMonogram,
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 },
);
watch(selectedBrowserId, () => {
expandedExtensionIds.value = [];
expandedBookmarkUrls.value = [];
});
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;
}
}
function sectionCount(section: "profiles" | "extensions" | "bookmarks") {
if (!currentBrowser.value) return 0;
if (section === "profiles") return currentBrowser.value.profiles.length;
if (section === "extensions") return currentBrowser.value.extensions.length;
return currentBrowser.value.bookmarks.length;
}
function toggleExtensionProfiles(extensionId: string) {
expandedExtensionIds.value = expandedExtensionIds.value.includes(extensionId)
? expandedExtensionIds.value.filter((id) => id !== extensionId)
: [...expandedExtensionIds.value, extensionId];
}
function toggleBookmarkProfiles(url: string) {
expandedBookmarkUrls.value = expandedBookmarkUrls.value.includes(url)
? expandedBookmarkUrls.value.filter((value) => value !== url)
: [...expandedBookmarkUrls.value, url];
}
function extensionProfilesExpanded(extensionId: string) {
return expandedExtensionIds.value.includes(extensionId);
}
function bookmarkProfilesExpanded(url: string) {
return expandedBookmarkUrls.value.includes(url);
}
function compareText(left: string, right: string) {
return left.localeCompare(right, undefined, {
sensitivity: "base",
numeric: true,
});
}
function compareProfileId(left: string, right: string) {
const leftKey = profileSortKeyValue(left);
const rightKey = profileSortKeyValue(right);
if (leftKey.group !== rightKey.group) return leftKey.group - rightKey.group;
if (leftKey.number !== rightKey.number) return leftKey.number - rightKey.number;
return compareText(leftKey.text, rightKey.text);
}
function profileSortKeyValue(profileId: string) {
if (profileId === "Default") {
return { group: 0, number: 0, text: profileId };
}
const profileNumber = profileId.startsWith("Profile ")
? Number(profileId.slice("Profile ".length))
: Number.NaN;
if (!Number.isNaN(profileNumber)) {
return { group: 1, number: profileNumber, text: profileId };
}
return { group: 2, number: Number.MAX_SAFE_INTEGER, text: profileId };
}
onMounted(() => {
void scanBrowsers();
});
currentBrowser,
domainFromUrl,
error,
extensionMonogram,
extensionProfilesExpanded,
extensionSortKey,
loading,
profileSortKey,
scanBrowsers,
sectionCount,
selectedBrowserId,
sortedBookmarks,
sortedExtensions,
sortedProfiles,
toggleBookmarkProfiles,
toggleExtensionProfiles,
} = useBrowserAssistant();
</script>
<template>

View File

@@ -0,0 +1,76 @@
import type {
BookmarkSortKey,
BookmarkSummary,
ExtensionSortKey,
ExtensionSummary,
ProfileSortKey,
ProfileSummary,
} from "./types";
export function compareText(left: string, right: string) {
return left.localeCompare(right, undefined, {
sensitivity: "base",
numeric: true,
});
}
export function compareProfileId(left: string, right: string) {
const leftKey = profileSortKeyValue(left);
const rightKey = profileSortKeyValue(right);
if (leftKey.group !== rightKey.group) return leftKey.group - rightKey.group;
if (leftKey.number !== rightKey.number) return leftKey.number - rightKey.number;
return compareText(leftKey.text, rightKey.text);
}
export function sortProfiles(items: ProfileSummary[], sortKey: ProfileSortKey) {
const profiles = [...items];
return profiles.sort((left, right) => {
if (sortKey === "email") {
return (
compareText(left.email ?? "", right.email ?? "") ||
compareText(left.name, right.name) ||
compareProfileId(left.id, right.id)
);
}
if (sortKey === "id") {
return compareProfileId(left.id, right.id);
}
return compareText(left.name, right.name) || compareProfileId(left.id, right.id);
});
}
export function sortExtensions(items: ExtensionSummary[], sortKey: ExtensionSortKey) {
const extensions = [...items];
return extensions.sort((left, right) => {
if (sortKey === "id") {
return compareText(left.id, right.id);
}
return compareText(left.name, right.name) || compareText(left.id, right.id);
});
}
export function sortBookmarks(items: BookmarkSummary[], sortKey: BookmarkSortKey) {
const bookmarks = [...items];
return bookmarks.sort((left, right) => {
if (sortKey === "url") {
return compareText(left.url, right.url);
}
return compareText(left.title, right.title) || compareText(left.url, right.url);
});
}
function profileSortKeyValue(profileId: string) {
if (profileId === "Default") {
return { group: 0, number: 0, text: profileId };
}
const profileNumber = profileId.startsWith("Profile ")
? Number(profileId.slice("Profile ".length))
: Number.NaN;
if (!Number.isNaN(profileNumber)) {
return { group: 1, number: profileNumber, text: profileId };
}
return { group: 2, number: Number.MAX_SAFE_INTEGER, text: profileId };
}

View File

@@ -0,0 +1,47 @@
export type BrowserStats = {
profileCount: number;
extensionCount: number;
bookmarkCount: number;
};
export type ProfileSummary = {
id: string;
name: string;
email: string | null;
avatarDataUrl: string | null;
avatarLabel: string;
path: string;
};
export type ExtensionSummary = {
id: string;
name: string;
version: string | null;
iconDataUrl: string | null;
profileIds: string[];
};
export type BookmarkSummary = {
url: string;
title: string;
profileIds: string[];
};
export type ProfileSortKey = "name" | "email" | "id";
export type ExtensionSortKey = "name" | "id";
export type BookmarkSortKey = "title" | "url";
export type ActiveSection = "profiles" | "extensions" | "bookmarks";
export type BrowserView = {
browserId: string;
browserName: string;
dataRoot: string;
profiles: ProfileSummary[];
extensions: ExtensionSummary[];
bookmarks: BookmarkSummary[];
stats: BrowserStats;
};
export type ScanResponse = {
browsers: BrowserView[];
};

View File

@@ -0,0 +1,157 @@
import { computed, onMounted, ref, watch } from "vue";
import { invoke } from "@tauri-apps/api/core";
import { sortBookmarks, sortExtensions, sortProfiles } from "./sort";
import type {
ActiveSection,
BookmarkSortKey,
BrowserView,
ExtensionSortKey,
ProfileSortKey,
ScanResponse,
} from "./types";
export function useBrowserAssistant() {
const loading = ref(true);
const error = ref("");
const response = ref<ScanResponse>({ browsers: [] });
const selectedBrowserId = ref("");
const activeSection = ref<ActiveSection>("profiles");
const expandedExtensionIds = ref<string[]>([]);
const expandedBookmarkUrls = ref<string[]>([]);
const profileSortKey = ref<ProfileSortKey>("name");
const extensionSortKey = ref<ExtensionSortKey>("name");
const bookmarkSortKey = ref<BookmarkSortKey>("title");
const browsers = computed(() => response.value.browsers);
const currentBrowser = computed<BrowserView | null>(
() =>
browsers.value.find((browser) => browser.browserId === selectedBrowserId.value) ??
browsers.value[0] ??
null,
);
const sortedProfiles = computed(() =>
sortProfiles(currentBrowser.value?.profiles ?? [], profileSortKey.value),
);
const sortedExtensions = computed(() =>
sortExtensions(currentBrowser.value?.extensions ?? [], extensionSortKey.value),
);
const sortedBookmarks = computed(() =>
sortBookmarks(currentBrowser.value?.bookmarks ?? [], bookmarkSortKey.value),
);
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 },
);
watch(selectedBrowserId, () => {
expandedExtensionIds.value = [];
expandedBookmarkUrls.value = [];
});
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;
}
}
function sectionCount(section: ActiveSection) {
if (!currentBrowser.value) return 0;
if (section === "profiles") return currentBrowser.value.profiles.length;
if (section === "extensions") return currentBrowser.value.extensions.length;
return currentBrowser.value.bookmarks.length;
}
function toggleExtensionProfiles(extensionId: string) {
expandedExtensionIds.value = expandedExtensionIds.value.includes(extensionId)
? expandedExtensionIds.value.filter((id) => id !== extensionId)
: [...expandedExtensionIds.value, extensionId];
}
function toggleBookmarkProfiles(url: string) {
expandedBookmarkUrls.value = expandedBookmarkUrls.value.includes(url)
? expandedBookmarkUrls.value.filter((value) => value !== url)
: [...expandedBookmarkUrls.value, url];
}
function extensionProfilesExpanded(extensionId: string) {
return expandedExtensionIds.value.includes(extensionId);
}
function bookmarkProfilesExpanded(url: string) {
return expandedBookmarkUrls.value.includes(url);
}
onMounted(() => {
void scanBrowsers();
});
return {
activeSection,
bookmarkProfilesExpanded,
bookmarkSortKey,
browserMonogram,
browsers,
currentBrowser,
domainFromUrl,
error,
extensionMonogram,
extensionProfilesExpanded,
extensionSortKey,
loading,
profileSortKey,
scanBrowsers,
sectionCount,
selectedBrowserId,
sortedBookmarks,
sortedExtensions,
sortedProfiles,
toggleBookmarkProfiles,
toggleExtensionProfiles,
};
}