This repository has been archived on 2026-04-20. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
chrom-tool/src/composables/useBrowserManager.ts
2026-04-17 13:56:38 -04:00

537 lines
16 KiB
TypeScript

import { computed, onMounted, ref, watch } from "vue";
import { invoke } from "@tauri-apps/api/core";
import { open } from "@tauri-apps/plugin-dialog";
import { sortBookmarks, sortExtensions, sortPasswordSites, sortProfiles } from "../utils/sort";
import type {
ActiveSection,
AppPage,
AssociatedProfileSummary,
BookmarkAssociatedProfileSummary,
BookmarkSortKey,
BrowserConfigEntry,
BrowserConfigListResponse,
BrowserView,
CleanupHistoryInput,
CleanupHistoryResponse,
CreateCustomBrowserConfigInput,
ExtensionSortKey,
PasswordSiteSortKey,
ProfileSortKey,
ScanResponse,
} from "../types/browser";
export function useBrowserManager() {
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: "",
iconKey: "chrome",
executablePath: "",
userDataPath: "",
});
const selectedBrowserId = ref("");
const activeSection = ref<ActiveSection>("profiles");
const associatedProfilesModal = ref<{
title: string;
browserId: string;
profiles: (AssociatedProfileSummary | BookmarkAssociatedProfileSummary)[];
isBookmark: boolean;
} | null>(null);
const profileSortKey = ref<ProfileSortKey>("name");
const extensionSortKey = ref<ExtensionSortKey>("name");
const bookmarkSortKey = ref<BookmarkSortKey>("title");
const passwordSiteSortKey = ref<PasswordSiteSortKey>("domain");
const cleanupHistorySelectedProfiles = ref<string[]>([]);
const historyCleanupBusy = ref(false);
const cleanupHistoryError = ref("");
const cleanupHistoryResults = ref<CleanupHistoryResponse["results"]>([]);
const historyCleanupConfirmProfileIds = ref<string[]>([]);
const historyCleanupResultOpen = ref(false);
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),
);
const sortedPasswordSites = computed(() =>
sortPasswordSites(currentBrowser.value?.passwordSites ?? [], passwordSiteSortKey.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, () => {
openProfileError.value = "";
associatedProfilesModal.value = null;
cleanupHistorySelectedProfiles.value = [];
cleanupHistoryResults.value = [];
cleanupHistoryError.value = "";
historyCleanupConfirmProfileIds.value = [];
historyCleanupResultOpen.value = false;
});
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 = "";
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;
}
}
async function refreshAll() {
await Promise.all([loadBrowserConfigs(), scanBrowsers()]);
}
async function openBrowserProfile(browserId: string, profileId: string) {
const profileKey = `${browserId}:${profileId}`;
openingProfileKey.value = profileKey;
openProfileError.value = "";
try {
await invoke("open_browser_profile", {
browserId,
profileId,
});
} catch (openError) {
openProfileError.value =
openError instanceof Error
? openError.message
: "Failed to open the selected browser profile.";
} finally {
openingProfileKey.value = "";
}
}
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: "",
iconKey: "chrome",
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 = "";
}
}
async function pickExecutablePath() {
const selected = await open({
multiple: false,
directory: false,
filters: [
{
name: "Executable",
extensions: ["exe"],
},
],
});
if (typeof selected === "string") {
createConfigForm.value.executablePath = selected;
}
}
async function pickUserDataPath() {
const selected = await open({
multiple: false,
directory: true,
});
if (typeof selected === "string") {
createConfigForm.value.userDataPath = selected;
}
}
function isDeletingConfig(configId: string) {
return deletingConfigId.value === configId;
}
function isOpeningProfile(browserId: string, profileId: string) {
return openingProfileKey.value === `${browserId}:${profileId}`;
}
function browserMonogram(browserId: string) {
const current = browsers.value.find((browser) => browser.browserId === browserId);
const iconKey = current?.iconKey ?? current?.browserFamilyId;
if (iconKey === "chrome") return "CH";
if (iconKey === "edge") return "ED";
if (iconKey === "brave") return "BR";
if (iconKey === "vivaldi") return "VI";
if (iconKey === "yandex") return "YA";
if (iconKey === "chromium") return "CR";
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) {
const iconKey = config.iconKey ?? config.browserFamilyId;
if (iconKey === "chrome") return "CH";
if (iconKey === "edge") return "ED";
if (iconKey === "brave") return "BR";
if (iconKey === "vivaldi") return "VI";
if (iconKey === "yandex") return "YA";
if (iconKey === "chromium") return "CR";
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() || "?";
}
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;
if (section === "bookmarks") return currentBrowser.value.bookmarks.length;
if (section === "passwords") return currentBrowser.value.passwordSites.length;
return currentBrowser.value.stats.historyCleanupProfileCount;
}
function showExtensionProfilesModal(extensionId: string) {
const extension = currentBrowser.value?.extensions.find((item) => item.id === extensionId);
if (!extension || !currentBrowser.value) return;
associatedProfilesModal.value = {
title: `${extension.name} Profiles`,
browserId: currentBrowser.value.browserId,
profiles: extension.profiles,
isBookmark: false,
};
}
function showBookmarkProfilesModal(url: string) {
const bookmark = currentBrowser.value?.bookmarks.find((item) => item.url === url);
if (!bookmark || !currentBrowser.value) return;
associatedProfilesModal.value = {
title: `${bookmark.title} Profiles`,
browserId: currentBrowser.value.browserId,
profiles: bookmark.profiles,
isBookmark: true,
};
}
function showPasswordSiteProfilesModal(url: string) {
const passwordSite = currentBrowser.value?.passwordSites.find((item) => item.url === url);
if (!passwordSite || !currentBrowser.value) return;
associatedProfilesModal.value = {
title: `${passwordSite.domain} Profiles`,
browserId: currentBrowser.value.browserId,
profiles: passwordSite.profiles,
isBookmark: false,
};
}
function toggleHistoryProfile(profileId: string) {
if (cleanupHistorySelectedProfiles.value.includes(profileId)) {
cleanupHistorySelectedProfiles.value = cleanupHistorySelectedProfiles.value.filter(
(selectedId) => selectedId !== profileId,
);
return;
}
cleanupHistorySelectedProfiles.value = [
...cleanupHistorySelectedProfiles.value,
profileId,
];
}
function toggleAllHistoryProfiles() {
const current = currentBrowser.value;
if (!current) return;
const selectableIds = current.profiles
.filter((profile) => {
const cleanup = profile.historyCleanup;
return (
cleanup.history === "found" ||
cleanup.topSites === "found" ||
cleanup.visitedLinks === "found"
);
})
.map((profile) => profile.id);
const allSelected =
selectableIds.length > 0 &&
selectableIds.every((profileId) =>
cleanupHistorySelectedProfiles.value.includes(profileId),
);
cleanupHistorySelectedProfiles.value = allSelected ? [] : selectableIds;
}
function cleanupProfileIdsWithHistory(browser: BrowserView) {
return browser.profiles
.filter((profile) => {
const cleanup = profile.historyCleanup;
return (
cleanup.history === "found" ||
cleanup.topSites === "found" ||
cleanup.visitedLinks === "found"
);
})
.map((profile) => profile.id);
}
function historyCleanupConfirmProfiles() {
const browser = currentBrowser.value;
if (!browser) return [];
return browser.profiles.filter((profile) =>
historyCleanupConfirmProfileIds.value.includes(profile.id),
);
}
function cleanupSelectedHistoryProfiles() {
if (!cleanupHistorySelectedProfiles.value.length) return;
historyCleanupConfirmProfileIds.value = [...cleanupHistorySelectedProfiles.value];
}
function cleanupHistoryForProfile(profileId: string) {
historyCleanupConfirmProfileIds.value = [profileId];
}
function closeHistoryCleanupConfirm() {
if (historyCleanupBusy.value) return;
historyCleanupConfirmProfileIds.value = [];
}
function closeHistoryCleanupResult() {
historyCleanupResultOpen.value = false;
cleanupHistoryResults.value = [];
cleanupHistoryError.value = "";
}
function applyCleanupHistoryResults(results: CleanupHistoryResponse["results"]) {
const browser = currentBrowser.value;
if (!browser) return;
const succeededProfileIds = results
.filter((result) => !result.error)
.map((result) => result.profileId);
if (!succeededProfileIds.length) return;
for (const profile of browser.profiles) {
if (!succeededProfileIds.includes(profile.id)) continue;
const deletedFiles = results.find((result) => result.profileId === profile.id)?.deletedFiles ?? [];
if (deletedFiles.includes("History")) {
profile.historyCleanup.history = "missing";
}
if (deletedFiles.includes("Top Sites")) {
profile.historyCleanup.topSites = "missing";
}
if (deletedFiles.includes("Visited Links")) {
profile.historyCleanup.visitedLinks = "missing";
}
}
browser.stats.historyCleanupProfileCount = cleanupProfileIdsWithHistory(browser).length;
}
async function confirmHistoryCleanup() {
const browser = currentBrowser.value;
const profileIds = [...historyCleanupConfirmProfileIds.value];
if (!browser || profileIds.length === 0) return;
if (!currentBrowser.value || profileIds.length === 0) return;
historyCleanupBusy.value = true;
cleanupHistoryError.value = "";
cleanupHistoryResults.value = [];
historyCleanupResultOpen.value = false;
try {
const input: CleanupHistoryInput = {
browserId: browser.browserId,
profileIds,
};
const result = await invoke<CleanupHistoryResponse>("cleanup_history_files", { input });
applyCleanupHistoryResults(result.results);
cleanupHistoryResults.value = result.results;
cleanupHistorySelectedProfiles.value = cleanupHistorySelectedProfiles.value.filter(
(profileId) => !profileIds.includes(profileId),
);
historyCleanupConfirmProfileIds.value = [];
historyCleanupResultOpen.value = true;
} catch (cleanupErrorValue) {
historyCleanupConfirmProfileIds.value = [];
cleanupHistoryError.value =
cleanupErrorValue instanceof Error
? cleanupErrorValue.message
: "Failed to clean history files.";
historyCleanupResultOpen.value = true;
} finally {
historyCleanupBusy.value = false;
}
}
function closeAssociatedProfilesModal() {
associatedProfilesModal.value = null;
}
onMounted(() => {
void refreshAll();
});
return {
activeSection,
associatedProfilesModal,
bookmarkSortKey,
browserConfigs,
browserMonogram,
browsers,
configError,
configMonogram,
configsLoading,
createConfigForm,
createCustomBrowserConfig,
currentBrowser,
deleteCustomBrowserConfig,
error,
extensionMonogram,
extensionSortKey,
cleanupHistoryError,
cleanupHistoryResults,
cleanupHistorySelectedProfiles,
cleanupSelectedHistoryProfiles,
closeHistoryCleanupConfirm,
closeHistoryCleanupResult,
confirmHistoryCleanup,
historyCleanupBusy,
historyCleanupConfirmProfiles: computed(historyCleanupConfirmProfiles),
historyCleanupResultOpen,
isDeletingConfig,
isOpeningProfile,
loading,
openProfileError,
openBrowserProfile,
page,
pickExecutablePath,
pickUserDataPath,
passwordSiteSortKey,
profileSortKey,
refreshAll,
savingConfig,
sectionCount,
selectedBrowserId,
showBookmarkProfilesModal,
showExtensionProfilesModal,
showPasswordSiteProfilesModal,
sortedBookmarks,
sortedExtensions,
sortedPasswordSites,
sortedProfiles,
cleanupHistoryForProfile,
toggleAllHistoryProfiles,
toggleHistoryProfile,
closeAssociatedProfilesModal,
};
}