9deebfa133
The icon+title v-card-title heading (d-flex align-center + gap + <v-icon size=small> + <span>) was hand-rolled identically in 13 cards/dialogs (15 heading instances). Consolidate to <CardHeading icon title> (components/common) with an iconColor prop (error headings) and a default slot for trailing content (spacer+actions, inline status chip). Adopted everywhere the pattern appears — all-or-nothing per the hardened DRY process. Over-DRY guard: plain text-only <v-card-title> one-liners are NOT this pattern and stay; DownloadDetailModal leads with a status CHIP (not an icon), a different concept, left alone. §8b: the only remaining d-flex align-center v-card-title is that intentional variant. Catalog updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
123 lines
3.6 KiB
Vue
123 lines
3.6 KiB
Vue
<template>
|
|
<v-card class="fc-clean-card">
|
|
<CardHeading icon="mdi-image-size-select-small" title="Minimum dimensions" />
|
|
<v-card-text>
|
|
<p class="fc-muted text-body-2 mb-3">
|
|
Find and delete images smaller than the threshold. Mirrors the
|
|
import-time <code>min_width</code> / <code>min_height</code>
|
|
filter, applied retroactively to the existing library.
|
|
</p>
|
|
|
|
<v-row dense>
|
|
<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>
|
|
</v-card-text>
|
|
|
|
<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"
|
|
/>
|
|
</v-card>
|
|
</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 CardHeading from '../common/CardHeading.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>
|
|
|
|
<style scoped>
|
|
.fc-clean-card { border-radius: 8px; }
|
|
</style>
|