forked from BRETT/BRETT
Mass-select grid and bulk delete for moderators
- Select mode in the filter row (upload.delete permission): tiles toggle selection in the capture phase, a floating action bar offers select-all and delete with confirmation popover - New POST /api/uploads/bulk-delete endpoint (validated, deduped, max 200 ids) removing rows, files and attachment data - upload-deleted broadcast on bulk and single delete; other clients refresh the grid and drop matching notifications - Also: skeleton tile grid as loading state, dropzone logic moved to the layout-level GlobalDropZone, obsolete ytimg preconnect removed Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -14,7 +14,11 @@ interface TileUpload {
|
||||
commentCount?: number
|
||||
}
|
||||
|
||||
const props = defineProps<{ upload: TileUpload }>()
|
||||
const props = withDefaults(defineProps<{ upload: TileUpload, selectable?: boolean, selected?: boolean }>(), {
|
||||
selectable: false,
|
||||
selected: false,
|
||||
})
|
||||
const emit = defineEmits<{ select: [] }>()
|
||||
const { t } = useI18n()
|
||||
|
||||
const { unreadUploadIds } = useNotifications()
|
||||
@@ -47,7 +51,17 @@ const spanClass = computed(() => {
|
||||
// 1. Ladeindikator oben sofort starten
|
||||
// 2. Thread-Daten + Bild/Video vorladen (min. 600 ms, damit der Indikator sichtbar läuft)
|
||||
// 3. Wenn geladen: viewTransitionName setzen und via startViewTransition navigieren
|
||||
// Capture-Phase: läuft vor NuxtLinks eigenem Klick-Handler — preventDefault +
|
||||
// stopImmediatePropagation verhindert die Navigation sicher in allen Browsern
|
||||
function captureClick(e: MouseEvent) {
|
||||
if (!props.selectable) return
|
||||
e.preventDefault()
|
||||
e.stopImmediatePropagation()
|
||||
emit('select')
|
||||
}
|
||||
|
||||
async function openUpload(e: MouseEvent) {
|
||||
if (props.selectable) return
|
||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return
|
||||
e.preventDefault()
|
||||
if (navigating.value) return
|
||||
@@ -116,10 +130,11 @@ async function openUpload(e: MouseEvent) {
|
||||
:to="`/thread/${upload.id}`"
|
||||
class="upload-tile group block"
|
||||
:class="[spanClass, navigating ? 'opacity-80' : '']"
|
||||
@click.capture="captureClick"
|
||||
@click="openUpload"
|
||||
>
|
||||
<div
|
||||
:class="cn('upload-tile-media relative h-full w-full overflow-hidden rounded-md', hasNotification && 'ring-4 ring-primary')"
|
||||
:class="cn('upload-tile-media relative h-full w-full overflow-hidden rounded-md', selected ? 'ring-4 ring-accent' : hasNotification && 'ring-4 ring-primary')"
|
||||
:style="lastViewedId === upload.id ? 'view-transition-name: upload-media' : undefined"
|
||||
>
|
||||
<NuxtImg
|
||||
@@ -145,6 +160,12 @@ async function openUpload(e: MouseEvent) {
|
||||
<Icon name="ph:music-note-fill" v-if="upload.type === 'audio'" class="size-8 text-muted-foreground" />
|
||||
<Icon name="ph:link-fill" v-else class="size-8 text-muted-foreground" />
|
||||
</div>
|
||||
<span
|
||||
v-if="selectable"
|
||||
:class="cn('absolute top-1.5 left-1.5 z-10 flex size-6 items-center justify-center rounded-full border shadow', selected ? 'border-accent bg-accent text-accent-foreground' : 'border-muted-foreground/40 bg-background/60')"
|
||||
>
|
||||
<Icon v-if="selected" name="ph:check-bold" class="size-3.5" />
|
||||
</span>
|
||||
<span
|
||||
v-if="hasNotification"
|
||||
class="absolute top-1.5 right-1.5 z-10 flex size-6 items-center justify-center rounded-full bg-accent text-accent-foreground shadow"
|
||||
|
||||
Reference in New Issue
Block a user