537 lines
16 KiB
TypeScript
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,
|
|
};
|
|
}
|