This commit is contained in:
team3
2026-10-08 16:03:27 +02:00
parent 3f12b0742d
commit 93fe2219b9
6 changed files with 94 additions and 25 deletions

View File

@@ -144,7 +144,7 @@ const tabs = computed(() => [
{ id: 'merkliste', label: 'Sammlungen', count: null },
{ id: 'wheel', label: 'Rad', count: null },
{ id: 'stream', label: 'Stream', count: null },
...(angemeldet.value || host.value ? [{ id: 'clips', label: 'Clips', count: null }] : []),
{ id: 'clips', label: 'Clips', count: null },
{ id: 'watched', label: 'Gesehen', count: gesehenZahl.value },
{
id: 'wuensche',

View File

@@ -0,0 +1,58 @@
<script setup>
// Das Vorschaubild eines geschnittenen Clips: der erste Frame des Videos.
// Ein Video-Element im Hintergrund lädt den Anfang, der Frame wandert
// auf ein Canvas und von dort als Bild in die Karte. So gibt es keine
// Player-Steuerung, keine Bild-in-Bild-Hinweise und kein schwarzes Feld.
import { onMounted, onUnmounted, ref } from 'vue'
const props = defineProps({
src: { type: String, required: true },
})
const bild = ref(null)
let video = null
onMounted(() => {
video = document.createElement('video')
video.muted = true
video.playsInline = true
video.preload = 'auto'
video.addEventListener('loadeddata', () => {
const canvas = document.createElement('canvas')
canvas.width = video.videoWidth
canvas.height = video.videoHeight
canvas.getContext('2d').drawImage(video, 0, 0)
try {
bild.value = canvas.toDataURL('image/jpeg', 0.82)
} catch {
bild.value = null
}
// Das Video wird nicht mehr gebraucht
video.removeAttribute('src')
video.load()
})
video.src = props.src
})
onUnmounted(() => {
if (video) {
video.removeAttribute('src')
video.load()
}
})
</script>
<template>
<img v-if="bild" :src="bild" alt="" />
<span v-else class="leer"></span>
</template>
<style scoped>
img,
.leer {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
</style>

View File

@@ -6,6 +6,9 @@ import ClipTrimmer from './ClipTrimmer.vue'
const props = defineProps({
clip: { type: Object, required: true },
// Besitzer und Host dürfen schneiden, ändern und löschen — alle
// anderen schauen nur
darf: Boolean,
})
const emit = defineEmits(['zu', 'geaendert'])
@@ -72,6 +75,7 @@ onUnmounted(() => window.removeEventListener('keydown', escape))
<div class="editor">
<header>
<input
v-if="darf"
v-model="titel"
type="text"
maxlength="80"
@@ -79,12 +83,20 @@ onUnmounted(() => window.removeEventListener('keydown', escape))
class="titel"
@change="patch({ titel })"
/>
<h2 v-else class="titel">{{ clip.titel || 'Ohne Titel' }}</h2>
<button type="button" class="zu" title="Schließen" @click="emit('zu')">×</button>
</header>
<video :src="vorschauUrl" controls playsinline class="vorschau"></video>
<!-- Wer nur schaut, bekommt bei geschnittenen Clips das fertige Video -->
<video
:src="!darf && clip.geschnitten ? `/api/clips/${clip.id}/video` : vorschauUrl"
controls
playsinline
class="vorschau"
></video>
<ClipTrimmer
v-if="darf"
v-model:vorher="vorher"
v-model:dauer="dauer"
:segmente="segmente"
@@ -95,14 +107,14 @@ onUnmounted(() => window.removeEventListener('keydown', escape))
<p v-if="fehler" class="fehler">{{ fehler }}</p>
<div class="knoepfe">
<button type="button" class="haupt" :disabled="schneidet" @click="schneiden">
<button v-if="darf" type="button" class="haupt" :disabled="schneidet" @click="schneiden">
{{ schneidet ? 'Schneidet …' : clip.geschnitten ? 'Speichern' : 'Schneiden' }}
</button>
<a v-if="clip.geschnitten" :href="`/api/clips/${clip.id}/video`" download class="link"
>Herunterladen</a
>
<span class="rest"></span>
<button type="button" class="weg" @click="loeschen">Löschen</button>
<button v-if="darf" type="button" class="weg" @click="loeschen">Löschen</button>
</div>
</div>
</div>

View File

@@ -5,6 +5,7 @@ import { computed, onMounted, onUnmounted, ref } from 'vue'
import { abonnieren } from '../live'
import { backdropUrl, posterUrl } from '../images'
import ClipEditor from './ClipEditor.vue'
import ClipBild from './ClipBild.vue'
import PersonName from './PersonName.vue'
const props = defineProps({
@@ -149,7 +150,7 @@ onUnmounted(() => abbestellen?.())
<p v-if="laden" class="note">Lade Clips…</p>
<p v-else-if="fehler" class="note error">{{ fehler }}</p>
<p v-else-if="!clips.length" class="note">
Noch keine Marken. Im Stream-Reiter setzt die Taste C eine.
Noch keine Clips. Im Stream-Reiter setzt die Taste C eine Marke.
</p>
<section v-for="g in gruppen" :key="g.key">
@@ -159,13 +160,7 @@ onUnmounted(() => abbestellen?.())
<!-- Geschnitten: das Video selbst als Bild. Sonst das Filmbild
mit dem Hinweis, dass hier erst eine Marke steht. -->
<span class="bild">
<video
v-if="c.geschnitten"
:src="`/api/clips/${c.id}/video#t=0.5`"
preload="metadata"
muted
playsinline
></video>
<ClipBild v-if="c.geschnitten" :src="`/api/clips/${c.id}/video`" />
<img v-else-if="c.poster_path" :src="backdropUrl(c.poster_path, 'w342')" alt="" />
<span v-else class="leer"></span>
<span class="dauer">{{ Math.round(c.dauer) }} s</span>
@@ -183,7 +178,13 @@ onUnmounted(() => abbestellen?.())
</div>
</section>
<ClipEditor v-if="offen" :clip="offen" @zu="offen = null" @geaendert="laden_" />
<ClipEditor
v-if="offen"
:clip="offen"
:darf="offen.mine || host"
@zu="offen = null"
@geaendert="laden_"
/>
</div>
</template>