322 lines
7.9 KiB
Vue
322 lines
7.9 KiB
Vue
<script setup lang="ts">
|
|
import { t } from '@/i18n'
|
|
|
|
import { computed, ref, watch } from 'vue'
|
|
import { useMediaQuery } from '@vueuse/core'
|
|
import MediaArticleStatusBadge from '@/components/MediaArticleStatusBadge.vue'
|
|
import MediaThumb from '@/components/MediaThumb.vue'
|
|
import MediaVariantsTab from '@/components/variants/MediaVariantsTab.vue'
|
|
import type { MediaItem, MediaUsageItem } from '@/composables/useMedia'
|
|
import {
|
|
formatDimensions,
|
|
formatMediaDate,
|
|
formatSize,
|
|
formatType,
|
|
joinNames,
|
|
pluralArticles,
|
|
} from '@/lib/mediaFormat'
|
|
import { PANEL_TAB_TRIGGER } from '@/lib/tabStyles'
|
|
import { useCanManageVariants } from '@/composables/useCanManageVariants'
|
|
|
|
const DESKTOP_QUERY = '(min-width: 48rem)'
|
|
const ACTION_BUTTON =
|
|
'h-11 justify-center border border-accented bg-default px-3 text-sm ring-0 max-md:flex-1 md:h-8'
|
|
|
|
const props = defineProps<{
|
|
item: MediaItem | null
|
|
usage: MediaUsageItem[]
|
|
copies: MediaItem[]
|
|
offline: boolean
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
'copy-url': []
|
|
rename: []
|
|
delete: []
|
|
'review-duplicates': []
|
|
gone: []
|
|
}>()
|
|
|
|
const open = defineModel<boolean>('open', { default: false })
|
|
const isDesktop = useMediaQuery(DESKTOP_QUERY)
|
|
const canManageVariants = useCanManageVariants()
|
|
const panelTab = ref<'details' | 'variants'>('details')
|
|
const variantsVisited = ref(false)
|
|
|
|
const tabItems = computed(() => [
|
|
{
|
|
get label() {
|
|
return t('Details')
|
|
},
|
|
value: 'details',
|
|
},
|
|
{
|
|
get label() {
|
|
return t('Variants')
|
|
},
|
|
value: 'variants',
|
|
},
|
|
])
|
|
const tabModel = computed({
|
|
get: () => panelTab.value,
|
|
set: (value: string) => {
|
|
panelTab.value = value === 'variants' ? 'variants' : 'details'
|
|
if (panelTab.value === 'variants') {
|
|
variantsVisited.value = true
|
|
}
|
|
},
|
|
})
|
|
|
|
watch(
|
|
() => [open.value, props.item?.id],
|
|
() => {
|
|
panelTab.value = 'details'
|
|
variantsVisited.value = false
|
|
},
|
|
)
|
|
|
|
const closeButton = computed(() => ({
|
|
color: 'neutral' as const,
|
|
variant: 'ghost' as const,
|
|
icon: isDesktop.value ? 'i-lucide-x' : 'i-lucide-arrow-left',
|
|
class: 'text-muted',
|
|
}))
|
|
|
|
const metadata = computed(() => {
|
|
const item = props.item
|
|
if (!item) {
|
|
return []
|
|
}
|
|
return [
|
|
{
|
|
get label() {
|
|
return t('Type')
|
|
},
|
|
value: formatType(item.content_type),
|
|
},
|
|
{
|
|
get label() {
|
|
return t('Size')
|
|
},
|
|
value: formatSize(item.size),
|
|
},
|
|
{
|
|
get label() {
|
|
return t('Dimensions')
|
|
},
|
|
value: formatDimensions(item.width, item.height),
|
|
},
|
|
{
|
|
get label() {
|
|
return t('Uploaded')
|
|
},
|
|
value: formatMediaDate(item.created_at),
|
|
},
|
|
{
|
|
get label() {
|
|
return t('Uploaded by')
|
|
},
|
|
value: item.uploaded_by,
|
|
},
|
|
]
|
|
})
|
|
|
|
const displayUrl = computed(() => `${window.location.host}${props.item?.url ?? ''}`)
|
|
|
|
const duplicateAlert = computed(() => {
|
|
if (!props.item?.duplicate || props.copies.length === 0) {
|
|
return null
|
|
}
|
|
const names = props.copies.map((copy) => copy.filename)
|
|
return {
|
|
get title() {
|
|
return t('Identical to {0} other {1}', [
|
|
props.copies.length,
|
|
props.copies.length === 1 ? 'image' : 'images',
|
|
])
|
|
},
|
|
get description() {
|
|
return t('{0} {1} the same content.', [
|
|
joinNames(names),
|
|
names.length === 1 ? 'has' : 'have',
|
|
])
|
|
},
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<USlideover
|
|
v-model:open="open"
|
|
side="right"
|
|
:title="item?.filename ?? ''"
|
|
:description="t('Image details')"
|
|
:close="closeButton"
|
|
:ui="{
|
|
content: 'w-full max-w-none md:max-w-115',
|
|
header: 'h-14 min-h-14 border-b border-default px-5 py-0 sm:px-5 max-md:px-1.5 max-md:sm:px-1.5',
|
|
wrapper: 'min-w-0 flex-1 max-md:ps-11 md:pe-9',
|
|
title: 'truncate text-lg font-semibold text-highlighted',
|
|
description: 'sr-only',
|
|
close: 'top-1.75 size-11 justify-center p-0 max-md:start-1.5 md:top-3.5 md:end-5 md:size-7',
|
|
body: 'p-4 md:p-5',
|
|
}"
|
|
>
|
|
<template #body>
|
|
<div v-if="item" class="flex flex-col gap-4">
|
|
<div
|
|
v-if="canManageVariants"
|
|
data-testid="panel-tabs"
|
|
class="-mx-4 -mt-4 h-11 border-b border-default px-4 md:-mx-5 md:-mt-5 md:mb-1 md:h-10 md:px-5"
|
|
>
|
|
<UTabs
|
|
v-model="tabModel"
|
|
:items="tabItems"
|
|
variant="link"
|
|
color="neutral"
|
|
:content="false"
|
|
class="-mb-px"
|
|
:ui="{
|
|
list: 'gap-5 border-b-0 p-0',
|
|
trigger: PANEL_TAB_TRIGGER,
|
|
}"
|
|
/>
|
|
</div>
|
|
|
|
<MediaVariantsTab
|
|
v-if="canManageVariants && variantsVisited"
|
|
v-show="panelTab === 'variants'"
|
|
:key="item.id"
|
|
:item="item"
|
|
@gone="emit('gone')"
|
|
/>
|
|
|
|
<div v-show="panelTab === 'details'" class="flex flex-col gap-5">
|
|
<MediaThumb
|
|
:item="item"
|
|
size="xl"
|
|
class="aspect-4/3 w-full border border-default md:aspect-16/10"
|
|
/>
|
|
|
|
<div class="flex gap-2 max-md:flex-wrap">
|
|
<UButton
|
|
color="neutral"
|
|
variant="outline"
|
|
icon="i-lucide-link"
|
|
:class="ACTION_BUTTON"
|
|
@click="emit('copy-url')"
|
|
>
|
|
{{ t('Copy URL') }}
|
|
</UButton>
|
|
<UButton
|
|
color="neutral"
|
|
variant="outline"
|
|
icon="i-lucide-pencil"
|
|
:disabled="offline"
|
|
:class="ACTION_BUTTON"
|
|
@click="emit('rename')"
|
|
>
|
|
{{ t('Rename') }}
|
|
</UButton>
|
|
<UButton
|
|
color="neutral"
|
|
variant="outline"
|
|
icon="i-lucide-trash-2"
|
|
:disabled="offline"
|
|
:class="ACTION_BUTTON"
|
|
class="text-error"
|
|
@click="emit('delete')"
|
|
>
|
|
{{ t('Delete') }}
|
|
</UButton>
|
|
</div>
|
|
|
|
<UAlert
|
|
v-if="duplicateAlert"
|
|
data-testid="duplicate-alert"
|
|
color="info"
|
|
variant="soft"
|
|
orientation="vertical"
|
|
:close="false"
|
|
icon="i-lucide-copy"
|
|
:title="duplicateAlert.title"
|
|
:description="duplicateAlert.description"
|
|
:ui="{ actions: 'mt-2 w-full', description: 'text-wrap' }"
|
|
>
|
|
<template #actions>
|
|
<UButton
|
|
color="neutral"
|
|
variant="outline"
|
|
icon="i-lucide-arrow-right"
|
|
block
|
|
class="h-11 gap-1.5 border border-accented bg-default px-2.5 text-xs text-highlighted ring-0 md:h-7"
|
|
:ui="{ leadingIcon: 'size-3.5' }"
|
|
@click="emit('review-duplicates')"
|
|
>
|
|
{{ t('Review duplicates') }}
|
|
</UButton>
|
|
</template>
|
|
</UAlert>
|
|
|
|
<dl class="grid grid-cols-[110px_minmax(0,1fr)] gap-x-3 gap-y-2 text-toned">
|
|
<template v-for="row in metadata" :key="row.label">
|
|
<dt class="text-muted">{{ row.label }}</dt>
|
|
<dd class="m-0 text-highlighted">{{ row.value }}</dd>
|
|
</template>
|
|
<dt class="text-muted">URL</dt>
|
|
<dd class="m-0 truncate font-mono text-xs/5 text-highlighted">
|
|
{{ displayUrl }}
|
|
</dd>
|
|
</dl>
|
|
|
|
<section class="flex flex-col gap-2">
|
|
<h3 class="text-base font-semibold text-highlighted">
|
|
{{
|
|
item.used_in > 0
|
|
? t('Used in {0}', [pluralArticles(item.used_in)])
|
|
: t('Not used in any article')
|
|
}}
|
|
</h3>
|
|
<template v-if="item.used_in > 0">
|
|
<ul v-if="usage.length > 0" class="border border-default">
|
|
<li
|
|
v-for="(article, index) in usage"
|
|
:key="article.id"
|
|
data-testid="usage-row"
|
|
class="flex min-h-12 items-center gap-2 px-3 md:min-h-10"
|
|
:class="{ 'border-t border-muted': index > 0 }"
|
|
>
|
|
<RouterLink
|
|
:to="`/article/${article.id}/edit`"
|
|
class="min-w-0 flex-1 truncate font-medium text-highlighted underline underline-offset-3"
|
|
>
|
|
{{ article.title }}
|
|
</RouterLink>
|
|
<MediaArticleStatusBadge
|
|
:published="article.published"
|
|
size="md"
|
|
/>
|
|
<UIcon
|
|
name="i-lucide-arrow-up-right"
|
|
class="size-3.5 shrink-0 text-muted"
|
|
/>
|
|
</li>
|
|
</ul>
|
|
<p class="text-xs text-muted">
|
|
{{
|
|
t(
|
|
'Counted from the text of your articles each time you open this page.',
|
|
)
|
|
}}
|
|
</p>
|
|
</template>
|
|
<p v-else class="text-muted">
|
|
{{ t('This image is safe to delete. No article shows it.') }}
|
|
</p>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</USlideover>
|
|
</template>
|