feat: image variants per image
This commit is contained in:
1 parent
f567866161
commit
951fef18ba
92 files changed
+8695
-413
No files matched your search
@@ -1,10 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import { computed } from 'vue'
|
||||
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,
|
||||
@@ -14,6 +15,8 @@ import {
|
||||
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 =
|
||||
@@ -31,10 +34,46 @@ const emit = defineEmits<{
|
||||
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,
|
||||
@@ -124,126 +163,158 @@ const duplicateAlert = computed(() => {
|
||||
}"
|
||||
>
|
||||
<template #body>
|
||||
<div v-if="item" 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
|
||||
<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"
|
||||
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>
|
||||
:content="false"
|
||||
class="-mb-px"
|
||||
:ui="{
|
||||
list: 'gap-5 border-b-0 p-0',
|
||||
trigger: PANEL_TAB_TRIGGER,
|
||||
}"
|
||||
/>
|
||||
</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>
|
||||
<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-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')"
|
||||
icon="i-lucide-link"
|
||||
:class="ACTION_BUTTON"
|
||||
@click="emit('copy-url')"
|
||||
>
|
||||
{{ t('Review duplicates') }}
|
||||
{{ t('Copy URL') }}
|
||||
</UButton>
|
||||
</template>
|
||||
</UAlert>
|
||||
<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>
|
||||
|
||||
<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 }"
|
||||
<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')"
|
||||
>
|
||||
<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('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">
|
||||
{{
|
||||
t(
|
||||
'Counted from the text of your articles each time you open this page.',
|
||||
)
|
||||
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>
|
||||
</template>
|
||||
<p v-else class="text-muted">
|
||||
{{ t('This image is safe to delete. No article shows it.') }}
|
||||
</p>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</USlideover>
|
||||
|
||||
Reference in new issue
Block a user