6104452d2e
Vuetify 4 is a Material-Design-3 styling refresh with revert snippets, not a component-API overhaul. FC's exposure was small: - Bump vuetify ^4, vite-plugin-vuetify ^2.1.0, vue ^3.5, engines node>=24. - Restore the dropped global CSS reset (minimal reset from the upgrade guide) in Vuetify's own low-precedence reset layer, so FC's margin-zeroing assumptions hold. - v-row prop→utility: 'dense' → density=compact (×3), align=center → class=align-center. - v-snackbar: multi-line removed → min-height=68. - v-autocomplete #item slot: item→internalItem (item now aliases raw) in TagPicker + GalleryFilterBar (item.raw.* → internalItem.raw.*). ACCEPTED (cosmetic, operator reviews live per plan #158): MD3 typography (text-body-2 ×73), non-uppercase buttons (v4 dropped the uppercase default), MD3 elevation. CI verifies BUILD only — the LOOK is the live-review pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
120 lines
3.4 KiB
Vue
120 lines
3.4 KiB
Vue
<template>
|
|
<MaintenanceTile
|
|
icon="mdi-image-size-select-small"
|
|
title="Minimum dimensions"
|
|
blurb="Delete images below the import min-width / min-height threshold."
|
|
destructive
|
|
>
|
|
<p class="fc-muted text-body-2 mb-3">
|
|
Mirrors the import-time <code>min_width</code> / <code>min_height</code>
|
|
filter, applied retroactively to the existing library.
|
|
</p>
|
|
|
|
<v-row density="compact">
|
|
<v-col cols="6">
|
|
<v-text-field
|
|
v-model.number="minW" label="Min width (px)" type="number"
|
|
min="0" density="compact" hide-details
|
|
/>
|
|
</v-col>
|
|
<v-col cols="6">
|
|
<v-text-field
|
|
v-model.number="minH" label="Min height (px)" type="number"
|
|
min="0" density="compact" hide-details
|
|
/>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<div class="d-flex align-center mt-3" style="gap: 10px;">
|
|
<v-btn
|
|
color="accent" variant="flat" rounded="pill"
|
|
prepend-icon="mdi-magnify"
|
|
:loading="busy"
|
|
@click="onPreview"
|
|
>Preview</v-btn>
|
|
|
|
<span v-if="preview" class="text-body-2">
|
|
<strong>{{ preview.count }}</strong> image(s) would be deleted.
|
|
</span>
|
|
</div>
|
|
|
|
<v-btn
|
|
v-if="preview && preview.count > 0"
|
|
class="mt-3"
|
|
color="error" variant="flat" rounded="pill"
|
|
prepend-icon="mdi-delete"
|
|
@click="onDeleteClick"
|
|
>Delete {{ preview.count }} matching...</v-btn>
|
|
|
|
<DestructiveConfirmModal
|
|
v-model="showModal"
|
|
action="delete"
|
|
kind="min-dim"
|
|
tier="C"
|
|
:expected-token-override="preview?.confirm_token || ''"
|
|
:projected-counts="projectedCounts"
|
|
:description="`Width < ${minW} OR height < ${minH}`"
|
|
@confirm="onConfirmedDelete"
|
|
/>
|
|
</MaintenanceTile>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { toast } from '../../utils/toast.js'
|
|
import { onMounted, ref } from 'vue'
|
|
|
|
import DestructiveConfirmModal from '../modal/DestructiveConfirmModal.vue'
|
|
import { useCleanupStore } from '../../stores/cleanup.js'
|
|
import MaintenanceTile from '../common/MaintenanceTile.vue'
|
|
|
|
// Backend's preview response hands the full Tier-C confirm token back
|
|
// as `confirm_token` (e.g. `delete-min-dim-1a2b3c4d`); passed straight
|
|
// to the modal via `expected-token-override`. We previously
|
|
// reconstructed via Web Crypto's SHA-256, but `crypto.subtle` is
|
|
// Secure-Context-gated and undefined on plain-HTTP origins, so the
|
|
// Delete button silently swallowed the TypeError. Operator-flagged
|
|
// 2026-05-27.
|
|
|
|
const store = useCleanupStore()
|
|
const minW = ref(0)
|
|
const minH = ref(0)
|
|
const preview = ref(null)
|
|
const busy = ref(false)
|
|
const showModal = ref(false)
|
|
const projectedCounts = ref({})
|
|
|
|
onMounted(async () => {
|
|
await store.loadDefaults()
|
|
minW.value = store.defaults.min_width
|
|
minH.value = store.defaults.min_height
|
|
})
|
|
|
|
async function onPreview() {
|
|
busy.value = true
|
|
try {
|
|
preview.value = await store.previewMinDim(minW.value, minH.value)
|
|
} catch (e) {
|
|
toast({ text: `Preview failed: ${e.message}`, type: 'error' })
|
|
} finally {
|
|
busy.value = false
|
|
}
|
|
}
|
|
|
|
function onDeleteClick() {
|
|
projectedCounts.value = { 'Images to delete': preview.value.count }
|
|
showModal.value = true
|
|
}
|
|
|
|
async function onConfirmedDelete(token) {
|
|
try {
|
|
const res = await store.deleteMinDim(minW.value, minH.value, token)
|
|
toast({
|
|
text: `Deleted ${res.deleted} image(s)`, type: 'success',
|
|
})
|
|
preview.value = null
|
|
} catch (e) {
|
|
toast({ text: `Delete failed: ${e.message}`, type: 'error' })
|
|
}
|
|
}
|
|
</script>
|