128 lines
3.7 KiB
Vue
128 lines
3.7 KiB
Vue
<template>
|
|
<v-card class="fc-clean-card">
|
|
<v-card-title class="d-flex align-center" style="gap: 10px;">
|
|
<v-icon icon="mdi-image-size-select-small" size="small" />
|
|
<span>Minimum dimensions</span>
|
|
</v-card-title>
|
|
<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"
|
|
:run-id="tokenSha8"
|
|
tier="C"
|
|
:projected-counts="projectedCounts"
|
|
:description="`Width < ${minW} OR height < ${minH}`"
|
|
@confirm="onConfirmedDelete"
|
|
/>
|
|
</v-card>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { onMounted, ref } from 'vue'
|
|
|
|
import DestructiveConfirmModal from '../modal/DestructiveConfirmModal.vue'
|
|
import { useCleanupStore } from '../../stores/cleanup.js'
|
|
|
|
const store = useCleanupStore()
|
|
const minW = ref(0)
|
|
const minH = ref(0)
|
|
const preview = ref(null)
|
|
const busy = ref(false)
|
|
const showModal = ref(false)
|
|
const tokenSha8 = ref('')
|
|
const projectedCounts = ref({})
|
|
|
|
onMounted(async () => {
|
|
await store.loadDefaults()
|
|
minW.value = store.defaults.min_width
|
|
minH.value = store.defaults.min_height
|
|
})
|
|
|
|
// SHA-256 truncated to 8 hex chars — matches the backend's
|
|
// _min_dim_token() exactly. Web Crypto rejects MD5 as insecure.
|
|
async function sha8(canon) {
|
|
const enc = new TextEncoder()
|
|
const buf = await crypto.subtle.digest('SHA-256', enc.encode(canon))
|
|
const hex = Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('')
|
|
return hex.slice(0, 8)
|
|
}
|
|
|
|
async function onPreview() {
|
|
busy.value = true
|
|
try {
|
|
preview.value = await store.previewMinDim(minW.value, minH.value)
|
|
} catch (e) {
|
|
window.__fcToast?.({ text: `Preview failed: ${e.message}`, type: 'error' })
|
|
} finally {
|
|
busy.value = false
|
|
}
|
|
}
|
|
|
|
async function onDeleteClick() {
|
|
tokenSha8.value = await sha8(`${minW.value}x${minH.value}`)
|
|
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)
|
|
window.__fcToast?.({
|
|
text: `Deleted ${res.deleted} image(s)`, type: 'success',
|
|
})
|
|
preview.value = null
|
|
} catch (e) {
|
|
window.__fcToast?.({ text: `Delete failed: ${e.message}`, type: 'error' })
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.fc-clean-card { border-radius: 8px; }
|
|
.fc-muted { color: rgb(var(--v-theme-on-surface-variant)); }
|
|
</style>
|