Ein Namensfeld für alles, oben rechts

Bisher fragten Vorschlag und Wertung getrennt nach dem Namen — einmal in der
Filmansicht, einmal über der Liste. Jetzt steht er einmal in der
Reiterleiste und gilt für beides. Wer keinen einträgt, bleibt anonym.

Dazu: make stream geht jetzt an den Server und beendet vorher ein laufendes
OBS. Der lokale Weg heißt make stream-local.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013XVnRop5c3or3B8xE2hpgZ
This commit is contained in:
team3
2026-08-31 13:48:40 +02:00
parent 56c3f10026
commit bbd1270fd9
5 changed files with 46 additions and 56 deletions

View File

@@ -1,4 +1,4 @@
.PHONY: install dev stop sync build prod logs down push pull stream stream-stop mtx mtx-stop .PHONY: install dev stop sync build prod logs down push pull stream stream-local stream-stop mtx mtx-stop
install: install:
cd backend && python3 -m venv .venv && .venv/bin/pip install -r requirements.txt cd backend && python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
@@ -48,13 +48,19 @@ down:
docker compose down docker compose down
# ── Senden ─────────────────────────────────────────────────────────────────── # ── Senden ───────────────────────────────────────────────────────────────────
# Ohne Argument an den lokalen MediaMTX, sonst an den Server: # Der Regelfall geht an den Server. Ein anderes Ziel bei Bedarf:
# make stream ZIEL=horror.marha.de # make stream ZIEL=anderer.host
ZIEL ?= 127.0.0.1 ZIEL ?= horror.marha.de
stream: mtx # Räumt vorher auf: ein noch laufendes OBS belegt sonst die Sendestrecke,
# und der Tonweg wird sauber neu aufgebaut.
stream: stream-stop
@scripts/stream.sh $(ZIEL) @scripts/stream.sh $(ZIEL)
# Zum Ausprobieren ohne Server, gegen das lokale MediaMTX
stream-local: mtx
@scripts/stream.sh 127.0.0.1
stream-stop: stream-stop:
-@pkill -f "obs --profile filmabend" 2>/dev/null -@pkill -f "obs --profile filmabend" 2>/dev/null
-@scripts/ton.sh ab -@scripts/ton.sh ab

View File

@@ -129,9 +129,9 @@ OBS ──RTMP──► MediaMTX ──LL-HLS──► FastAPI ──► Browser
**Senden:** **Senden:**
```bash ```bash
make stream # an den lokalen MediaMTX make stream # an den Server, beendet vorher ein laufendes OBS
make stream ZIEL=horror.marha.de # an den Server make stream-local # an den lokalen MediaMTX, zum Ausprobieren
make stream-stop # OBS beenden make stream-stop # OBS beenden und den Tonweg zurückstellen
``` ```
Strg-C im Terminal beendet OBS nicht zuverlässig — deshalb `make stream-stop` Strg-C im Terminal beendet OBS nicht zuverlässig — deshalb `make stream-stop`

View File

@@ -224,6 +224,13 @@ const gesehenZahl = computed(
// Wer den Abend leitet, darf drehen und die Runde leeren. Kein Login — // Wer den Abend leitet, darf drehen und die Runde leeren. Kein Login —
// die Tastenkombination reicht, die Seite steht ohnehin nur Freunden offen. // die Tastenkombination reicht, die Seite steht ohnehin nur Freunden offen.
// Ein Name für alles: Vorschläge und Wertungen. Ohne Namen läuft es
// anonym, das ist ausdrücklich erlaubt.
const NAME_SCHLUESSEL = 'filmabend_name'
const name = ref(localStorage.getItem(NAME_SCHLUESSEL) || '')
watch(name, (wert) => localStorage.setItem(NAME_SCHLUESSEL, wert))
const HOST_SCHLUESSEL = 'filmabend_host' const HOST_SCHLUESSEL = 'filmabend_host'
const host = ref(localStorage.getItem(HOST_SCHLUESSEL) === '1') const host = ref(localStorage.getItem(HOST_SCHLUESSEL) === '1')
@@ -389,6 +396,13 @@ onUnmounted(() => window.removeEventListener('keydown', tastatur))
{{ tab.label }} {{ tab.label }}
<span v-if="tab.count !== null" class="count">{{ tab.count }}</span> <span v-if="tab.count !== null" class="count">{{ tab.count }}</span>
</button> </button>
<input
v-model.trim="name"
type="text"
class="name"
placeholder="Dein Name"
maxlength="40"
/>
<span v-if="host" class="host">Host</span> <span v-if="host" class="host">Host</span>
</nav> </nav>
@@ -405,6 +419,7 @@ onUnmounted(() => window.removeEventListener('keydown', tastatur))
<WatchedTab <WatchedTab
v-else-if="activeTab === 'watched'" v-else-if="activeTab === 'watched'"
:host="host" :host="host"
:name="name"
@open="selected = $event" @open="selected = $event"
@changed="ladeGesehen" @changed="ladeGesehen"
/> />
@@ -466,6 +481,7 @@ onUnmounted(() => window.removeEventListener('keydown', tastatur))
@open="selected = $event" @open="selected = $event"
:mein-vorschlag="meinVorschlag" :mein-vorschlag="meinVorschlag"
:host="host" :host="host"
:name="name"
:gesehen="gesehen.some((e) => e.movie_id === selected.id)" :gesehen="gesehen.some((e) => e.movie_id === selected.id)"
@suggested="nachVorschlag" @suggested="nachVorschlag"
@watched="ladeGesehen" @watched="ladeGesehen"
@@ -512,9 +528,15 @@ onUnmounted(() => window.removeEventListener('keydown', tastatur))
border-bottom-color: var(--accent); border-bottom-color: var(--accent);
} }
.tabs .name {
width: 11rem;
margin: 0 0 0.35rem auto;
padding: 0.35rem 0.6rem;
font-size: 0.82rem;
}
.host { .host {
align-self: center; align-self: center;
margin-left: auto;
padding: 0.15rem 0.5rem; padding: 0.15rem 0.5rem;
color: var(--accent); color: var(--accent);
font-size: 0.7rem; font-size: 0.7rem;

View File

@@ -7,33 +7,35 @@ const props = defineProps({
series: { type: Array, default: () => [] }, series: { type: Array, default: () => [] },
meinVorschlag: { type: Object, default: null }, meinVorschlag: { type: Object, default: null },
host: { type: Boolean, default: false }, host: { type: Boolean, default: false },
name: { type: String, default: '' },
gesehen: { type: Boolean, default: false }, gesehen: { type: Boolean, default: false },
}) })
const emit = defineEmits(['close', 'open', 'suggested', 'watched']) const emit = defineEmits(['close', 'open', 'suggested', 'watched'])
const vorschlagen = ref({ zustand: 'bereit', name: '' }) const vorschlagen = ref({ zustand: 'bereit' })
const schonVorgeschlagen = computed( const schonVorgeschlagen = computed(
() => props.meinVorschlag?.movie_id === props.movie.id () => props.meinVorschlag?.movie_id === props.movie.id
) )
async function abschicken() { async function abschicken() {
vorschlagen.value = { ...vorschlagen.value, zustand: 'laeuft' } vorschlagen.value = { zustand: 'laeuft' }
try { try {
const resp = await fetch('/api/suggestions', { const resp = await fetch('/api/suggestions', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
movie_id: props.movie.id, movie_id: props.movie.id,
name: vorschlagen.value.name || null, // Der Name steht oben rechts, einmal für alles
name: props.name || null,
}), }),
}) })
if (!resp.ok) throw new Error() if (!resp.ok) throw new Error()
vorschlagen.value = { ...vorschlagen.value, zustand: 'fertig' } vorschlagen.value = { zustand: 'fertig' }
emit('suggested') emit('suggested')
} catch { } catch {
vorschlagen.value = { ...vorschlagen.value, zustand: 'fehler' } vorschlagen.value = { zustand: 'fehler' }
} }
} }
@@ -145,12 +147,6 @@ onUnmounted(() => {
Du hast „{{ meinVorschlag.title }}“ vorgeschlagen. Ein neuer Du hast „{{ meinVorschlag.title }}“ vorgeschlagen. Ein neuer
Vorschlag ersetzt ihn. Vorschlag ersetzt ihn.
</span> </span>
<input
v-model="vorschlagen.name"
type="text"
placeholder="Dein Name (optional)"
maxlength="40"
/>
<button <button
type="button" type="button"
:disabled="vorschlagen.zustand === 'laeuft'" :disabled="vorschlagen.zustand === 'laeuft'"
@@ -361,12 +357,6 @@ h2 {
border-top: 1px solid var(--line); border-top: 1px solid var(--line);
} }
.vorschlag input {
width: 11rem;
padding: 0.45rem 0.7rem;
font-size: 0.85rem;
}
.vorschlag button { .vorschlag button {
padding: 0.45rem 1rem; padding: 0.45rem 1rem;
font-size: 0.85rem; font-size: 0.85rem;

View File

@@ -3,7 +3,7 @@ import { computed, onMounted, onUnmounted, ref } from 'vue'
import { posterUrl } from '../images' import { posterUrl } from '../images'
import { abonnieren } from '../live' import { abonnieren } from '../live'
const props = defineProps({ host: Boolean }) const props = defineProps({ host: Boolean, name: { type: String, default: '' } })
const emit = defineEmits(['open', 'changed']) const emit = defineEmits(['open', 'changed'])
const items = ref([]) const items = ref([])
@@ -16,10 +16,6 @@ const sichtbar = computed(() =>
const fehler = ref('') const fehler = ref('')
const laden = ref(true) const laden = ref(true)
// Der Name gilt für alle Bewertungen und überlebt das Neuladen
const NAME_SCHLUESSEL = 'filmabend_name'
const name = ref(localStorage.getItem(NAME_SCHLUESSEL) || '')
const STERNE = [1, 2, 3, 4, 5] const STERNE = [1, 2, 3, 4, 5]
// Beim Schweben füllen sich alle Sterne bis zum Zeiger, wie gewohnt // Beim Schweben füllen sich alle Sterne bis zum Zeiger, wie gewohnt
@@ -45,14 +41,13 @@ async function laden_() {
} }
async function bewerten(item, sterne) { async function bewerten(item, sterne) {
localStorage.setItem(NAME_SCHLUESSEL, name.value)
// Sofort anzeigen, der Live-Kanal liefert gleich die richtigen Zahlen // Sofort anzeigen, der Live-Kanal liefert gleich die richtigen Zahlen
item.mine = sterne item.mine = sterne
try { try {
await fetch(`/api/watched/${item.id}/rating`, { await fetch(`/api/watched/${item.id}/rating`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ stars: sterne, name: name.value || null }), body: JSON.stringify({ stars: sterne, name: props.name || null }),
}) })
} catch { } catch {
fehler.value = 'Bewerten hat nicht geklappt.' fehler.value = 'Bewerten hat nicht geklappt.'
@@ -114,16 +109,6 @@ onUnmounted(() => abbestellen?.())
<template> <template>
<div class="watched-tab"> <div class="watched-tab">
<div class="kopf">
<input
v-model="name"
type="text"
class="name"
placeholder="Dein Name (optional)"
maxlength="40"
/>
</div>
<p v-if="fehler" class="note error">{{ fehler }}</p> <p v-if="fehler" class="note error">{{ fehler }}</p>
<p v-else-if="!laden && !sichtbar.length" class="note"> <p v-else-if="!laden && !sichtbar.length" class="note">
Noch kein Film geschaut. Noch kein Film geschaut.
@@ -223,19 +208,6 @@ onUnmounted(() => abbestellen?.())
</template> </template>
<style scoped> <style scoped>
.kopf {
display: flex;
gap: 0.7rem;
align-items: center;
margin-bottom: 1.5rem;
}
.name {
width: 14rem;
padding: 0.55rem 0.8rem;
font-size: 0.9rem;
}
.grid { .grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));