feat: media duplicate purge, part 3 of 3: the SPA (Settings "Media cleanup" card, PurgeDuplicatesDialog, Media Duplicates toolbar button)

This commit is contained in:
fsylvain committed 2026-10-05 23:57:50 +02:00
1 parent 1a55a39425
commit 2303b12377
26 files changed
+3856 -83

No files matched your search

+52
View File
@@ -21,6 +21,7 @@ import MediaTable from '@/components/MediaTable.vue'
import MediaToolbar from '@/components/MediaToolbar.vue'
import MediaUploadPanel from '@/components/MediaUploadPanel.vue'
import MergeMediaDialog from '@/components/MergeMediaDialog.vue'
import PurgeDuplicatesDialog from '@/components/PurgeDuplicatesDialog.vue'
import RenameMediaDialog from '@/components/RenameMediaDialog.vue'
import { ApiError } from '@/composables/useApi'
import {
@@ -32,6 +33,7 @@ import {
} from '@/composables/useMedia'
import { useMediaSelection } from '@/composables/useMediaSelection'
import { useMediaUpload } from '@/composables/useMediaUpload'
import type { PurgeResult } from '@/composables/useMediaPurge'
import { toastsAboveBulkBar } from '@/composables/useToastOffset'
import { groupDuplicates, type DuplicateGroup } from '@/lib/mediaDuplicates'
import { joinNames } from '@/lib/mediaFormat'
@@ -41,7 +43,9 @@ import {
toRouteQuery,
type MediaControls,
} from '@/lib/mediaQuery'
import { purgedToast } from '@/lib/mediaPurge'
import { RATE_LIMITED_TEXT, RATE_LIMITED_TITLE, UPLOAD_ACCEPT } from '@/lib/mediaUploadRows'
import { useSessionStore } from '@/stores/session'
const DESKTOP_QUERY = '(min-width: 48rem)'
const TOAST_DURATION_MS = 5000
@@ -71,6 +75,7 @@ const OFFLINE_COPY =
const route = useRoute()
const router = useRouter()
const toast = useLocalizedToast()
const session = useSessionStore()
const media = useMedia()
const selection = useMediaSelection(media.items)
const upload = useMediaUpload({ onAdded: refreshInBackground })
@@ -91,12 +96,14 @@ const deleteOpen = ref(false)
const bulkDeleteOpen = ref(false)
const bulkTargets = ref<MediaItem[]>([])
const mergeTarget = ref<{ group: DuplicateGroup; keep: MediaItem } | null>(null)
const purgeOpen = ref(false)
const dragDepth = ref(0)
const announcementSource = ref<() => string>(() => '')
const announcement = computed(() => announcementSource.value())
const usage = ref<MediaUsageItem[]>([])
const copies = ref<MediaItem[]>([])
let opener: HTMLElement | null = null
let purgeChangedList = false
let skipFocusRestore = false
let pendingSelectId: number | null = null
let bulkDeletedTotal = 0
@@ -138,6 +145,12 @@ const filterLabel = computed(
],
)
const showList = computed(() => ready.value && !nothingToShow.value && !emptyLibrary.value)
const canPurge = computed(
() => session.session?.role === 'owner' || session.session?.role === 'admin',
)
const showPurge = computed(
() => canPurge.value && isDuplicates.value && showList.value && groups.value.length > 0,
)
const dragging = computed(() => dragDepth.value > 0)
const uploadBlocked = computed(() => offline.value || upload.limited.value)
const uploadLabel = computed(() =>
@@ -150,6 +163,7 @@ const dialogOpen = computed(
renameOpen.value ||
deleteOpen.value ||
bulkDeleteOpen.value ||
purgeOpen.value ||
mergeTarget.value !== null,
)
const hasSelection = computed(() => selection.count.value > 0)
@@ -724,6 +738,33 @@ function openMerge(group: DuplicateGroup, keep: MediaItem) {
mergeTarget.value = { group, keep }
}
function openPurge(event?: Event) {
if (offline.value) {
return
}
opener = (event?.currentTarget as HTMLElement | null) ?? null
purgeChangedList = false
purgeOpen.value = true
}
function onPurged(result: PurgeResult) {
toast.add(purgedToast(result))
purgeChangedList = true
}
function onPurgePartial() {
purgeChangedList = true
}
function closePurge() {
purgeOpen.value = false
if (purgeChangedList) {
purgeChangedList = false
refreshInBackground()
}
restoreFocusToOpener(null)
}
function mergeText(merged: number, articlesUpdated: number) {
const copiesText = merged === 1 ? t('1 copy') : t('{0} copies', [merged])
const articlesText =
@@ -873,10 +914,13 @@ async function mergeAction() {
<MediaToolbar
:controls="controls"
:counts="counts"
:show-purge="showPurge"
:purge-disabled="offline"
@update:q="updateControls({ q: $event })"
@update:filter="updateControls({ filter: $event })"
@update:sort="updateControls({ sort: $event })"
@update:view="updateControls({ view: $event })"
@purge="openPurge"
/>
<MediaUploadPanel
@@ -1022,6 +1066,14 @@ async function mergeAction() {
@close="bulkDeleteOpen = false"
/>
<PurgeDuplicatesDialog
v-if="purgeOpen"
@close="closePurge"
@purged="onPurged"
@partial="onPurgePartial"
@review="purgeOpen = false"
/>
<MergeMediaDialog
v-if="mergeTarget"
:keep="mergeTarget.keep"