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

@@ -0,0 +1,129 @@
import { onBeforeUnmount, ref } from 'vue'
import {
useMediaPurge,
type PurgeSettings,
type PurgeSettingsPatch,
} from '@/composables/useMediaPurge'
import { isForbidden, isHandledGlobally } from '@/lib/mediaPurge'
export type CleanupLoadState = 'loading' | 'ready' | 'error'
export type SaveStatus = 'idle' | 'saving' | 'saved'
export type SaveFailure = 'enabled' | 'days'
const SKELETON_DELAY_MS = 150
const SAVED_VISIBLE_MS = 2000
export function useMediaCleanupSettings(onForbidden: () => void) {
const { getSettings, updateSettings } = useMediaPurge()
const settings = ref<PurgeSettings | null>(null)
const loadState = ref<CleanupLoadState>('loading')
const showSkeleton = ref(false)
const saveStatus = ref<SaveStatus>('idle')
const saveFailure = ref<SaveFailure | null>(null)
let skeletonTimer: ReturnType<typeof setTimeout> | null = null
let savedTimer: ReturnType<typeof setTimeout> | null = null
let latestLoad = 0
let pendingSaves = 0
function clearSkeletonTimer() {
if (skeletonTimer !== null) {
clearTimeout(skeletonTimer)
skeletonTimer = null
}
}
function clearSavedTimer() {
if (savedTimer !== null) {
clearTimeout(savedTimer)
savedTimer = null
}
}
async function load() {
latestLoad += 1
const ticket = latestLoad
loadState.value = 'loading'
showSkeleton.value = false
clearSkeletonTimer()
skeletonTimer = setTimeout(() => {
showSkeleton.value = true
}, SKELETON_DELAY_MS)
try {
const loaded = await getSettings()
if (ticket === latestLoad) {
settings.value = loaded
loadState.value = 'ready'
}
} catch (error) {
if (ticket !== latestLoad) {
return
}
if (isHandledGlobally(error)) {
if (isForbidden(error)) {
onForbidden()
}
return
}
loadState.value = 'error'
} finally {
if (ticket === latestLoad) {
clearSkeletonTimer()
showSkeleton.value = false
}
}
}
async function refresh() {
try {
settings.value = await getSettings()
} catch (error) {
if (isForbidden(error)) {
onForbidden()
}
}
}
function finishSaving(succeeded: boolean) {
if (pendingSaves > 0) {
return
}
if (!succeeded) {
saveStatus.value = 'idle'
return
}
saveStatus.value = 'saved'
savedTimer = setTimeout(() => {
saveStatus.value = 'idle'
}, SAVED_VISIBLE_MS)
}
async function save(patch: PurgeSettingsPatch, field: SaveFailure) {
pendingSaves += 1
clearSavedTimer()
saveStatus.value = 'saving'
let succeeded = false
try {
settings.value = await updateSettings(patch)
saveFailure.value = null
succeeded = true
} catch (error) {
if (isForbidden(error)) {
onForbidden()
} else if (!isHandledGlobally(error)) {
saveFailure.value = field
}
} finally {
pendingSaves -= 1
finishSaving(succeeded)
}
return succeeded
}
onBeforeUnmount(() => {
clearSkeletonTimer()
clearSavedTimer()
})
return { settings, loadState, showSkeleton, saveStatus, saveFailure, load, refresh, save }
}