184 lines
5.2 KiB
Vue
184 lines
5.2 KiB
Vue
<script setup lang="ts">
|
|
import { t } from '@/i18n'
|
|
|
|
import MediaThumb from '@/components/MediaThumb.vue'
|
|
import type { UploadRow } from '@/composables/useMediaUpload'
|
|
import { formatUploadProgress } from '@/lib/imageUpload'
|
|
import { formatMediaDate, formatSize } from '@/lib/mediaFormat'
|
|
import {
|
|
ADDED_TEXT,
|
|
DUPLICATE_TITLE,
|
|
FAILED_TEXT,
|
|
FAILED_TITLE,
|
|
duplicateText,
|
|
rejectionText,
|
|
rejectionTitle,
|
|
type UploadHeader,
|
|
} from '@/lib/mediaUploadRows'
|
|
|
|
const BUTTON =
|
|
'h-11 gap-1.5 border border-accented bg-default px-2.5 text-xs text-highlighted ring-0 max-md:flex-1 md:h-7'
|
|
const PERCENT = 100
|
|
|
|
defineProps<{
|
|
rows: UploadRow[]
|
|
header: UploadHeader
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
'use-existing': [key: number]
|
|
'upload-anyway': [key: number]
|
|
retry: [key: number]
|
|
'cancel-row': [key: number]
|
|
'cancel-all': []
|
|
dismiss: []
|
|
}>()
|
|
|
|
function thumbItem(row: UploadRow) {
|
|
return {
|
|
url: row.previewUrl ?? '',
|
|
content_type: row.file.type,
|
|
sha256: String(row.tone),
|
|
}
|
|
}
|
|
|
|
function percent(row: UploadRow) {
|
|
return row.total > 0 ? Math.round((row.loaded / row.total) * PERCENT) : 0
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<section
|
|
data-testid="media-upload-panel"
|
|
:aria-label="t('Uploads')"
|
|
class="flex flex-col border border-default bg-default"
|
|
>
|
|
<div class="flex items-center gap-3 px-3.5 py-3">
|
|
<div class="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
<span data-testid="upload-title" class="font-semibold text-highlighted">{{
|
|
header.title
|
|
}}</span>
|
|
<span data-testid="upload-sub" class="text-muted">{{ header.sub }}</span>
|
|
</div>
|
|
<UButton
|
|
color="neutral"
|
|
variant="outline"
|
|
class="h-11 border border-accented bg-default px-3 text-xs text-highlighted ring-0 md:h-7"
|
|
@click="header.phase === 'working' ? emit('cancel-all') : emit('dismiss')"
|
|
>
|
|
{{ header.phase === 'working' ? t('Cancel all') : t('Dismiss') }}
|
|
</UButton>
|
|
</div>
|
|
|
|
<div
|
|
v-for="row in rows"
|
|
:key="row.key"
|
|
data-testid="upload-row"
|
|
:data-status="row.status"
|
|
class="flex items-start gap-3 border-t border-muted px-3.5 py-3"
|
|
>
|
|
<MediaThumb :item="thumbItem(row)" size="sm" class="size-10 border border-default" />
|
|
<div class="flex min-w-0 flex-1 flex-col gap-1.5">
|
|
<div class="flex justify-between gap-2">
|
|
<span class="truncate font-medium text-highlighted">{{ row.name }}</span>
|
|
<span class="whitespace-nowrap text-muted">{{ formatSize(row.size) }}</span>
|
|
</div>
|
|
|
|
<template v-if="row.status === 'waiting'">
|
|
<span class="text-muted">{{ t('Waiting') }}</span>
|
|
</template>
|
|
|
|
<template v-else-if="row.status === 'uploading'">
|
|
<span>{{ formatUploadProgress(row.loaded, row.total) }}</span>
|
|
<UProgress
|
|
:model-value="percent(row)"
|
|
size="sm"
|
|
:ui="{
|
|
base: 'rounded-none',
|
|
indicator: 'rounded-none',
|
|
}"
|
|
/>
|
|
</template>
|
|
|
|
<template v-else-if="row.status === 'added'">
|
|
<span class="flex items-center gap-1.5 text-success">
|
|
<UIcon name="i-lucide-circle-check" class="size-3.5 shrink-0" />
|
|
{{ t(ADDED_TEXT) }}
|
|
</span>
|
|
</template>
|
|
|
|
<template v-else-if="row.status === 'duplicate' && row.existing">
|
|
<span class="font-semibold text-highlighted">{{ t(DUPLICATE_TITLE) }}</span>
|
|
<span class="text-xs/5 text-toned">{{
|
|
duplicateText(row.existing, formatMediaDate)
|
|
}}</span>
|
|
<div class="flex flex-wrap gap-2">
|
|
<UButton
|
|
color="neutral"
|
|
variant="outline"
|
|
:class="BUTTON"
|
|
@click="emit('use-existing', row.key)"
|
|
>
|
|
{{ t('Use existing') }}
|
|
</UButton>
|
|
<UButton
|
|
color="neutral"
|
|
variant="outline"
|
|
:class="BUTTON"
|
|
@click="emit('upload-anyway', row.key)"
|
|
>
|
|
{{ t('Upload anyway') }}
|
|
</UButton>
|
|
</div>
|
|
</template>
|
|
|
|
<template v-else-if="row.status === 'rejected' && row.rejection">
|
|
<span class="flex items-center gap-1.5 font-semibold text-error">
|
|
<UIcon name="i-lucide-file-x" class="size-3.5 shrink-0" />
|
|
{{ rejectionTitle(row.rejection, row.name, row.size) }}
|
|
</span>
|
|
<span class="text-xs/5 text-toned">{{ rejectionText(row.rejection) }}</span>
|
|
</template>
|
|
|
|
<template v-else-if="row.status === 'failed'">
|
|
<span class="flex items-center gap-1.5 font-semibold text-error">
|
|
<UIcon name="i-lucide-circle-x" class="size-3.5 shrink-0" />
|
|
{{ t(FAILED_TITLE) }}
|
|
</span>
|
|
<span class="text-xs/5 text-toned">{{ t(FAILED_TEXT) }}</span>
|
|
<div class="flex">
|
|
<UButton
|
|
color="neutral"
|
|
variant="outline"
|
|
icon="i-lucide-rotate-cw"
|
|
:class="BUTTON"
|
|
:ui="{ leadingIcon: 'size-3.5' }"
|
|
@click="emit('retry', row.key)"
|
|
>
|
|
{{ t('Try again') }}
|
|
</UButton>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
<UButton
|
|
v-if="row.status === 'uploading'"
|
|
color="neutral"
|
|
variant="ghost"
|
|
icon="i-lucide-x"
|
|
:aria-label="t('Cancel upload of {0}', [row.name])"
|
|
class="size-6 justify-center text-muted"
|
|
:ui="{ leadingIcon: 'size-3.5' }"
|
|
@click="emit('cancel-row', row.key)"
|
|
/>
|
|
</div>
|
|
|
|
<div
|
|
v-if="header.summary"
|
|
data-testid="upload-summary"
|
|
class="border-t border-default bg-elevated px-3.5 py-3 font-medium text-toned"
|
|
>
|
|
{{ header.summary }}
|
|
</div>
|
|
</section>
|
|
</template>
|