Show document icon when no preview is available or has failed. While loading, pulse the icon.
This commit is contained in:
@@ -1,9 +1,35 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if=showProgress() class="preview-progress" aria-label="Preview pending">
|
<div v-if="showPreviewImage || showNativeImage" class="preview-image-shell">
|
||||||
<SpinnerIcon />
|
<span
|
||||||
|
v-show="activeImageLoading"
|
||||||
|
class="file icon"
|
||||||
|
:class="[`ext-${doc.ext}`, 'loading-pulse']"
|
||||||
|
:style="loadingPulseStyle"
|
||||||
|
></span>
|
||||||
|
<img
|
||||||
|
v-if="showPreviewImage"
|
||||||
|
:src="previewSrc"
|
||||||
|
alt=""
|
||||||
|
:class="{ ready: !previewImageLoading }"
|
||||||
|
@load="onPreviewImageLoad"
|
||||||
|
@error="onPreviewImageError"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-else
|
||||||
|
:src="doc.url"
|
||||||
|
alt=""
|
||||||
|
:class="{ ready: !nativeImageLoading }"
|
||||||
|
@load="onNativeImageLoad"
|
||||||
|
@error="onNativeImageError"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div v-else-if=showProgress() class="preview-progress" aria-label="Preview pending">
|
||||||
|
<span
|
||||||
|
class="file icon"
|
||||||
|
:class="[`ext-${doc.ext}`, { 'loading-pulse': !previewLoadFailed }]"
|
||||||
|
:style="loadingPulseStyle"
|
||||||
|
></span>
|
||||||
</div>
|
</div>
|
||||||
<img v-else-if="previewSrc && !video() && !audio()" :src="previewSrc" alt="">
|
|
||||||
<img v-else-if=doc.img :src=doc.url alt="">
|
|
||||||
<span v-else-if=doc.dir class="folder icon"></span>
|
<span v-else-if=doc.dir class="folder icon"></span>
|
||||||
<div v-else-if=video() class="video-container" :class="{ pending: !doc.complete }">
|
<div v-else-if=video() class="video-container" :class="{ pending: !doc.complete }">
|
||||||
<video v-if=doc.complete ref=vid :src=doc.url :poster=previewSrc preload=none @play=onplay @pause=onpaused @ended=next @seeking=media!.play()></video>
|
<video v-if=doc.complete ref=vid :src=doc.url :poster=previewSrc preload=none @play=onplay @pause=onpaused @ended=next @seeking=media!.play()></video>
|
||||||
@@ -18,10 +44,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Play as PlayIcon, Spinner as SpinnerIcon } from '@/assets/svg'
|
import { Play as PlayIcon } from '@/assets/svg'
|
||||||
import type { Doc } from '@/repositories/Document'
|
import type { Doc } from '@/repositories/Document'
|
||||||
import { useMainStore } from '@/stores/main'
|
import { useMainStore } from '@/stores/main'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
|
|
||||||
const aud = ref<HTMLAudioElement | null>(null)
|
const aud = ref<HTMLAudioElement | null>(null)
|
||||||
const vid = ref<HTMLVideoElement | null>(null)
|
const vid = ref<HTMLVideoElement | null>(null)
|
||||||
@@ -30,11 +56,58 @@ const props = defineProps<{
|
|||||||
doc: Doc
|
doc: Doc
|
||||||
quality: string
|
quality: string
|
||||||
}>()
|
}>()
|
||||||
|
const previewImageFailed = ref(false)
|
||||||
|
const nativeImageFailed = ref(false)
|
||||||
|
const previewImageLoading = ref(true)
|
||||||
|
const nativeImageLoading = ref(true)
|
||||||
const previewSrc = computed(() =>
|
const previewSrc = computed(() =>
|
||||||
props.doc.previewurl
|
props.doc.previewurl
|
||||||
? `${props.doc.previewurl}?${props.quality}&t=${props.doc.mtime}`
|
? `${props.doc.previewurl}?${props.quality}&t=${props.doc.mtime}`
|
||||||
: ''
|
: ''
|
||||||
)
|
)
|
||||||
|
const showPreviewImage = computed(
|
||||||
|
() => !!previewSrc.value && !video() && !audio() && !previewImageFailed.value
|
||||||
|
)
|
||||||
|
const showNativeImage = computed(() => props.doc.img && !nativeImageFailed.value)
|
||||||
|
const activeImageLoading = computed(() =>
|
||||||
|
showPreviewImage.value ? previewImageLoading.value : nativeImageLoading.value
|
||||||
|
)
|
||||||
|
const previewLoadFailed = computed(
|
||||||
|
() => previewImageFailed.value || nativeImageFailed.value
|
||||||
|
)
|
||||||
|
const loadingPulseDelayMs = computed(() => {
|
||||||
|
let hash = 0
|
||||||
|
for (const ch of props.doc.key) hash = (hash * 31 + ch.charCodeAt(0)) >>> 0
|
||||||
|
return hash % 1800
|
||||||
|
})
|
||||||
|
const loadingPulseStyle = computed(() => ({
|
||||||
|
animationDelay: `${-loadingPulseDelayMs.value}ms`
|
||||||
|
}))
|
||||||
|
|
||||||
|
const onPreviewImageLoad = () => {
|
||||||
|
previewImageLoading.value = false
|
||||||
|
}
|
||||||
|
const onPreviewImageError = () => {
|
||||||
|
previewImageLoading.value = false
|
||||||
|
previewImageFailed.value = true
|
||||||
|
}
|
||||||
|
const onNativeImageLoad = () => {
|
||||||
|
nativeImageLoading.value = false
|
||||||
|
}
|
||||||
|
const onNativeImageError = () => {
|
||||||
|
nativeImageLoading.value = false
|
||||||
|
nativeImageFailed.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.doc.key,
|
||||||
|
() => {
|
||||||
|
previewImageFailed.value = false
|
||||||
|
nativeImageFailed.value = false
|
||||||
|
previewImageLoading.value = true
|
||||||
|
nativeImageLoading.value = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const onplay = () => {
|
const onplay = () => {
|
||||||
if (!media.value) return
|
if (!media.value) return
|
||||||
@@ -154,6 +227,7 @@ img, embed, .icon, audio, video {
|
|||||||
border-radius: calc(.5em / 8);
|
border-radius: calc(.5em / 8);
|
||||||
}
|
}
|
||||||
.preview-progress {
|
.preview-progress {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -162,18 +236,47 @@ img, embed, .icon, audio, video {
|
|||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
}
|
}
|
||||||
.preview-progress :deep(svg) {
|
.preview-progress .icon {
|
||||||
width: 4.5em;
|
opacity: 0.9;
|
||||||
height: 4.5em;
|
|
||||||
opacity: 0.8;
|
|
||||||
animation: media-preview-spin 0.9s linear infinite;
|
|
||||||
}
|
}
|
||||||
@keyframes media-preview-spin {
|
.preview-image-shell {
|
||||||
from {
|
position: relative;
|
||||||
transform: rotate(0deg);
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
}
|
}
|
||||||
to {
|
.preview-image-shell img {
|
||||||
transform: rotate(360deg);
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
object-fit: contain;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
.preview-image-shell img.ready {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.loading-pulse {
|
||||||
|
animation: media-preview-pulse 1.8s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes media-preview-pulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 0.86;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.04);
|
||||||
|
opacity: 0.98;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 0.86;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.folder::before {
|
.folder::before {
|
||||||
@@ -219,12 +322,6 @@ img, embed, .icon, audio, video {
|
|||||||
figure.cursor .icon {
|
figure.cursor .icon {
|
||||||
filter: brightness(1);
|
filter: brightness(1);
|
||||||
}
|
}
|
||||||
img::before {
|
|
||||||
/* broken image */
|
|
||||||
text-shadow: 0 0 .5rem #000;
|
|
||||||
filter: grayscale(1);
|
|
||||||
content: '❌';
|
|
||||||
}
|
|
||||||
.video-container {
|
.video-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Reference in New Issue
Block a user