130 lines
3.4 KiB
Vue
130 lines
3.4 KiB
Vue
<script setup lang="ts">
|
|
import { t } from '@/i18n'
|
|
|
|
import { computed, onMounted, ref, watch } from 'vue'
|
|
import AuthorAvatar from '@/components/AuthorAvatar.vue'
|
|
import DirectoryField from '@/components/DirectoryField.vue'
|
|
import ImageUploader from '@/components/ImageUploader.vue'
|
|
import MediaThumb from '@/components/MediaThumb.vue'
|
|
import type { MediaItem } from '@/composables/useMedia'
|
|
import { useMediaNames } from '@/composables/useMediaNames'
|
|
import type { ImageChoice } from '@/lib/imageChoice'
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
label: string
|
|
help?: string
|
|
picker: ImageChoice
|
|
avatarName?: string
|
|
square?: boolean
|
|
disabled?: boolean
|
|
autofocusChoose?: boolean
|
|
}>(),
|
|
{ help: '', avatarName: '', square: false, disabled: false, autofocusChoose: false },
|
|
)
|
|
|
|
const imageId = defineModel<number | null>({ default: null })
|
|
const item = defineModel<MediaItem | null>('item', { default: null })
|
|
|
|
const names = useMediaNames()
|
|
const shownItem = computed(() => item.value ?? names.itemOf(imageId.value))
|
|
|
|
const pickerOpen = ref(false)
|
|
const BUTTON_CLASS =
|
|
'h-11 gap-1.5 border border-accented bg-default px-2.5 text-xs text-highlighted ring-0 md:h-7'
|
|
|
|
const hasImage = computed(() => imageId.value !== null)
|
|
const chooseLabel = computed(() => (hasImage.value ? t('Change image') : t('Choose image')))
|
|
const caption = computed(() => {
|
|
if (shownItem.value) {
|
|
return shownItem.value.filename
|
|
}
|
|
return imageId.value === null ? '' : t('Image #{0}', [imageId.value])
|
|
})
|
|
|
|
function resolveMissing() {
|
|
if (!item.value) {
|
|
names.resolve([imageId.value])
|
|
}
|
|
}
|
|
|
|
onMounted(resolveMissing)
|
|
watch(imageId, resolveMissing)
|
|
|
|
function choose(picked: MediaItem) {
|
|
item.value = picked
|
|
imageId.value = picked.id
|
|
}
|
|
|
|
function remove() {
|
|
item.value = null
|
|
imageId.value = null
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<DirectoryField :label="label" optional :help="help">
|
|
<div class="flex items-center gap-3">
|
|
<template v-if="square">
|
|
<MediaThumb
|
|
v-if="shownItem"
|
|
:item="shownItem"
|
|
size="md"
|
|
class="size-16 border border-default"
|
|
/>
|
|
<AuthorAvatar v-else :name="avatarName" :size="64" />
|
|
</template>
|
|
<div
|
|
v-else
|
|
class="flex h-12.5 w-24 shrink-0 items-center justify-center overflow-hidden border text-muted"
|
|
:class="
|
|
hasImage
|
|
? 'border-accented bg-(--rc-sand)'
|
|
: 'border-dashed border-accented bg-default'
|
|
"
|
|
>
|
|
<MediaThumb v-if="shownItem" :item="shownItem" size="sm" class="size-full" />
|
|
<UIcon v-else name="i-lucide-image" class="size-4.5" />
|
|
</div>
|
|
<div class="flex min-w-0 flex-1 flex-col gap-1.5">
|
|
<span
|
|
v-if="caption"
|
|
class="truncate font-mono text-xs/5 text-highlighted"
|
|
data-testid="media-field-caption"
|
|
>{{ caption }}</span
|
|
>
|
|
<div class="flex flex-wrap gap-2">
|
|
<UButton
|
|
color="neutral"
|
|
variant="outline"
|
|
icon="i-lucide-images"
|
|
:disabled="disabled"
|
|
:class="BUTTON_CLASS"
|
|
:ui="{ leadingIcon: 'size-3.5' }"
|
|
:data-autofocus="autofocusChoose ? '' : undefined"
|
|
@click="pickerOpen = true"
|
|
>
|
|
{{ chooseLabel }}
|
|
</UButton>
|
|
<UButton
|
|
v-if="hasImage"
|
|
color="neutral"
|
|
variant="outline"
|
|
:disabled="disabled"
|
|
:class="BUTTON_CLASS"
|
|
@click="remove"
|
|
>
|
|
{{ t('Remove') }}
|
|
</UButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<ImageUploader
|
|
v-model:open="pickerOpen"
|
|
:choose="props.picker"
|
|
@inserted="choose"
|
|
@uploaded="choose"
|
|
/>
|
|
</DirectoryField>
|
|
</template>
|