Skip to content
Snippets Groups Projects
ImagePickerDialog.vue 4.68 KiB
<template>
  <ADialog v-model="localIsOpen" is-modal class="tw-w-screen lg:tw-w-[60vw] tw-max-w-[800px]">
    <template #header>
      <p class="tw-text-lg tw-font-semibold tw-m-0">{{ t('imagePicker.title') }}</p>
    </template>

    <div
      v-if="!selectedImage && !showBrowser"
      class="tw-flex tw-flex-wrap tw-items-center tw-gap-2"
    >
      <ImageUploader class="tw-grow md:tw-grow-0" @input="setImage" />
      <button
        type="button"
        class="btn btn-sm btn-secondary tw-flex tw-items-center tw-grow md:tw-grow-0"
        @click.stop="showBrowser = true"
      >
        <icon-carbon-search class="tw-mr-1 tw-w-6 tw-h-6" />
        {{ t('imagePicker.browseImages') }}
      </button>
      <button
        v-if="currentImage"
        type="button"
        class="btn btn-sm btn-secondary tw-flex tw-items-center tw-grow md:tw-grow-0"
        @click.stop="selectedImage = currentImage"
      >
        <icon-carbon-pen class="tw-mr-1 tw-w-6 tw-h-6" />
        {{ t('imagePicker.editCurrentImage') }}
      </button>
    </div>

    <p v-if="error" role="alert" aria-live="assertive" class="alert alert-danger">
      {{ error.message }}
    </p>

    <ImageBrowser v-if="showBrowser" @input="setImage" />
    <ImageEditor v-if="selectedImage" v-model="selectedImage" />

    <template v-if="showBrowser || selectedImage" #footer>
      <div class="tw-flex tw-gap-x-6">
        <button
          v-if="selectedImage"
          :disabled="isSaving"
          type="button"
          class="btn btn-primary"
          @click.stop="selectedImage && saveAndSelectImage(selectedImage)"
        >
          <template v-if="isSelectedImageNew">{{ t('imagePicker.useImage') }}</template>
          <template v-else>{{ t('imagePicker.saveChanges') }}</template>
        </button>
        <button v-if="selectedImage" type="button" class="btn" @click.stop="deselectImage">
          {{ t('imagePicker.abort') }}
        </button>
        <button
          v-if="showBrowser"
          type="button"
          class="btn btn-secondary"
          @click.stop="showBrowser = false"
        >
          {{ t('imagePicker.abort') }}
        </button>
      </div>
    </template>
  </ADialog>
</template>

<script lang="ts" setup>
import { computed, ref } from 'vue'
import { useI18n } from '@/i18n'
import { Image, NewImage, useImage, useImageStore } from '@/stores/images'
import { useAsyncFunction, useUpdatableState } from '@/util'
import ADialog from '../generic/ADialog.vue'
import ImageBrowser from './ImageBrowser.vue'
import ImageEditor from './ImageEditor.vue'
import ImageUploader from './ImageUploader.vue'
import { APIResponseError } from '@/api'

const props = defineProps<{
  isOpen: boolean
  modelValue: number | null
}>()
const emit = defineEmits<{
  (e: 'show', state: boolean): void
  (e: 'update:modelValue', value: number | null): void
}>()
const { t } = useI18n()
const imageStore = useImageStore()
const localIsOpen = useUpdatableState(
  computed(() => props.isOpen),
  (isOpen) => emit('show', isOpen),
)
const error = ref<Error>()
const showBrowser = ref(false)
const selectedImage = ref<Image | NewImage | null>(null)

const currentImage = useImage(computed(() => props.modelValue))
const isSelectedImageNew = computed(() => {
  if (!selectedImage.value) return false
  else if ('id' in selectedImage.value) return selectedImage.value.id !== props.modelValue
  else return true
})
const { isLoading: isSaving, fn: saveAndSelectImage } = useAsyncFunction(
  async (image: Image | NewImage) => {
    error.value = undefined
    const data = new FormData()
    data.set('ppoi', image.ppoi)
    data.set('alt_text', image.alt_text ?? '')
    data.set('credits', image.credits ?? '')
    let newOrUpdatedImage: Image
    try {
      if ('id' in image) {
        newOrUpdatedImage = await imageStore.update(image.id, data)
      } else {
        data.set('image', image.file, image.file.name)
        newOrUpdatedImage = await imageStore.create(data)
      }
    } catch (_error) {
      const _errorObj = _error instanceof Error ? _error : new Error(String(_error))
      const message =
        _errorObj instanceof APIResponseError && _errorObj.response.status === 413
          ? t('imagePicker.error.tooLarge')
          : t('imagePicker.error.default')
      // eslint-disable-next-line no-undef
      error.value = new AggregateError([_errorObj], message)
      return
    }
    emit('update:modelValue', newOrUpdatedImage.id)
    emit('show', false)
  },
)

function setImage(image: Image | NewImage | null) {
  selectedImage.value = image
  showBrowser.value = false
}

function deselectImage() {
  selectedImage.value = null
  error.value = undefined
}
</script>

<script lang="ts">
export default {
  compatConfig: {
    MODE: 3,
  },
}
</script>