support delete bookmarks

This commit is contained in:
Julian Freeman
2026-04-17 16:27:57 -04:00
parent 42905bf6d3
commit 45662dc642
10 changed files with 946 additions and 24 deletions

View File

@@ -8,6 +8,14 @@ const {
activeSection,
associatedProfilesModal,
bookmarkSortKey,
bookmarkDeleteBusy,
bookmarkModalSelectedProfileIds,
bookmarkRemovalConfirmBookmarkCount,
bookmarkRemovalConfirmProfileCount,
bookmarkRemovalError,
bookmarkRemovalResultOpen,
bookmarkRemovalResults,
bookmarkSelectedUrls,
browserConfigs,
browserMonogram,
browsers,
@@ -16,6 +24,8 @@ const {
configsLoading,
createConfigForm,
createCustomBrowserConfig,
deleteBookmarkFromAllProfiles,
deleteBookmarkFromProfile,
cleanupHistoryError,
cleanupHistoryForProfile,
cleanupHistoryResults,
@@ -26,6 +36,8 @@ const {
confirmHistoryCleanup,
currentBrowser,
deleteCustomBrowserConfig,
deleteSelectedBookmarkProfiles,
deleteSelectedBookmarks,
deleteExtensionFromAllProfiles,
deleteExtensionFromProfile,
deleteSelectedExtensionProfiles,
@@ -58,6 +70,8 @@ const {
savingConfig,
sectionCount,
selectedBrowserId,
closeBookmarkRemovalConfirm,
closeBookmarkRemovalResult,
showBookmarkProfilesModal,
showExtensionProfilesModal,
showPasswordSiteProfilesModal,
@@ -65,10 +79,15 @@ const {
sortedExtensions,
sortedPasswordSites,
sortedProfiles,
confirmBookmarkRemoval,
closeExtensionRemovalConfirm,
closeExtensionRemovalResult,
confirmExtensionRemoval,
toggleAllBookmarks,
toggleAllBookmarkModalProfiles,
toggleAllExtensions,
toggleBookmarkModalProfileSelection,
toggleBookmarkSelection,
toggleAllExtensionModalProfiles,
toggleExtensionModalProfileSelection,
toggleExtensionSelection,
@@ -147,6 +166,14 @@ const {
:history-cleanup-result-open="historyCleanupResultOpen"
:cleanup-history-error="cleanupHistoryError"
:cleanup-history-results="cleanupHistoryResults"
:bookmark-selected-urls="bookmarkSelectedUrls"
:bookmark-modal-selected-profile-ids="bookmarkModalSelectedProfileIds"
:bookmark-delete-busy="bookmarkDeleteBusy"
:bookmark-removal-confirm-bookmark-count="bookmarkRemovalConfirmBookmarkCount"
:bookmark-removal-confirm-profile-count="bookmarkRemovalConfirmProfileCount"
:bookmark-removal-result-open="bookmarkRemovalResultOpen"
:bookmark-removal-error="bookmarkRemovalError"
:bookmark-removal-results="bookmarkRemovalResults"
:extension-selected-ids="extensionSelectedIds"
:extension-modal-selected-profile-ids="extensionModalSelectedProfileIds"
:extension-delete-busy="extensionDeleteBusy"
@@ -169,6 +196,17 @@ const {
@show-extension-profiles="showExtensionProfilesModal"
@show-bookmark-profiles="showBookmarkProfilesModal"
@show-password-site-profiles="showPasswordSiteProfilesModal"
@toggle-bookmark-selection="toggleBookmarkSelection"
@toggle-all-bookmarks="toggleAllBookmarks"
@delete-bookmark-from-all-profiles="deleteBookmarkFromAllProfiles"
@delete-selected-bookmarks="deleteSelectedBookmarks"
@toggle-bookmark-modal-profile-selection="toggleBookmarkModalProfileSelection"
@toggle-all-bookmark-modal-profiles="toggleAllBookmarkModalProfiles"
@delete-bookmark-from-profile="deleteBookmarkFromProfile"
@delete-selected-bookmark-profiles="deleteSelectedBookmarkProfiles"
@confirm-bookmark-removal="confirmBookmarkRemoval"
@close-bookmark-removal-confirm="closeBookmarkRemovalConfirm"
@close-bookmark-removal-result="closeBookmarkRemovalResult"
@toggle-extension-selection="toggleExtensionSelection"
@toggle-all-extensions="toggleAllExtensions"
@delete-extension-from-all-profiles="deleteExtensionFromAllProfiles"

View File

@@ -43,6 +43,7 @@ const allSelected = computed(
sortedProfiles.value.length > 0 &&
sortedProfiles.value.every((profile) => selectedProfileIds.value.includes(profile.id)),
);
const isSelectableMode = computed(() => props.isExtension || props.isBookmark);
function hasBookmarkPath(profile: ModalProfile): profile is BookmarkAssociatedProfileSummary {
return "bookmarkPath" in profile;
@@ -67,7 +68,7 @@ function isSelected(profileId: string) {
</button>
</div>
<div v-if="isExtension" class="modal-toolbar">
<div v-if="isSelectableMode" class="modal-toolbar">
<label class="toolbar-checkbox" :class="{ disabled: !sortedProfiles.length }">
<input
type="checkbox"
@@ -95,10 +96,18 @@ function isSelected(profileId: string) {
<div class="modal-table">
<div class="modal-table-header modal-grid" :class="{ bookmark: isBookmark, extension: isExtension }">
<div v-if="isExtension" class="header-cell checkbox-cell">Pick</div>
<div v-if="isSelectableMode" class="header-cell checkbox-cell">Pick</div>
<div class="header-cell icon-cell">Avatar</div>
<button class="header-cell sortable" :class="{ active: sortKey === 'name' }" type="button" @click="sortKey = 'name'">Name</button>
<button v-if="!isExtension" class="header-cell sortable" :class="{ active: sortKey === 'id' }" type="button" @click="sortKey = 'id'">Profile ID</button>
<button
v-if="!isExtension && !isBookmark"
class="header-cell sortable"
:class="{ active: sortKey === 'id' }"
type="button"
@click="sortKey = 'id'"
>
Profile ID
</button>
<div v-if="isExtension" class="header-cell">Source</div>
<div v-if="isBookmark" class="header-cell">Bookmark Path</div>
<div class="header-cell actions-cell">Action</div>
@@ -110,7 +119,7 @@ function isSelected(profileId: string) {
class="modal-table-row modal-grid"
:class="{ bookmark: isBookmark, extension: isExtension }"
>
<div v-if="isExtension" class="row-cell checkbox-cell">
<div v-if="isSelectableMode" class="row-cell checkbox-cell">
<label class="table-checkbox" :class="{ disabled: deleteBusy }">
<input
type="checkbox"
@@ -136,9 +145,9 @@ function isSelected(profileId: string) {
</div>
<div class="row-cell primary-cell">
<strong>{{ profile.name }}</strong>
<span v-if="isExtension" class="subtle-line">{{ profile.id }}</span>
<span v-if="isExtension || isBookmark" class="subtle-line">{{ profile.id }}</span>
</div>
<div v-if="!isExtension" class="row-cell">
<div v-if="!isExtension && !isBookmark" class="row-cell">
<span class="badge neutral">{{ profile.id }}</span>
</div>
<div v-if="isExtension && hasInstallSource(profile)" class="row-cell muted-cell">
@@ -161,7 +170,7 @@ function isSelected(profileId: string) {
{{ isOpeningProfile(browserId, profile.id) ? "Opening..." : "Open" }}
</button>
<button
v-if="isExtension"
v-if="isSelectableMode"
class="danger-button inline-danger-button"
type="button"
:disabled="deleteBusy"
@@ -234,7 +243,7 @@ function isSelected(profileId: string) {
}
.modal-grid.bookmark {
grid-template-columns: 56px minmax(140px, 0.9fr) 120px minmax(180px, 1fr) 110px;
grid-template-columns: 52px 56px minmax(180px, 0.78fr) minmax(180px, 1fr) 168px;
}
.modal-table-header.modal-grid.extension,

View File

@@ -0,0 +1,151 @@
<script setup lang="ts">
import { computed } from "vue";
import type { RemoveBookmarkResult } from "../../types/browser";
const props = defineProps<{
mode: "confirm" | "result";
title: string;
bookmarkCount: number;
profileCount: number;
results: RemoveBookmarkResult[];
busy?: boolean;
generalError?: string;
}>();
const emit = defineEmits<{
close: [];
confirm: [];
}>();
const resultSummary = computed(() => {
const statusByUrl = new Map<string, boolean>();
for (const result of props.results) {
const previous = statusByUrl.get(result.url);
const succeeded = !result.error;
if (previous === undefined) {
statusByUrl.set(result.url, succeeded);
continue;
}
statusByUrl.set(result.url, previous && succeeded);
}
let successCount = 0;
let failedCount = 0;
for (const succeeded of statusByUrl.values()) {
if (succeeded) {
successCount += 1;
} else {
failedCount += 1;
}
}
return { successCount, failedCount };
});
</script>
<template>
<div class="modal-backdrop" @click.self="emit('close')">
<section class="modal-card">
<div class="modal-header">
<h3>{{ title }}</h3>
<button class="secondary-button" type="button" @click="emit('close')">Close</button>
</div>
<template v-if="mode === 'confirm'">
<p class="modal-copy">
This will remove {{ bookmarkCount }} bookmark{{ bookmarkCount === 1 ? "" : "s" }} from
{{ profileCount }} profile{{ profileCount === 1 ? "" : "s" }}.
</p>
<div class="modal-actions">
<button class="secondary-button" type="button" @click="emit('close')">Cancel</button>
<button class="danger-button" type="button" :disabled="busy" @click="emit('confirm')">
{{ busy ? "Deleting..." : "Confirm Delete" }}
</button>
</div>
</template>
<template v-else>
<p v-if="generalError" class="result-banner error">{{ generalError }}</p>
<p class="modal-copy">
Successfully removed {{ resultSummary.successCount }} bookmark{{
resultSummary.successCount === 1 ? "" : "s"
}}. Failed to remove {{ resultSummary.failedCount }} bookmark{{
resultSummary.failedCount === 1 ? "" : "s"
}}.
</p>
<div class="modal-actions">
<button class="primary-button" type="button" @click="emit('close')">Close</button>
</div>
</template>
</section>
</div>
</template>
<style scoped>
.modal-backdrop {
position: fixed;
inset: 0;
z-index: 50;
display: grid;
place-items: center;
padding: 24px;
background: rgba(15, 23, 42, 0.26);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
}
.modal-card {
width: min(560px, 100%);
max-height: min(72vh, 820px);
display: flex;
flex-direction: column;
gap: 14px;
padding: 16px;
border: 1px solid var(--panel-border);
border-radius: 22px;
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 28px 70px rgba(15, 23, 42, 0.18);
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.modal-header h3,
.modal-copy {
margin: 0;
}
.modal-copy {
color: var(--muted);
line-height: 1.55;
}
.result-banner {
margin: 0;
padding: 12px 14px;
border-radius: 14px;
font-size: 0.9rem;
}
.result-banner.error {
background: rgba(254, 242, 242, 0.96);
color: #b42318;
border: 1px solid rgba(239, 68, 68, 0.18);
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
}
</style>

View File

@@ -1,27 +1,88 @@
<script setup lang="ts">
import { computed } from "vue";
import type { BookmarkSortKey, BookmarkSummary } from "../../types/browser";
defineProps<{
const props = defineProps<{
bookmarks: BookmarkSummary[];
sortKey: BookmarkSortKey;
selectedBookmarkUrls: string[];
deleteBusy: boolean;
}>();
const emit = defineEmits<{
"update:sortKey": [value: BookmarkSortKey];
showProfiles: [url: string];
toggleBookmark: [url: string];
toggleAllBookmarks: [];
deleteBookmark: [url: string];
deleteSelected: [];
}>();
const allSelected = computed(
() =>
props.bookmarks.length > 0 &&
props.bookmarks.every((bookmark) => props.selectedBookmarkUrls.includes(bookmark.url)),
);
function isSelected(url: string) {
return props.selectedBookmarkUrls.includes(url);
}
</script>
<template>
<section class="table-section">
<div class="bookmarks-toolbar">
<label class="toolbar-checkbox" :class="{ disabled: !bookmarks.length }">
<input
type="checkbox"
class="native-checkbox"
:checked="allSelected"
:disabled="!bookmarks.length || deleteBusy"
@change="emit('toggleAllBookmarks')"
/>
<span class="custom-checkbox" :class="{ checked: allSelected }" aria-hidden="true">
<svg viewBox="0 0 16 16">
<path d="M3.5 8.2L6.4 11.1L12.5 4.9" />
</svg>
</span>
<span>Select All</span>
</label>
<button
class="danger-button"
type="button"
:disabled="!selectedBookmarkUrls.length || deleteBusy"
@click="emit('deleteSelected')"
>
{{ deleteBusy ? "Deleting..." : `Delete Selected (${selectedBookmarkUrls.length})` }}
</button>
</div>
<div v-if="bookmarks.length" class="data-table">
<div class="data-table-header bookmarks-grid">
<div class="header-cell checkbox-cell">Pick</div>
<button class="header-cell sortable" :class="{ active: sortKey === 'title' }" type="button" @click="emit('update:sortKey', 'title')">Name</button>
<button class="header-cell sortable" :class="{ active: sortKey === 'url' }" type="button" @click="emit('update:sortKey', 'url')">URL</button>
<div class="header-cell actions-cell">Profiles</div>
<div class="header-cell actions-cell">Actions</div>
</div>
<div class="data-table-body styled-scrollbar">
<article v-for="bookmark in bookmarks" :key="bookmark.url" class="data-table-row bookmarks-grid">
<div class="row-cell checkbox-cell">
<label class="table-checkbox" :class="{ disabled: deleteBusy }">
<input
type="checkbox"
class="native-checkbox"
:checked="isSelected(bookmark.url)"
:disabled="deleteBusy"
@change="emit('toggleBookmark', bookmark.url)"
/>
<span class="custom-checkbox" :class="{ checked: isSelected(bookmark.url) }" aria-hidden="true">
<svg viewBox="0 0 16 16">
<path d="M3.5 8.2L6.4 11.1L12.5 4.9" />
</svg>
</span>
</label>
</div>
<div class="row-cell primary-cell">
<strong :title="bookmark.title">{{ bookmark.title }}</strong>
</div>
@@ -31,6 +92,14 @@ const emit = defineEmits<{
<span>View</span>
<span class="badge neutral">{{ bookmark.profileIds.length }}</span>
</button>
<button
class="danger-button inline-danger-button"
type="button"
:disabled="deleteBusy"
@click="emit('deleteBookmark', bookmark.url)"
>
Delete
</button>
</div>
</article>
</div>
@@ -43,11 +112,21 @@ const emit = defineEmits<{
<style scoped>
.table-section {
display: flex;
flex-direction: column;
gap: 10px;
padding: 0;
height: 100%;
min-height: 0;
}
.bookmarks-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.data-table {
display: flex;
flex-direction: column;
@@ -68,7 +147,7 @@ const emit = defineEmits<{
.bookmarks-grid {
display: grid;
grid-template-columns: minmax(180px, 0.9fr) minmax(260px, 1.2fr) 154px;
grid-template-columns: 52px minmax(180px, 0.9fr) minmax(260px, 1.2fr) 250px;
gap: 12px;
align-items: center;
}
@@ -102,6 +181,79 @@ const emit = defineEmits<{
color: var(--text);
}
.toolbar-checkbox {
display: inline-flex;
align-items: center;
gap: 10px;
color: var(--text);
font-size: 0.88rem;
cursor: pointer;
}
.toolbar-checkbox.disabled {
opacity: 0.55;
cursor: default;
}
.native-checkbox {
position: absolute;
opacity: 0;
pointer-events: none;
}
.table-checkbox {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.table-checkbox.disabled {
cursor: default;
opacity: 0.5;
}
.custom-checkbox {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border: 1px solid rgba(148, 163, 184, 0.34);
border-radius: 7px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(241, 245, 249, 0.92));
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.78),
0 4px 10px rgba(15, 23, 42, 0.06);
}
.custom-checkbox svg {
width: 12px;
height: 12px;
}
.custom-checkbox path {
fill: none;
stroke: #fff;
stroke-width: 2.2;
stroke-linecap: round;
stroke-linejoin: round;
opacity: 0;
}
.custom-checkbox.checked {
border-color: rgba(47, 111, 237, 0.2);
background: linear-gradient(135deg, #2f6fed, #5aa1f7);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.22),
0 8px 18px rgba(47, 111, 237, 0.22);
}
.custom-checkbox.checked path {
opacity: 1;
}
.data-table-row {
padding: 12px 14px;
border-bottom: 1px solid rgba(148, 163, 184, 0.12);
@@ -153,20 +305,32 @@ const emit = defineEmits<{
.actions-cell {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.checkbox-cell {
display: flex;
justify-content: center;
}
.inline-danger-button {
padding: 6px 10px;
border-radius: 10px;
font-size: 0.8rem;
}
@media (max-width: 900px) {
.bookmarks-grid {
grid-template-columns: minmax(160px, 0.9fr) minmax(200px, 1fr) 148px;
grid-template-columns: 52px minmax(160px, 0.9fr) minmax(200px, 1fr) 236px;
}
}
@media (max-width: 720px) {
.bookmarks-grid {
grid-template-columns: minmax(0, 1fr) 132px;
grid-template-columns: 52px minmax(0, 1fr) 152px;
}
.bookmarks-grid > :nth-child(2) {
.bookmarks-grid > :nth-child(3) {
display: none;
}
}

View File

@@ -8,11 +8,13 @@ import type {
ExtensionSortKey,
CleanupHistoryResult,
ExtensionAssociatedProfileSummary,
RemoveBookmarkResult,
PasswordSiteSortKey,
ProfileSortKey,
RemoveExtensionResult,
} from "../../types/browser";
import AssociatedProfilesModal from "./AssociatedProfilesModal.vue";
import BookmarkRemovalModal from "./BookmarkRemovalModal.vue";
import BookmarksList from "./BookmarksList.vue";
import ExtensionRemovalModal from "./ExtensionRemovalModal.vue";
import ExtensionsList from "./ExtensionsList.vue";
@@ -38,6 +40,14 @@ defineProps<{
historyCleanupResultOpen: boolean;
cleanupHistoryError: string;
cleanupHistoryResults: CleanupHistoryResult[];
bookmarkSelectedUrls: string[];
bookmarkModalSelectedProfileIds: string[];
bookmarkDeleteBusy: boolean;
bookmarkRemovalConfirmBookmarkCount: number;
bookmarkRemovalConfirmProfileCount: number;
bookmarkRemovalResultOpen: boolean;
bookmarkRemovalError: string;
bookmarkRemovalResults: RemoveBookmarkResult[];
extensionSelectedIds: string[];
extensionModalSelectedProfileIds: string[];
extensionDeleteBusy: boolean;
@@ -61,6 +71,7 @@ defineProps<{
isBookmark: boolean;
isExtension?: boolean;
extensionId?: string;
bookmarkUrl?: string;
} | null;
}>();
@@ -74,6 +85,17 @@ const emit = defineEmits<{
showExtensionProfiles: [extensionId: string];
showBookmarkProfiles: [url: string];
showPasswordSiteProfiles: [url: string];
toggleBookmarkSelection: [url: string];
toggleAllBookmarks: [];
deleteBookmarkFromAllProfiles: [url: string];
deleteSelectedBookmarks: [];
toggleBookmarkModalProfileSelection: [profileId: string];
toggleAllBookmarkModalProfiles: [];
deleteBookmarkFromProfile: [profileId: string];
deleteSelectedBookmarkProfiles: [];
confirmBookmarkRemoval: [];
closeBookmarkRemovalConfirm: [];
closeBookmarkRemovalResult: [];
toggleExtensionSelection: [extensionId: string];
toggleAllExtensions: [];
deleteExtensionFromAllProfiles: [extensionId: string];
@@ -177,8 +199,14 @@ const emit = defineEmits<{
v-else-if="activeSection === 'bookmarks'"
:bookmarks="sortedBookmarks"
:sort-key="bookmarkSortKey"
:selected-bookmark-urls="bookmarkSelectedUrls"
:delete-busy="bookmarkDeleteBusy"
@update:sort-key="emit('update:bookmarkSortKey', $event)"
@show-profiles="emit('showBookmarkProfiles', $event)"
@toggle-bookmark="emit('toggleBookmarkSelection', $event)"
@toggle-all-bookmarks="emit('toggleAllBookmarks')"
@delete-bookmark="emit('deleteBookmarkFromAllProfiles', $event)"
@delete-selected="emit('deleteSelectedBookmarks')"
/>
<PasswordSitesList
@@ -223,6 +251,29 @@ const emit = defineEmits<{
@close="emit('closeHistoryCleanupResult')"
/>
<BookmarkRemovalModal
v-if="bookmarkRemovalConfirmBookmarkCount > 0"
mode="confirm"
title="Confirm Bookmark Removal"
:bookmark-count="bookmarkRemovalConfirmBookmarkCount"
:profile-count="bookmarkRemovalConfirmProfileCount"
:results="[]"
:busy="bookmarkDeleteBusy"
@close="emit('closeBookmarkRemovalConfirm')"
@confirm="emit('confirmBookmarkRemoval')"
/>
<BookmarkRemovalModal
v-if="bookmarkRemovalResultOpen"
mode="result"
title="Bookmark Removal Result"
:bookmark-count="0"
:profile-count="0"
:results="bookmarkRemovalResults"
:general-error="bookmarkRemovalError"
@close="emit('closeBookmarkRemovalResult')"
/>
<ExtensionRemovalModal
v-if="extensionRemovalConfirmExtensions.length || extensionRemovalConfirmProfiles.length"
mode="confirm"
@@ -254,15 +305,35 @@ const emit = defineEmits<{
:browser-family-id="currentBrowser.browserFamilyId"
:is-bookmark="associatedProfilesModal.isBookmark"
:is-extension="associatedProfilesModal.isExtension"
:selected-profile-ids="extensionModalSelectedProfileIds"
:delete-busy="extensionDeleteBusy"
:selected-profile-ids="
associatedProfilesModal.isExtension
? extensionModalSelectedProfileIds
: bookmarkModalSelectedProfileIds
"
:delete-busy="associatedProfilesModal.isExtension ? extensionDeleteBusy : bookmarkDeleteBusy"
:is-opening-profile="isOpeningProfile"
@close="emit('closeAssociatedProfiles')"
@open-profile="(browserId, profileId) => emit('openProfile', browserId, profileId)"
@toggle-profile-selection="emit('toggleExtensionModalProfileSelection', $event)"
@toggle-all-profile-selection="emit('toggleAllExtensionModalProfiles')"
@delete-profile="emit('deleteExtensionFromProfile', $event)"
@delete-selected-profiles="emit('deleteSelectedExtensionProfiles')"
@toggle-profile-selection="
associatedProfilesModal.isExtension
? emit('toggleExtensionModalProfileSelection', $event)
: emit('toggleBookmarkModalProfileSelection', $event)
"
@toggle-all-profile-selection="
associatedProfilesModal.isExtension
? emit('toggleAllExtensionModalProfiles')
: emit('toggleAllBookmarkModalProfiles')
"
@delete-profile="
associatedProfilesModal.isExtension
? emit('deleteExtensionFromProfile', $event)
: emit('deleteBookmarkFromProfile', $event)
"
@delete-selected-profiles="
associatedProfilesModal.isExtension
? emit('deleteSelectedExtensionProfiles')
: emit('deleteSelectedBookmarkProfiles')
"
/>
</template>

View File

@@ -12,6 +12,7 @@ import type {
BrowserConfigEntry,
BrowserConfigListResponse,
BrowserView,
BookmarkRemovalRequest,
CleanupHistoryInput,
CleanupHistoryResponse,
CreateCustomBrowserConfigInput,
@@ -20,6 +21,8 @@ import type {
ExtensionSortKey,
PasswordSiteSortKey,
ProfileSortKey,
RemoveBookmarksInput,
RemoveBookmarksResponse,
RemoveExtensionsInput,
RemoveExtensionsResponse,
ScanResponse,
@@ -56,11 +59,21 @@ export function useBrowserManager() {
isBookmark: boolean;
isExtension?: boolean;
extensionId?: string;
bookmarkUrl?: string;
} | null>(null);
const profileSortKey = ref<ProfileSortKey>("name");
const extensionSortKey = ref<ExtensionSortKey>("name");
const bookmarkSortKey = ref<BookmarkSortKey>("title");
const passwordSiteSortKey = ref<PasswordSiteSortKey>("domain");
const bookmarkSelectedUrls = ref<string[]>([]);
const bookmarkModalSelectedProfileIds = ref<string[]>([]);
const bookmarkDeleteBusy = ref(false);
const bookmarkRemovalError = ref("");
const bookmarkRemovalResults = ref<RemoveBookmarksResponse["results"]>([]);
const bookmarkRemovalResultOpen = ref(false);
const bookmarkRemovalConfirmRemovals = ref<BookmarkRemovalRequest[]>([]);
const bookmarkRemovalConfirmUrls = ref<string[]>([]);
const bookmarkRemovalConfirmProfileIds = ref<string[]>([]);
const extensionSelectedIds = ref<string[]>([]);
const extensionModalSelectedProfileIds = ref<string[]>([]);
const extensionDeleteBusy = ref(false);
@@ -123,6 +136,14 @@ export function useBrowserManager() {
cleanupHistorySelectedProfiles.value = [];
cleanupHistoryResults.value = [];
cleanupHistoryError.value = "";
bookmarkSelectedUrls.value = [];
bookmarkModalSelectedProfileIds.value = [];
bookmarkRemovalError.value = "";
bookmarkRemovalResults.value = [];
bookmarkRemovalResultOpen.value = false;
bookmarkRemovalConfirmRemovals.value = [];
bookmarkRemovalConfirmUrls.value = [];
bookmarkRemovalConfirmProfileIds.value = [];
extensionSelectedIds.value = [];
extensionModalSelectedProfileIds.value = [];
extensionRemovalError.value = "";
@@ -339,11 +360,13 @@ export function useBrowserManager() {
function showBookmarkProfilesModal(url: string) {
const bookmark = currentBrowser.value?.bookmarks.find((item) => item.url === url);
if (!bookmark || !currentBrowser.value) return;
bookmarkModalSelectedProfileIds.value = [];
associatedProfilesModal.value = {
title: `${bookmark.title} Profiles`,
browserId: currentBrowser.value.browserId,
profiles: bookmark.profiles,
isBookmark: true,
bookmarkUrl: url,
};
}
@@ -510,6 +533,201 @@ export function useBrowserManager() {
function closeAssociatedProfilesModal() {
associatedProfilesModal.value = null;
extensionModalSelectedProfileIds.value = [];
bookmarkModalSelectedProfileIds.value = [];
}
function toggleBookmarkSelection(url: string) {
if (bookmarkSelectedUrls.value.includes(url)) {
bookmarkSelectedUrls.value = bookmarkSelectedUrls.value.filter((item) => item !== url);
return;
}
bookmarkSelectedUrls.value = [...bookmarkSelectedUrls.value, url];
}
function toggleAllBookmarks() {
const bookmarkUrls = currentBrowser.value?.bookmarks.map((bookmark) => bookmark.url) ?? [];
const allSelected =
bookmarkUrls.length > 0 &&
bookmarkUrls.every((url) => bookmarkSelectedUrls.value.includes(url));
bookmarkSelectedUrls.value = allSelected ? [] : bookmarkUrls;
}
function toggleBookmarkModalProfileSelection(profileId: string) {
if (bookmarkModalSelectedProfileIds.value.includes(profileId)) {
bookmarkModalSelectedProfileIds.value = bookmarkModalSelectedProfileIds.value.filter(
(id) => id !== profileId,
);
return;
}
bookmarkModalSelectedProfileIds.value = [...bookmarkModalSelectedProfileIds.value, profileId];
}
function toggleAllBookmarkModalProfiles() {
if (!associatedProfilesModal.value?.isBookmark) return;
const profileIds = associatedProfilesModal.value.profiles.map((profile) => profile.id);
const allSelected =
profileIds.length > 0 &&
profileIds.every((profileId) => bookmarkModalSelectedProfileIds.value.includes(profileId));
bookmarkModalSelectedProfileIds.value = allSelected ? [] : profileIds;
}
function bookmarkRemovalConfirmBookmarkCount() {
return bookmarkRemovalConfirmUrls.value.length;
}
function bookmarkRemovalConfirmProfileCount() {
return bookmarkRemovalConfirmProfileIds.value.length;
}
function requestBookmarkRemoval(removals: BookmarkRemovalRequest[]) {
if (!removals.length) return;
bookmarkRemovalConfirmRemovals.value = removals;
bookmarkRemovalConfirmUrls.value = [...new Set(removals.map((item) => item.url))];
bookmarkRemovalConfirmProfileIds.value = [...new Set(removals.flatMap((item) => item.profileIds))];
}
function resetBookmarkRemovalConfirmState() {
bookmarkRemovalConfirmRemovals.value = [];
bookmarkRemovalConfirmUrls.value = [];
bookmarkRemovalConfirmProfileIds.value = [];
}
function deleteBookmarkFromAllProfiles(url: string) {
const bookmark = currentBrowser.value?.bookmarks.find((item) => item.url === url);
if (!bookmark) return;
requestBookmarkRemoval([
{
url,
profileIds: [...bookmark.profileIds],
},
]);
}
function deleteSelectedBookmarks() {
const browser = currentBrowser.value;
if (!browser || !bookmarkSelectedUrls.value.length) return;
const removals = browser.bookmarks
.filter((bookmark) => bookmarkSelectedUrls.value.includes(bookmark.url))
.map((bookmark) => ({
url: bookmark.url,
profileIds: [...bookmark.profileIds],
}));
requestBookmarkRemoval(removals);
}
function deleteBookmarkFromProfile(profileId: string) {
const modal = associatedProfilesModal.value;
if (!modal?.isBookmark || !modal.bookmarkUrl) return;
requestBookmarkRemoval([
{
url: modal.bookmarkUrl,
profileIds: [profileId],
},
]);
}
function deleteSelectedBookmarkProfiles() {
const modal = associatedProfilesModal.value;
if (!modal?.isBookmark || !modal.bookmarkUrl || !bookmarkModalSelectedProfileIds.value.length) {
return;
}
requestBookmarkRemoval([
{
url: modal.bookmarkUrl,
profileIds: [...bookmarkModalSelectedProfileIds.value],
},
]);
}
function closeBookmarkRemovalConfirm() {
if (bookmarkDeleteBusy.value) return;
resetBookmarkRemovalConfirmState();
}
function closeBookmarkRemovalResult() {
bookmarkRemovalResultOpen.value = false;
bookmarkRemovalResults.value = [];
bookmarkRemovalError.value = "";
}
function applyBookmarkRemovalResults(results: RemoveBookmarksResponse["results"]) {
const browser = currentBrowser.value;
if (!browser) return;
for (const result of results) {
if (result.error || result.removedCount === 0) continue;
const bookmark = browser.bookmarks.find((item) => item.url === result.url);
if (!bookmark) continue;
bookmark.profileIds = bookmark.profileIds.filter((id) => id !== result.profileId);
bookmark.profiles = bookmark.profiles.filter((profile) => profile.id !== result.profileId);
}
browser.bookmarks = browser.bookmarks.filter((bookmark) => bookmark.profileIds.length > 0);
browser.stats.bookmarkCount = browser.bookmarks.length;
bookmarkSelectedUrls.value = bookmarkSelectedUrls.value.filter((selectedUrl) =>
browser.bookmarks.some((bookmark) => bookmark.url === selectedUrl),
);
if (associatedProfilesModal.value?.isBookmark) {
const currentBookmark = browser.bookmarks.find(
(bookmark) => bookmark.url === associatedProfilesModal.value?.bookmarkUrl,
);
if (!currentBookmark) {
associatedProfilesModal.value = null;
bookmarkModalSelectedProfileIds.value = [];
} else {
associatedProfilesModal.value = {
...associatedProfilesModal.value,
title: `${currentBookmark.title} Profiles`,
profiles: currentBookmark.profiles,
};
bookmarkModalSelectedProfileIds.value = bookmarkModalSelectedProfileIds.value.filter((id) =>
currentBookmark.profiles.some((profile) => profile.id === id),
);
}
}
}
async function confirmBookmarkRemoval() {
const browser = currentBrowser.value;
const removals = bookmarkRemovalConfirmRemovals.value.map((item) => ({
url: item.url,
profileIds: [...item.profileIds],
}));
if (!browser || !removals.length) return;
bookmarkDeleteBusy.value = true;
bookmarkRemovalError.value = "";
bookmarkRemovalResults.value = [];
bookmarkRemovalResultOpen.value = false;
try {
const input: RemoveBookmarksInput = {
browserId: browser.browserId,
removals,
};
const result = await invoke<RemoveBookmarksResponse>("remove_bookmarks", { input });
applyBookmarkRemovalResults(result.results);
bookmarkRemovalResults.value = result.results;
resetBookmarkRemovalConfirmState();
bookmarkRemovalResultOpen.value = true;
} catch (removeError) {
resetBookmarkRemovalConfirmState();
bookmarkRemovalError.value =
removeError instanceof Error ? removeError.message : "Failed to remove bookmarks.";
bookmarkRemovalResultOpen.value = true;
} finally {
bookmarkDeleteBusy.value = false;
}
}
function toggleExtensionSelection(extensionId: string) {
@@ -729,6 +947,14 @@ export function useBrowserManager() {
activeSection,
associatedProfilesModal,
bookmarkSortKey,
bookmarkDeleteBusy,
bookmarkModalSelectedProfileIds,
bookmarkRemovalConfirmBookmarkCount: computed(bookmarkRemovalConfirmBookmarkCount),
bookmarkRemovalConfirmProfileCount: computed(bookmarkRemovalConfirmProfileCount),
bookmarkRemovalError,
bookmarkRemovalResultOpen,
bookmarkRemovalResults,
bookmarkSelectedUrls,
browserConfigs,
browserMonogram,
browsers,
@@ -739,6 +965,10 @@ export function useBrowserManager() {
createCustomBrowserConfig,
currentBrowser,
deleteCustomBrowserConfig,
deleteBookmarkFromAllProfiles,
deleteBookmarkFromProfile,
deleteSelectedBookmarkProfiles,
deleteSelectedBookmarks,
deleteExtensionFromAllProfiles,
deleteExtensionFromProfile,
deleteSelectedExtensionProfiles,
@@ -787,10 +1017,17 @@ export function useBrowserManager() {
sortedProfiles,
closeExtensionRemovalConfirm,
closeExtensionRemovalResult,
closeBookmarkRemovalConfirm,
closeBookmarkRemovalResult,
confirmExtensionRemoval,
confirmBookmarkRemoval,
cleanupHistoryForProfile,
toggleAllBookmarks,
toggleAllExtensions,
toggleAllBookmarkModalProfiles,
toggleAllExtensionModalProfiles,
toggleBookmarkModalProfileSelection,
toggleBookmarkSelection,
toggleExtensionModalProfileSelection,
toggleExtensionSelection,
toggleAllHistoryProfiles,

View File

@@ -72,15 +72,29 @@ export type RemoveExtensionsInput = {
removals: ExtensionRemovalRequest[];
};
export type RemoveBookmarksInput = {
browserId: string;
removals: BookmarkRemovalRequest[];
};
export type ExtensionRemovalRequest = {
extensionId: string;
profileIds: string[];
};
export type BookmarkRemovalRequest = {
url: string;
profileIds: string[];
};
export type RemoveExtensionsResponse = {
results: RemoveExtensionResult[];
};
export type RemoveBookmarksResponse = {
results: RemoveBookmarkResult[];
};
export type RemoveExtensionResult = {
extensionId: string;
profileId: string;
@@ -89,6 +103,15 @@ export type RemoveExtensionResult = {
error: string | null;
};
export type RemoveBookmarkResult = {
url: string;
profileId: string;
removedCount: number;
removedFiles: string[];
skippedFiles: string[];
error: string | null;
};
export type AssociatedProfileSummary = {
id: string;
name: string;