update
This commit is contained in:
1 parent
bfaf0313c1
commit
cb78225e86
29 files changed
+1992
-1227
No files matched your search
@@ -1,9 +1,12 @@
|
||||
<script setup>
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { posterUrl } from '../images'
|
||||
import { abonnieren } from '../live'
|
||||
import { klang } from '../klang'
|
||||
import { useZiehung } from '../composables/useZiehung'
|
||||
import VorschlagsListe from './VorschlagsListe.vue'
|
||||
|
||||
const props = defineProps({
|
||||
defineProps({
|
||||
host: Boolean,
|
||||
// Beides hält die App schon aktuell — auch nach einem Namenswechsel
|
||||
merkliste: { type: Array, default: () => [] },
|
||||
@@ -16,118 +19,10 @@ const items = ref([])
|
||||
const laden = ref(true)
|
||||
const fehler = ref('')
|
||||
|
||||
// Die Ziehung läuft wie das Öffnen einer Lootbox: ein Streifen aus
|
||||
// Postern rast unter der Marke durch und bremst auf dem Gewinner aus.
|
||||
const KACHEL = 220
|
||||
const LUECKE = 10
|
||||
const SCHRITT = KACHEL + LUECKE
|
||||
// Die Ziehung läuft in Phasen: Anlauf (die Marke pulst), Fahrt (die Bahn
|
||||
// rast los und kriecht am Ende). Jedes zweite Mal bricht danach die
|
||||
// Halterung: kurze Ruhe, dann rollt die Bahn ein paar Poster zurück. Der
|
||||
// Server nennt die Gesamtdauer und die Halte, die Phasenlängen stehen
|
||||
// hier — Ruhe plus Rückfahrt müssen zu BRUCH_DAUER im Backend passen.
|
||||
const ANLAUF = 1.0
|
||||
const BRUCH_RUHE = 1.2
|
||||
const BRUCH_FAHRT = 2.5
|
||||
// Kurze Ruhe zwischen Stillstand und Enthüllung — der Moment vor dem Glow
|
||||
const ENTHUELLUNG = 0.6
|
||||
|
||||
const ergebnis = ref(null)
|
||||
const dreht = ref(false)
|
||||
const gewinner = ref(null)
|
||||
const dauer = ref(0)
|
||||
const x = ref(0)
|
||||
const anlauf = ref(false)
|
||||
const bruch = ref(false)
|
||||
// Fahrt: schnell weg, lange kriechen. Bruch: sanft ausrollen.
|
||||
const KURVEN = {
|
||||
fahrt: 'cubic-bezier(0.15, 0.7, 0.3, 1)',
|
||||
bruch: 'cubic-bezier(0.2, 0.6, 0.3, 1)',
|
||||
}
|
||||
const kurve = ref(KURVEN.fahrt)
|
||||
const bahn = ref(null)
|
||||
let timer = null
|
||||
let enthuellung = null
|
||||
|
||||
// ── Ton ─────────────────────────────────────────────────────────────
|
||||
// Ein Tick je Poster, das unter der Marke durchläuft, und ein Klang zur
|
||||
// Enthüllung. Alles synthetisch, keine Dateien. Der Browser gibt Ton erst
|
||||
// nach einer Eingabe frei — wer nur zuschaut und nie geklickt hat, hört
|
||||
// nichts, sieht aber alles.
|
||||
let audio = null
|
||||
|
||||
function klang() {
|
||||
if (!audio) audio = new AudioContext()
|
||||
if (audio.state === 'suspended') audio.resume()
|
||||
return audio
|
||||
}
|
||||
|
||||
function ton(frequenz, dauer, lautstaerke, art = 'triangle', start = 0, bis = null) {
|
||||
const a = klang()
|
||||
const t = a.currentTime + start
|
||||
const osc = a.createOscillator()
|
||||
const gain = a.createGain()
|
||||
osc.type = art
|
||||
osc.frequency.value = frequenz
|
||||
if (bis) osc.frequency.linearRampToValueAtTime(bis, t + dauer)
|
||||
gain.gain.setValueAtTime(lautstaerke, t)
|
||||
gain.gain.exponentialRampToValueAtTime(0.0005, t + dauer)
|
||||
osc.connect(gain).connect(a.destination)
|
||||
osc.start(t)
|
||||
osc.stop(t + dauer)
|
||||
}
|
||||
|
||||
// Schnell: hell und knapp. Beim Kriechen: tiefer, leiser, länger —
|
||||
// man hört das Bremsen.
|
||||
function tick(tempo) {
|
||||
const a = Math.min(1, tempo / 3000)
|
||||
ton(700 + 800 * a, 0.04 + 0.06 * (1 - a), 0.05 + 0.07 * a)
|
||||
}
|
||||
|
||||
function anlaufTon(sekunden) {
|
||||
ton(150, sekunden, 0.08, 'sine', 0, 520)
|
||||
}
|
||||
|
||||
function knacken() {
|
||||
ton(120, 0.12, 0.25, 'square')
|
||||
ton(80, 0.3, 0.2, 'sawtooth', 0.08)
|
||||
}
|
||||
|
||||
function fanfare() {
|
||||
ton(660, 0.5, 0.12, 'sine')
|
||||
ton(990, 0.7, 0.1, 'sine', 0.12)
|
||||
}
|
||||
|
||||
// Liest die laufende Position aus dem Übergang und tickt bei jedem
|
||||
// Kachelwechsel unter der Marke. Das Tempo kommt aus zwei Frames.
|
||||
let zuletzt = -1
|
||||
let letzteX = 0
|
||||
let letzteZeit = 0
|
||||
|
||||
function ticken(zeit) {
|
||||
if (!dreht.value || !bahn.value) return
|
||||
const m = new DOMMatrixReadOnly(getComputedStyle(bahn.value).transform)
|
||||
const idx = Math.floor((-m.m41 + LUECKE / 2) / SCHRITT)
|
||||
const dt = (zeit - letzteZeit) / 1000
|
||||
const tempo = letzteZeit && dt > 0 ? Math.abs(m.m41 - letzteX) / dt : 0
|
||||
letzteX = m.m41
|
||||
letzteZeit = zeit
|
||||
if (zuletzt === -1) {
|
||||
// Die Startkachel zählt nicht — sonst tickt es vor dem Losfahren
|
||||
zuletzt = idx
|
||||
} else if (idx !== zuletzt) {
|
||||
zuletzt = idx
|
||||
tick(tempo)
|
||||
}
|
||||
requestAnimationFrame(ticken)
|
||||
}
|
||||
|
||||
// Kräftige, gut unterscheidbare Töne — nicht die Akzentfarbe, sonst
|
||||
// verschwimmt die Marke mit dem Rand
|
||||
const FARBEN = [
|
||||
'#b3202a', '#1f6f8b', '#c9772b', '#4c6b2f', '#6d3f8f',
|
||||
'#9c2d5c', '#2b7a6f', '#8a6a1f', '#3a4a8f', '#7a3520',
|
||||
]
|
||||
const {
|
||||
masse, ergebnis, dreht, gewinner, dauer, x, anlauf, bruch, kurve, bahn,
|
||||
kacheln, ruhen, abspielen, laufendenDrehHolen, zurueckgesetzt,
|
||||
} = useZiehung({ items })
|
||||
|
||||
// Ohne Server-Ziehung sähe jede Seite ein anderes Ergebnis
|
||||
let abbestellen = null
|
||||
@@ -136,138 +31,12 @@ function verbinden() {
|
||||
abbestellen = abonnieren((nachricht) => {
|
||||
if (nachricht.type === 'suggestions') laden_()
|
||||
else if (nachricht.type === 'spin') abspielen(nachricht.data)
|
||||
else if (nachricht.type === 'reset') zurueckgesetzt()
|
||||
})
|
||||
}
|
||||
|
||||
// Die Bahn hängt mit ihrem Anfang an der Fenstermitte. So landet die
|
||||
// Zielkachel unter der Marke, egal wie breit das Fenster ist.
|
||||
function stoppX(stopp) {
|
||||
return -(stopp.position * SCHRITT + KACHEL / 2 + stopp.versatz * KACHEL)
|
||||
}
|
||||
|
||||
// Vor dem ersten Dreh stehen die Vorschläge einfach in einer Reihe
|
||||
function ruheX() {
|
||||
return -(items.value.length * SCHRITT - LUECKE) / 2
|
||||
}
|
||||
|
||||
function fahren(sekunden, k, ziel) {
|
||||
kurve.value = k
|
||||
dauer.value = sekunden
|
||||
x.value = ziel
|
||||
}
|
||||
|
||||
function ticksStarten() {
|
||||
zuletzt = -1
|
||||
letzteZeit = 0
|
||||
requestAnimationFrame(ticken)
|
||||
}
|
||||
|
||||
// Jede Phase hat eine Dauer, einen Start (mit der Restzeit, die sie noch
|
||||
// hat) und ein Ende. Wer später dazukommt, überspringt die vorderen.
|
||||
function phasen(erg) {
|
||||
const erst = erg.stopps[0]
|
||||
const ende = erg.stopps.at(-1)
|
||||
const extra = erg.stopps.length > 1 ? BRUCH_RUHE + BRUCH_FAHRT : 0
|
||||
const fahrt = erg.dauer - ANLAUF - extra
|
||||
const liste = [
|
||||
{
|
||||
d: ANLAUF,
|
||||
start(rest) {
|
||||
anlauf.value = true
|
||||
if (rest > 0) anlaufTon(rest)
|
||||
},
|
||||
ende() {
|
||||
anlauf.value = false
|
||||
},
|
||||
},
|
||||
{
|
||||
d: fahrt,
|
||||
start(rest) {
|
||||
fahren(rest, KURVEN.fahrt, stoppX(erst))
|
||||
ticksStarten()
|
||||
},
|
||||
},
|
||||
]
|
||||
if (erg.stopps.length > 1) {
|
||||
liste.push(
|
||||
{ d: BRUCH_RUHE },
|
||||
{
|
||||
d: BRUCH_FAHRT,
|
||||
start(rest) {
|
||||
bruch.value = true
|
||||
if (rest > 0) knacken()
|
||||
fahren(rest, KURVEN.bruch, stoppX(ende))
|
||||
},
|
||||
ende() {
|
||||
bruch.value = false
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
return liste
|
||||
}
|
||||
|
||||
function ablauf(erg, liste, i, uebersprungen) {
|
||||
if (i >= liste.length) {
|
||||
dreht.value = false
|
||||
// Erst Stille, dann der Glow
|
||||
enthuellung = window.setTimeout(() => {
|
||||
gewinner.value = erg.gewinner
|
||||
fanfare()
|
||||
}, ENTHUELLUNG * 1000)
|
||||
return
|
||||
}
|
||||
const ph = liste[i]
|
||||
if (uebersprungen >= ph.d) {
|
||||
// Schon vorbei: nur den Endzustand setzen
|
||||
ph.start?.(0)
|
||||
ph.ende?.()
|
||||
ablauf(erg, liste, i + 1, uebersprungen - ph.d)
|
||||
return
|
||||
}
|
||||
const rest = ph.d - uebersprungen
|
||||
ph.start?.(rest)
|
||||
timer = window.setTimeout(() => {
|
||||
ph.ende?.()
|
||||
ablauf(erg, liste, i + 1, 0)
|
||||
}, rest * 1000)
|
||||
}
|
||||
|
||||
async function abspielen(erg, uebersprungen = 0) {
|
||||
clearTimeout(timer)
|
||||
clearTimeout(enthuellung)
|
||||
gewinner.value = null
|
||||
ergebnis.value = erg
|
||||
dreht.value = true
|
||||
|
||||
// Erst ohne Übergang an den Anfang, sonst fährt die Bahn vom letzten
|
||||
// Ergebnis aus los
|
||||
fahren(0, KURVEN.fahrt, -3 * SCHRITT)
|
||||
await nextTick()
|
||||
void bahn.value?.offsetWidth
|
||||
|
||||
ablauf(erg, phasen(erg), 0, uebersprungen)
|
||||
}
|
||||
|
||||
async function laufendenDrehHolen() {
|
||||
try {
|
||||
const resp = await fetch('/api/spin')
|
||||
if (!resp.ok) return
|
||||
const daten = await resp.json()
|
||||
if (!daten.result) return
|
||||
if (daten.running) {
|
||||
abspielen(daten.result, daten.elapsed)
|
||||
} else {
|
||||
// Dreh ist durch: Bahn ohne Animation auf die Endstellung setzen,
|
||||
// sonst steht die Marke woanders als die Gewinnerkarte
|
||||
ergebnis.value = daten.result
|
||||
fahren(0, KURVEN.fahrt, stoppX(daten.result.stopps.at(-1)))
|
||||
gewinner.value = daten.result.gewinner
|
||||
else if (nachricht.type === 'reset') {
|
||||
zurueckgesetzt()
|
||||
emit('reset')
|
||||
laden_()
|
||||
}
|
||||
} catch {
|
||||
/* dann eben ohne */
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function laden_() {
|
||||
@@ -278,10 +47,7 @@ async function laden_() {
|
||||
if (!resp.ok) throw new Error('Vorschläge konnten nicht geladen werden')
|
||||
const daten = await resp.json()
|
||||
items.value = daten.items
|
||||
if (!ergebnis.value) {
|
||||
dauer.value = 0
|
||||
x.value = ruheX()
|
||||
}
|
||||
ruhen()
|
||||
} catch (err) {
|
||||
fehler.value = err.message
|
||||
} finally {
|
||||
@@ -289,44 +55,11 @@ async function laden_() {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await laden_()
|
||||
await laufendenDrehHolen()
|
||||
verbinden()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clearTimeout(timer)
|
||||
clearTimeout(enthuellung)
|
||||
abbestellen?.()
|
||||
})
|
||||
|
||||
defineExpose({ neu: laden_ })
|
||||
|
||||
// Läuft eine Ziehung, zeigt die Bahn deren Streifen — sonst die Vorschläge
|
||||
const kacheln = computed(() => {
|
||||
const erg = ergebnis.value
|
||||
if (erg) {
|
||||
const ziel = erg.stopps.at(-1).position
|
||||
return erg.streifen.map((i, k) => ({
|
||||
key: k,
|
||||
film: erg.karten[i],
|
||||
farbe: FARBEN[i % FARBEN.length],
|
||||
treffer: k === ziel,
|
||||
}))
|
||||
}
|
||||
return items.value.map((item, i) => ({
|
||||
key: i,
|
||||
film: item,
|
||||
farbe: FARBEN[i % FARBEN.length],
|
||||
treffer: false,
|
||||
}))
|
||||
})
|
||||
|
||||
async function drehen() {
|
||||
if (dreht.value || !items.value.length) return
|
||||
// Der Host hat geklickt — jetzt darf der Browser Ton. Der Server lost
|
||||
// aus und schickt das Ergebnis an alle, auch an uns.
|
||||
klang()
|
||||
// Der Server lost aus und schickt das Ergebnis an alle — auch an uns
|
||||
try {
|
||||
const resp = await fetch('/api/spin', { method: 'POST' })
|
||||
if (!resp.ok) fehler.value = 'Ziehen hat nicht geklappt.'
|
||||
@@ -335,18 +68,6 @@ async function drehen() {
|
||||
}
|
||||
}
|
||||
|
||||
function zurueckgesetzt() {
|
||||
clearTimeout(timer)
|
||||
clearTimeout(enthuellung)
|
||||
anlauf.value = false
|
||||
bruch.value = false
|
||||
ergebnis.value = null
|
||||
gewinner.value = null
|
||||
dreht.value = false
|
||||
emit('reset')
|
||||
laden_()
|
||||
}
|
||||
|
||||
async function zuruecksetzen() {
|
||||
// Der Server meldet den Reset an alle zurück, auch an uns
|
||||
try {
|
||||
@@ -356,45 +77,20 @@ async function zuruecksetzen() {
|
||||
}
|
||||
}
|
||||
|
||||
// Aus der eigenen Sammlung würfeln. Der Schalter trennt Horror von allem
|
||||
// anderen — für Abende, an denen es keiner gruselig mag.
|
||||
const nurHorror = ref(true)
|
||||
const sammlung = computed(() => props.merkliste.filter((e) => e.mine))
|
||||
const wuerfelt = ref(false)
|
||||
|
||||
function nachGenre(horror) {
|
||||
return sammlung.value.filter(
|
||||
(e) => (e.genres || []).includes('Horror') === horror
|
||||
)
|
||||
}
|
||||
|
||||
const auswahl = computed(() => nachGenre(nurHorror.value))
|
||||
const anzahlHorror = computed(() => nachGenre(true).length)
|
||||
const anzahlOhne = computed(() => nachGenre(false).length)
|
||||
|
||||
async function wuerfeln() {
|
||||
const treffer = auswahl.value
|
||||
if (!treffer.length || wuerfelt.value) return
|
||||
wuerfelt.value = true
|
||||
const film = treffer[Math.floor(Math.random() * treffer.length)]
|
||||
try {
|
||||
const resp = await fetch('/api/suggestions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ movie_id: film.movie_id }),
|
||||
})
|
||||
if (!resp.ok) throw new Error()
|
||||
} catch {
|
||||
fehler.value = 'Eintragen hat nicht geklappt.'
|
||||
} finally {
|
||||
wuerfelt.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function oeffnen(item) {
|
||||
const resp = await fetch(`/api/search/${item.movie_id}`)
|
||||
if (resp.ok) emit('open', await resp.json())
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await laden_()
|
||||
await laufendenDrehHolen()
|
||||
verbinden()
|
||||
})
|
||||
|
||||
onUnmounted(() => abbestellen?.())
|
||||
|
||||
defineExpose({ neu: laden_ })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -402,7 +98,10 @@ async function oeffnen(item) {
|
||||
<div class="buehne">
|
||||
<p v-if="fehler" class="note error">{{ fehler }}</p>
|
||||
|
||||
<div class="fenster">
|
||||
<div
|
||||
class="fenster"
|
||||
:style="{ '--kachel': `${masse.kachel}px`, '--luecke': `${masse.luecke}px` }"
|
||||
>
|
||||
<div class="marke" :class="{ anlauf, bruch }"></div>
|
||||
<div
|
||||
ref="bahn"
|
||||
@@ -468,61 +167,16 @@ async function oeffnen(item) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="liste">
|
||||
<div v-if="!host" class="sammlung-wuerfeln">
|
||||
<div class="schalter">
|
||||
<button type="button" :class="{ an: nurHorror }" @click="nurHorror = true">
|
||||
Horror ({{ anzahlHorror }})
|
||||
</button>
|
||||
<button type="button" :class="{ an: !nurHorror }" @click="nurHorror = false">
|
||||
Kein Horror ({{ anzahlOhne }})
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="wuerfeln"
|
||||
:disabled="!auswahl.length || wuerfelt"
|
||||
@click="wuerfeln"
|
||||
>
|
||||
Zufällig aus Sammlung
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h3>Vorschläge <span class="anzahl">{{ items.length }}</span></h3>
|
||||
<p v-if="!items.length && !laden" class="leer">Noch nichts vorgeschlagen.</p>
|
||||
<ul>
|
||||
<li v-for="(item, i) in items" :key="item.id">
|
||||
<button
|
||||
type="button"
|
||||
class="eintrag"
|
||||
:class="{ eigen: item.id === mine, gesehen: gesehenIds.has(item.movie_id) }"
|
||||
@click="oeffnen(item)"
|
||||
>
|
||||
<span
|
||||
class="cover"
|
||||
:style="{ borderColor: FARBEN[i % FARBEN.length] }"
|
||||
>
|
||||
<img
|
||||
v-if="posterUrl(item.poster_path, 'w154')"
|
||||
:src="posterUrl(item.poster_path, 'w154')"
|
||||
:alt="item.title"
|
||||
loading="lazy"
|
||||
/>
|
||||
</span>
|
||||
<span class="daten">
|
||||
<span class="titel">{{ item.title }}</span>
|
||||
<span class="meta">
|
||||
{{ item.year }}<template v-if="item.runtime"> · {{ item.runtime }} Min.</template>
|
||||
</span>
|
||||
<span v-if="item.genres?.length" class="genres">
|
||||
{{ item.genres.join(' · ') }}
|
||||
</span>
|
||||
<span class="von">{{ item.name || 'Anonym' }}</span>
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
<VorschlagsListe
|
||||
:items="items"
|
||||
:laden="laden"
|
||||
:host="host"
|
||||
:merkliste="merkliste"
|
||||
:mine="mine"
|
||||
:gesehen-ids="gesehenIds"
|
||||
@open="oeffnen"
|
||||
@fehler="fehler = $event"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -546,7 +200,8 @@ async function oeffnen(item) {
|
||||
.fenster {
|
||||
position: relative;
|
||||
align-self: stretch;
|
||||
height: 370px;
|
||||
/* Ein Poster ist 2:3, plus Luft für den Glow des Gewinners */
|
||||
height: calc(var(--kachel) * 1.5 + 40px);
|
||||
overflow: hidden;
|
||||
/* Links und rechts läuft der Streifen ins Dunkel aus */
|
||||
mask-image: linear-gradient(to right, transparent, #000 12%, #000 88%, transparent);
|
||||
@@ -557,7 +212,7 @@ async function oeffnen(item) {
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
gap: var(--luecke);
|
||||
will-change: transform;
|
||||
/* Die Kurve setzt das Skript je Phase */
|
||||
transition-property: transform;
|
||||
@@ -565,8 +220,8 @@ async function oeffnen(item) {
|
||||
|
||||
.kachel {
|
||||
position: relative;
|
||||
flex: 0 0 220px;
|
||||
height: 330px;
|
||||
flex: 0 0 var(--kachel);
|
||||
height: calc(var(--kachel) * 1.5);
|
||||
overflow: hidden;
|
||||
background: var(--bg-soft);
|
||||
border: 2px solid;
|
||||
@@ -634,6 +289,28 @@ async function oeffnen(item) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.marke::before,
|
||||
.marke::after {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
margin-left: -9px;
|
||||
content: '';
|
||||
border-right: 9px solid transparent;
|
||||
border-left: 9px solid transparent;
|
||||
}
|
||||
|
||||
.marke::before {
|
||||
top: 0;
|
||||
border-top: 14px solid var(--accent);
|
||||
}
|
||||
|
||||
.marke::after {
|
||||
bottom: 0;
|
||||
border-bottom: 14px solid var(--accent);
|
||||
}
|
||||
|
||||
/* Im Anlauf pulst die Marke dreimal — gleich geht es los */
|
||||
.marke.anlauf {
|
||||
animation: puls 0.33s ease-in-out 3;
|
||||
@@ -667,28 +344,6 @@ async function oeffnen(item) {
|
||||
}
|
||||
}
|
||||
|
||||
.marke::before,
|
||||
.marke::after {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
margin-left: -9px;
|
||||
content: '';
|
||||
border-right: 9px solid transparent;
|
||||
border-left: 9px solid transparent;
|
||||
}
|
||||
|
||||
.marke::before {
|
||||
top: 0;
|
||||
border-top: 14px solid var(--accent);
|
||||
}
|
||||
|
||||
.marke::after {
|
||||
bottom: 0;
|
||||
border-bottom: 14px solid var(--accent);
|
||||
}
|
||||
|
||||
.leer-bahn {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
@@ -702,6 +357,21 @@ async function oeffnen(item) {
|
||||
gap: 0.7rem;
|
||||
}
|
||||
|
||||
.dreh {
|
||||
padding: 0.7rem 2.2rem;
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
background: var(--accent);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.dreh:disabled {
|
||||
background: var(--line);
|
||||
color: var(--muted);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.reset {
|
||||
padding: 0.7rem 1.4rem;
|
||||
color: var(--muted);
|
||||
@@ -721,73 +391,6 @@ async function oeffnen(item) {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.leer {
|
||||
color: var(--muted);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.dreh {
|
||||
padding: 0.7rem 2.2rem;
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
background: var(--accent);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.dreh:disabled {
|
||||
background: var(--line);
|
||||
color: var(--muted);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.sammlung-wuerfeln {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.6rem;
|
||||
}
|
||||
|
||||
.schalter {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.schalter button {
|
||||
flex: 1;
|
||||
padding: 0.45rem 0.9rem;
|
||||
color: var(--muted);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.schalter button.an {
|
||||
color: var(--text);
|
||||
background: var(--bg-soft);
|
||||
}
|
||||
|
||||
.wuerfeln {
|
||||
padding: 0.5rem 1.1rem;
|
||||
font-size: 0.88rem;
|
||||
font-weight: 600;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.wuerfeln:hover:not(:disabled) {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.wuerfeln:disabled {
|
||||
color: var(--muted);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.gewinner {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
@@ -837,111 +440,6 @@ async function oeffnen(item) {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.liste h3 {
|
||||
margin-bottom: 0.7rem;
|
||||
color: var(--muted);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.09em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.anzahl {
|
||||
padding: 0.1rem 0.45rem;
|
||||
background: var(--bg-soft);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.liste ul {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.eintrag {
|
||||
display: flex;
|
||||
gap: 0.7rem;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
padding: 0.55rem;
|
||||
color: var(--text);
|
||||
font-size: 0.86rem;
|
||||
text-align: left;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* Schon gelaufen — die Ziehung überspringt diesen Film */
|
||||
.eintrag.gesehen {
|
||||
opacity: 0.45;
|
||||
filter: grayscale(1);
|
||||
}
|
||||
|
||||
.eintrag.gesehen:hover {
|
||||
opacity: 1;
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.eintrag:hover {
|
||||
background: var(--bg-soft);
|
||||
}
|
||||
|
||||
/* Der farbige Rand verbindet den Eintrag mit seinen Kacheln auf der Bahn */
|
||||
.cover {
|
||||
display: block;
|
||||
width: 46px;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
background: var(--bg-soft);
|
||||
border-left: 3px solid;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.cover img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
aspect-ratio: 2 / 3;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.daten {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
padding-top: 0.1rem;
|
||||
}
|
||||
|
||||
.titel {
|
||||
overflow: hidden;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.eintrag.eigen .titel {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.meta,
|
||||
.genres,
|
||||
.von {
|
||||
overflow: hidden;
|
||||
color: var(--muted);
|
||||
font-size: 0.74rem;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.von {
|
||||
margin-top: 0.15rem;
|
||||
font-size: 0.72rem;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.note {
|
||||
padding: 3rem 0;
|
||||
color: var(--muted);
|
||||
|
||||
Reference in new issue
Block a user