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:
16
Makefile
16
Makefile
@@ -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
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
Reference in New Issue
Block a user