This commit is contained in:
team3
2026-09-30 14:31:50 +02:00
parent 624df7e7ee
commit fd7893b5d2
7 changed files with 302 additions and 247 deletions

View File

@@ -434,12 +434,7 @@ onMounted(load)
<StreamTab v-else-if="activeTab === 'stream'" :angemeldet="angemeldet" />
<ClipsTab
v-else-if="activeTab === 'clips'"
:angemeldet="angemeldet"
:host="host"
:abende="gesehen"
/>
<ClipsTab v-else-if="activeTab === 'clips'" :angemeldet="angemeldet" :host="host" />
<div v-else class="layout">
<div class="results">

View File

@@ -10,64 +10,37 @@ import PersonName from './PersonName.vue'
const props = defineProps({
angemeldet: Boolean,
host: Boolean,
// Die gesehenen Abende — für Clips aus alten Aufnahmen
abende: { type: Array, default: () => [] },
})
// "Clip erstellen": erst den Abend wählen, dann öffnet der Editor.
// Zur Wahl stehen nur Abende, zu denen es eine Aufnahme gibt. Wurde der
// Stream an dem Abend neu gestartet, fragt ein zweiter Schritt nach dem
// Stück.
// "Clip erstellen": eine Aufnahme wählen, dann öffnet der Editor. Jede
// Aufnahme bringt ihren Film mit — oder keinen, dann läuft der Clip
// unter „Sonstige".
const wahl = ref(false)
const wahlFehler = ref('')
const tage = ref(null)
const gewaehlt = ref(null)
const stuecke = ref(null)
const aufnahmen = ref(null)
async function wahlOeffnen() {
wahl.value = true
wahlFehler.value = ''
tage.value = null
gewaehlt.value = null
stuecke.value = null
aufnahmen.value = null
try {
const resp = await fetch('/api/clips/aufnahmen')
tage.value = resp.ok ? (await resp.json()).tage : []
const resp = await fetch('/api/aufnahmen')
aufnahmen.value = resp.ok ? (await resp.json()).stuecke : []
} catch {
tage.value = []
aufnahmen.value = []
}
}
const abendeMitAufnahme = computed(() =>
tage.value ? props.abende.filter((a) => tage.value.includes(a.watched_at.slice(0, 10))) : [],
)
async function abendWaehlen(abend) {
wahlFehler.value = ''
gewaehlt.value = abend
stuecke.value = null
let liste = []
try {
const resp = await fetch(`/api/clips/von-film/${abend.movie_id}`)
liste = resp.ok ? (await resp.json()).stuecke : []
} catch {
liste = []
}
// Ein Stück: direkt los. Sonst wählt man, welches.
if (liste.length <= 1) return erstellen(abend, liste[0]?.start)
stuecke.value = liste
}
function minuten(sekunden) {
return `${Math.max(1, Math.round(sekunden / 60))} Min`
}
async function erstellen(abend, start = null) {
async function erstellen(aufnahme) {
wahlFehler.value = ''
const resp = await fetch('/api/clips/von-film', {
const resp = await fetch('/api/clips/von-aufnahme', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ movie_id: abend.movie_id, start }),
body: JSON.stringify({ start: aufnahme.start }),
})
if (!resp.ok) {
const daten = await resp.json().catch(() => ({}))
@@ -104,7 +77,7 @@ const gruppen = computed(() => {
const nach = new Map()
for (const c of clips.value) {
const key = c.movie_id ?? 0
if (!nach.has(key)) nach.set(key, { key, titel: c.title || 'Ohne Film', clips: [] })
if (!nach.has(key)) nach.set(key, { key, titel: c.title || 'Sonstige', clips: [] })
nach.get(key).clips.push(c)
}
return [...nach.values()]
@@ -140,46 +113,31 @@ onUnmounted(() => abbestellen?.())
<button type="button" class="erstellen" @click="wahlOeffnen">Clip erstellen</button>
</div>
<!-- Abend wählen: nur, wo es eine Aufnahme gibt, klappt es -->
<!-- Aufnahme wählen: mit Film oder ohne -->
<Teleport to="body">
<div v-if="wahl" class="abendwahl-overlay" @click.self="wahl = false">
<div class="abendwahl">
<header>
<span>{{ stuecke ? 'Welche Aufnahme?' : 'Zu welchem Abend?' }}</span>
<span>Welche Aufnahme?</span>
<button type="button" class="zu" @click="wahl = false">×</button>
</header>
<p v-if="wahlFehler" class="abendwahl-fehler">{{ wahlFehler }}</p>
<!-- Zweiter Schritt: der Stream lief an dem Abend mehrmals -->
<template v-if="stuecke">
<p class="abendwahl-hinweis">
{{ gewaehlt.title }} · {{ datum(gewaehlt.watched_at) }} · der Stream lief
{{ stuecke.length }}-mal
</p>
<ul>
<li v-for="s in stuecke" :key="s.start">
<button type="button" @click="erstellen(gewaehlt, s.start)">
<span class="abend-text">
<span class="abend-titel">ab {{ uhrzeit(s.start) }} Uhr</span>
<span class="abend-datum">{{ minuten(s.dauer) }}</span>
</span>
</button>
</li>
</ul>
<button type="button" class="zurueck" @click="stuecke = null">← Anderer Abend</button>
</template>
<p v-else-if="tage === null" class="abendwahl-hinweis">Suche Aufnahmen …</p>
<p v-else-if="!abendeMitAufnahme.length" class="abendwahl-hinweis">
Zu keinem Abend gibt es eine Aufnahme.
</p>
<p v-if="aufnahmen === null" class="abendwahl-hinweis">Suche Aufnahmen …</p>
<p v-else-if="!aufnahmen.length" class="abendwahl-hinweis">Es gibt keine Aufnahmen.</p>
<ul v-else>
<li v-for="a in abendeMitAufnahme" :key="a.id">
<button type="button" @click="abendWaehlen(a)">
<img v-if="a.poster_path" :src="posterUrl(a.poster_path, 'w154')" alt="" />
<li v-for="s in aufnahmen" :key="s.start">
<button type="button" @click="erstellen(s)">
<img
v-if="s.film?.poster_path"
:src="posterUrl(s.film.poster_path, 'w154')"
alt=""
/>
<span v-else class="ohne-cover"></span>
<span class="abend-text">
<span class="abend-titel">{{ a.title }}</span>
<span class="abend-datum">{{ datum(a.watched_at) }}</span>
<span class="abend-titel">{{ s.film?.title || 'Sonstige Aufnahme' }}</span>
<span class="abend-datum">
{{ datum(s.start) }} · {{ uhrzeit(s.start) }} Uhr · {{ minuten(s.dauer) }}
</span>
</span>
</button>
</li>
@@ -290,6 +248,8 @@ h3 {
position: relative;
display: block;
aspect-ratio: 16 / 9;
/* Sonst zieht ein hochkantiges Poster die Karte in die Länge */
overflow: hidden;
background: #000;
}
@@ -404,18 +364,6 @@ h3 {
line-height: 1;
}
.abendwahl .zurueck {
align-self: flex-start;
padding: 0.3rem 0.5rem;
color: var(--muted);
font-size: 0.85rem;
border-radius: 6px;
}
.abendwahl .zurueck:hover {
color: var(--text);
}
.abendwahl-fehler {
color: var(--accent);
font-size: 0.85rem;
@@ -453,6 +401,14 @@ h3 {
border-radius: 4px;
}
/* Ohne Film: ein leeres Cover an Stelle des Posters */
.abendwahl li .ohne-cover {
flex: 0 0 40px;
height: 60px;
background: var(--line);
border-radius: 4px;
}
.abend-text {
display: flex;
flex-direction: column;

View File

@@ -106,6 +106,21 @@ async function aufnahmeLoeschen(s) {
await aufnahmenLaden()
}
// Welcher Film in einer Aufnahme lief, sagt der Host — per Filmsuche
const filmDialogFuer = ref(null)
async function filmZuweisen(start, movieId) {
aufnahmenFehler.value = ''
const resp = await fetch('/api/aufnahmen/film', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ start, movie_id: movieId }),
})
if (!resp.ok) aufnahmenFehler.value = 'Zuweisen hat nicht geklappt.'
filmDialogFuer.value = null
await aufnahmenLaden()
}
function aufnahmeText(s) {
const d = new Date(s.start)
const datum = d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })
@@ -245,19 +260,52 @@ async function entfernen(user) {
<p v-else-if="!aufnahmen.length" class="leer">Keine Aufnahmen.</p>
<ul v-else>
<li v-for="s in aufnahmen" :key="s.start" class="zeile">
<span>{{ aufnahmeText(s) }}</span>
<button
type="button"
class="klein"
:class="{ warnung: loeschFrage === s.start }"
@click="aufnahmeLoeschen(s)"
>
{{ loeschFrage === s.start ? 'Wirklich löschen?' : 'Löschen' }}
</button>
<span>
{{ aufnahmeText(s) }} ·
<strong v-if="s.film">{{ s.film.title }}</strong>
<em v-else>kein Film</em>
</span>
<span class="knoepfe">
<button type="button" class="klein" @click="filmDialogFuer = s.start">Film</button>
<button
v-if="s.zugewiesen"
type="button"
class="klein"
@click="filmZuweisen(s.start, null)"
>
Aufheben
</button>
<button
type="button"
class="klein"
:class="{ warnung: loeschFrage === s.start }"
@click="aufnahmeLoeschen(s)"
>
{{ loeschFrage === s.start ? 'Wirklich löschen?' : 'Löschen' }}
</button>
</span>
</li>
</ul>
</section>
<Teleport to="body">
<div v-if="filmDialogFuer" class="hintergrund" @click.self="filmDialogFuer = null">
<div class="fenster" role="dialog" aria-modal="true">
<p class="label">Aufnahme</p>
<h2>Welcher Film lief?</h2>
<FilmSuche
platzhalter="Film suchen …"
@waehlen="filmZuweisen(filmDialogFuer, $event.id)"
/>
<div class="fuss">
<button type="button" class="abbrechen" @click="filmDialogFuer = null">
Abbrechen
</button>
</div>
</div>
</div>
</Teleport>
<Teleport to="body">
<div v-if="hostFilmDialog" class="hintergrund" @click.self="hostFilmDialog = false">
<div class="fenster" role="dialog" aria-modal="true">
@@ -552,6 +600,17 @@ h3 {
font-size: 0.9rem;
}
.aufnahmen .knoepfe {
display: flex;
flex-shrink: 0;
gap: 0.4rem;
}
.aufnahmen em {
color: var(--muted);
font-style: normal;
}
.klein.warnung {
color: #fff;
background: var(--accent);