kodadot/nft-gallery

View on GitHub
components/base/MediaItem.vue

Summary

Maintainability
Test Coverage
<template>
  <div
    ref="mediaItem"
    class="media-object h-fit"
    :class="{ relative: hasNormalTag }"
  >
    <component
      :is="resolveComponent"
      ref="mediaRef"
      :src="properSrc"
      :sizes="sizes"
      :mime-type="mimeType"
      :animation-src="animationSrc"
      :alt="title"
      :placeholder="placeholder"
      :original="original"
      :is-lewd="isLewd"
      :is-detail="isDetail"
      :is-fullscreen="isFullscreen"
      :disable-operation="disableOperation"
      :player-cover="audioPlayerCover"
      :hover-on-cover-play="audioHoverOnCoverPlay"
      :parent-hovering="isMediaItemHovering"
      :image-component="imageComponent"
      :preview="preview"
      :autoplay="autoplay"
      :lazy-loading="lazyLoading"
      :inner-class="innerClass"
    />
    <div
      v-if="isLewd && isLewdBlurredLayer"
      class="nsfw-blur flex capitalize items-center justify-center flex-col"
    >
      <NeoIcon
        icon="eye-slash"
        class="mb-3"
      />
      <span class="font-bold">
        {{ $t('lewd.explicit') }}
      </span>
      <span class="nsfw-desc text-center">{{ $t('lewd.explicitDesc') }}</span>
    </div>
    <div
      v-if="hasNormalTag"
      class="bg-k-shade border-k-grey text-text-color flex items-center justify-center border rounded-md absolute right-3 top-3 image size-6 z-[18]"
    >
      <NeoIcon
        icon="image"
        pack="far"
        class="text-sm font-medium"
      />
    </div>
    <NeoButton
      v-if="isLewd"
      rounded
      no-shadow
      class="nsfw-action border-0 px-4 py-1 text-base"
      :class="{ hide: isLewdBlurredLayer }"
      :label="
        isLewdBlurredLayer ? $t('lewd.showContent') : $t('lewd.hideContent')
      "
      @click="toggleContent"
    />
  </div>
</template>

<script lang="ts" setup>
import type { ComputedOptions, ConcreteComponent, MethodOptions } from 'vue'
import { useElementHover, useElementVisibility } from '@vueuse/core'
import {
  NeoButton,
  NeoIFrameMedia,
  NeoIcon,
  NeoImageMedia,
  NeoJsonMedia,
  NeoObjectMedia,
  NeoUnknownMedia,
  NeoVideoMedia,
} from '@kodadot1/brick'
import AudioMedia from '@/components/shared/AudioMedia.vue'
import { getMimeType, resolveMedia } from '@/utils/gallery/media'
import { MediaType } from '@/components/rmrk/types'

const props = withDefaults(
  defineProps<{
    src?: string
    animationSrc?: string
    mimeType?: string
    title?: string
    original?: boolean
    isLewd?: boolean
    isDetail?: boolean
    placeholder?: string
    disableOperation?: boolean
    audioPlayerCover?: string
    audioHoverOnCoverPlay?: boolean
    isFullscreen?: boolean
    // props for video component
    preview?: boolean
    autoplay?: boolean
    // props for image component
    lazyLoading?: boolean
    enableNormalTag?: boolean
    sizes?: string
    innerClass?: string
    imageComponent?:
      | string
      // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
      | ConcreteComponent<{}, any, any, ComputedOptions, MethodOptions>
  }>(),
  {
    src: '',
    animationSrc: '',
    mimeType: '',
    title: 'KodaDot NFT',
    original: false,
    isLewd: false,
    isDetail: false,
    placeholder: undefined,
    disableOperation: undefined,
    audioPlayerCover: '',
    isFullscreen: false,
    imageComponent: 'img',
    lazyLoading: false,
    enableNormalTag: false,
  },
)

const mediaRef = ref()
const mediaItem = ref<HTMLDivElement>()
// props.mimeType may be empty string "". Add `image/png` as fallback
const mimeType = computed(() => props.mimeType || type.value || 'image/png')

useMediaFullscreen({
  ref: mediaItem,
  isFullscreen: computed(() => props.isFullscreen),
})

const targetIsVisible = useElementVisibility(mediaItem)
const { placeholder: themedPlaceholder } = useTheme()

const modelComponent = ref<Component>()
const isModelComponentLoaded = ref(false)
const shouldLoadModelComponent = computed(() => {
  return targetIsVisible.value && mimeType.value === 'model/gltf-binary'
})
watch(shouldLoadModelComponent, (shouldLoad) => {
  if (shouldLoad && !isModelComponentLoaded.value) {
    modelComponent.value = defineAsyncComponent(
      async () => (await import('@kodadot1/brick')).NeoModelMedia,
    )
    isModelComponentLoaded.value = true
  }
})

const PREFIX = 'Neo'
const SUFFIX = 'Media'
const type = ref('')

const hasNormalTag = computed<boolean>(() => {
  return (
    props.enableNormalTag
    && Boolean(props.mimeType || type.value || !props.animationSrc) // avoid showing normal tag before type has updated
    && resolveMedia(mimeType.value) !== MediaType.IFRAME
    && !props.isDetail
    && !IMG_PLACEHOLDERS.includes(props.src)
  )
})
const isLewdBlurredLayer = ref(props.isLewd)
const components = {
  NeoImageMedia,
  NeoVideoMedia,
  NeoAudioMedia: AudioMedia,
  NeoJsonMedia,
  NeoIFrameMedia,
  NeoObjectMedia,
  NeoUnknownMedia,
}

const resolveComponent = computed(() => {
  let mediaType = resolveMedia(mimeType.value)

  if (mediaType === MediaType.IFRAME && !props.isDetail) {
    mediaType = MediaType.IMAGE
  }

  return mediaType === 'Model'
    ? modelComponent.value
    : components[PREFIX + mediaType + SUFFIX]
})
const placeholder = computed(() =>
  !props.placeholder ? themedPlaceholder.value : props.placeholder,
)
const properSrc = computed(() => props.src || placeholder.value)

const updateComponent = async () => {
  if (props.animationSrc && !props.mimeType) {
    type.value = await getMimeType(props.animationSrc)
  }
}

watch(
  () => props.animationSrc,
  () => updateComponent(),
  {
    immediate: true,
  },
)

const toggleContent = () => {
  isLewdBlurredLayer.value = !isLewdBlurredLayer.value
}

const isMediaItemHovering = useElementHover(mediaItem)

function toggleFullscreen() {
  if (mediaRef.value.toggleFullscreen) {
    mediaRef.value.toggleFullscreen()
  }
}

defineExpose({ isLewdBlurredLayer, toggleFullscreen })
</script>

<style lang="scss" scoped>
@import '@/assets/styles/abstracts/variables';
.media-object {
  .nsfw-blur {
    backdrop-filter: blur(60px);
    position: absolute;
    top: 0;
    height: 100%;
    width: 100%;
    background-color: rgb(0 0 0 / 50%);
    color: #fff;

    .nsfw-desc {
      max-width: 18.75rem;
    }
  }
  .nsfw-action {
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
    bottom: 1.25rem;
    @include ktheme() {
      color: theme('text-color') !important;
      background: theme('background-color') !important;
    }
    &.hide {
      @include ktheme() {
        color: theme('background-color') !important;
        background: theme('text-color') !important;
      }
    }
  }
}
</style>