Skip to content

Commit 11d0abc

Browse files
committed
feat(vault): image thumbnails in search grid
1 parent 3eab784 commit 11d0abc

1 file changed

Lines changed: 28 additions & 5 deletions

File tree

‎frontend/app/components/vault/VaultCard.vue‎

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,37 @@ const { t } = useI18n();
1111
1212
// Optional trick-play fraction (0..1 along the asset) while hovering a video.
1313
const frac = ref<number | null>(null);
14-
const imgFailed = ref(false);
14+
15+
// Cantemo doesn't auto-generate `/thumbnailresource` for static images, so the
16+
// thumbnail endpoint 404s for them. Fall back to the preview shape (the actual
17+
// image bytes) for image items only — for video/audio the preview is media,
18+
// not an image, so loading it into <img> would just waste bandwidth.
19+
type ImgStage = "thumbnail" | "preview" | "failed";
20+
const imgStage = ref<ImgStage>("thumbnail");
1521
1622
const thumbSrc = computed(() => {
1723
let url = `${props.base}/vault/thumbnail?vxid=${encodeURIComponent(props.item.VXID)}`;
1824
if (frac.value != null) url += `&f=${frac.value}`;
1925
return url;
2026
});
2127
28+
const previewSrc = computed(
29+
() =>
30+
`${props.base}/vault/preview?vxid=${encodeURIComponent(props.item.VXID)}`,
31+
);
32+
33+
const imgSrc = computed(() =>
34+
imgStage.value === "thumbnail" ? thumbSrc.value : previewSrc.value,
35+
);
36+
37+
function onImgError() {
38+
if (imgStage.value === "thumbnail" && props.item.mediaType === "image") {
39+
imgStage.value = "preview";
40+
} else {
41+
imgStage.value = "failed";
42+
}
43+
}
44+
2245
const typeIcon = computed(() => {
2346
switch (props.item.mediaType) {
2447
case "video":
@@ -44,7 +67,7 @@ const durationLabel = computed(() => {
4467
// Quantize to a handful of steps to limit thumbnail requests while hovering.
4568
function onMove(e: MouseEvent) {
4669
if (props.item.mediaType !== "video") return;
47-
if (imgFailed.value) return;
70+
if (imgStage.value === "failed") return;
4871
const el = e.currentTarget as HTMLElement;
4972
const rect = el.getBoundingClientRect();
5073
const f = Math.min(Math.max((e.clientX - rect.left) / rect.width, 0), 1);
@@ -69,12 +92,12 @@ function onLeave() {
6992
@mouseleave="onLeave"
7093
>
7194
<img
72-
v-if="!imgFailed"
73-
:src="thumbSrc"
95+
v-if="imgStage !== 'failed'"
96+
:src="imgSrc"
7497
:alt="item.title"
7598
loading="lazy"
7699
class="h-full w-full object-contain"
77-
@error="imgFailed = true"
100+
@error="onImgError"
78101
/>
79102
<UIcon v-else :name="typeIcon" class="size-10 opacity-40" />
80103
<span

0 commit comments

Comments
 (0)