Preload pending thread media, hide tile hover overlays on click, and avoid stretching view transition snapshots

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
2026-10-18 16:51:17 +02:00
co-authored by Devin
parent ce106ffae5
commit 86c022a4cb
4 changed files with 31 additions and 15 deletions
+6 -5
View File
@@ -1288,19 +1288,20 @@
::view-transition-group(upload-media) {
animation-duration: 380ms;
animation-timing-function: cubic-bezier(0.2, 0, 0, 1);
overflow: hidden;
border-radius: var(--radius-md);
}
::view-transition-old(upload-media),
::view-transition-new(upload-media) {
animation-duration: 380ms;
animation-timing-function: cubic-bezier(0.2, 0, 0, 1);
/* Neuere Browser zeichnen die Snapshots mit natürlichem Seitenverhältnis -
dann ragt der alte Snapshot bei Aspect-Wechsel (quadratisches Tile ->
breite Thread-Box) unter der Gruppen-Box heraus und das Bild wirkt, als
würde es erst in die falsche Richtung rutschen. Fill + volle Größe zwingt
den Snapshot wieder exakt in die morphierende Box. */
ohne Clipping ragt der alte quadratische Tile-Snapshot bei breiten
Ziel-Medien unten aus der Gruppe heraus. cover + overflow:hidden auf der
Group hält beide Snapshots unverzerrt exakt in der morphierenden Box. */
width: 100%;
height: 100%;
object-fit: fill;
object-fit: cover;
}
/* Phosphor-Glyphen haben mehr Innenrand im ViewBox als Lucide -
+8 -2
View File
@@ -144,8 +144,14 @@ function openUpload(e: MouseEvent) {
>
<Icon name="ph:bell-fill" class="size-3.5" />
</span>
<div class="absolute inset-0 bg-primary/30 opacity-0 transition-opacity group-hover:opacity-100" />
<div class="absolute inset-0 flex items-end bg-gradient-to-t from-black/70 via-black/20 to-transparent p-2 opacity-0 transition-opacity group-hover:opacity-100">
<div
class="absolute inset-0 bg-primary/30 opacity-0 transition-opacity"
:class="!navigating && 'group-hover:opacity-100'"
/>
<div
class="absolute inset-0 flex items-end bg-gradient-to-t from-black/70 via-black/20 to-transparent p-2 opacity-0 transition-opacity"
:class="!navigating && 'group-hover:opacity-100'"
>
<div class="flex w-full items-end justify-between gap-2 text-xs text-white">
<UserHoverCard
v-if="upload.userId != null"
+2
View File
@@ -4,6 +4,7 @@ import 'plyr/dist/plyr.css'
defineProps<{
src: string
poster?: string | null
autoplay?: boolean
}>()
@@ -47,6 +48,7 @@ function onLoadedMetadata() {
<video
ref="videoEl"
:src="src"
:poster="poster || undefined"
:autoplay="autoplay"
playsinline
controls
+15 -8
View File
@@ -323,18 +323,24 @@ function markThreadRead() {
onMounted(markThreadRead)
watch([notifications, id], markThreadRead)
// Medium laden, bevor die Seite fertig ist - sonst enthält der View-Transition-Snapshot eine leere Box
if (import.meta.client && data.value) {
if (data.value.upload.type === 'image') {
// Medium laden, bevor die Seite fertig ist - sonst enthält der View-Transition-Snapshot eine leere Box.
// Auf dem Client ist data.value hier noch null (useFetch läuft async), deshalb
// auch pendingMedia berücksichtigen - ohne decode() hat <img class="w-auto"> noch 0×0.
const preloadTarget = data.value?.upload ?? pendingMediaForPage.value
if (import.meta.client && preloadTarget) {
const preloadImgUrl = preloadTarget.type === 'image'
? preloadTarget.url
: ('previewImage' in preloadTarget ? preloadTarget.previewImage : preloadTarget.thumbUrl)
if (preloadImgUrl) {
const pre = new Image()
pre.src = data.value.upload.url
pre.src = preloadImgUrl
try {
await Promise.race([pre.decode(), new Promise(r => setTimeout(r, 2000))])
await Promise.race([pre.decode(), new Promise(r => setTimeout(r, 1500))])
}
catch { /* Bild lädt dann normal nach */ }
}
else if (data.value.upload.type === 'video') {
const url = data.value.upload.url
else if (preloadTarget.type === 'video' && preloadTarget.url) {
const url = preloadTarget.url
await new Promise<void>((resolve) => {
const v = document.createElement('video')
v.preload = 'auto'
@@ -342,7 +348,7 @@ if (import.meta.client && data.value) {
v.onloadeddata = () => resolve()
v.onerror = () => resolve()
v.src = url
setTimeout(resolve, 2000)
setTimeout(resolve, 1500)
})
}
}
@@ -1346,6 +1352,7 @@ async function deleteComment(commentId: number) {
<VideoPlayer
v-else-if="media?.type === 'video' && media.url"
:src="media.url"
:poster="media.previewImage"
/>
<div
v-else-if="media?.type === 'audio' && media.url"