diff --git a/Flixy/Views/Detail/PlayableDetailView.swift b/Flixy/Views/Detail/PlayableDetailView.swift index a4a27fd..6e7af06 100644 --- a/Flixy/Views/Detail/PlayableDetailView.swift +++ b/Flixy/Views/Detail/PlayableDetailView.swift @@ -11,6 +11,7 @@ struct PlayableDetailView: View { @Environment(PlaybackCoordinator.self) private var playback @Environment(\.flixyAccent) private var accent + @Environment(\.horizontalSizeClass) private var sizeClass @State private var selectedVersion = 0 @State private var showsTechnicalDetails = false @@ -72,11 +73,13 @@ struct PlayableDetailView: View { metadata: item.kind == .episode ? nil : metadataLine, metadataContent: item.kind == .episode ? AnyView(episodeMetadata) : nil, summary: nil, - artworkURL: session.backend?.artworkURL( - path: item.art ?? item.seriesArt ?? item.thumb, - width: 900, - height: 1200 - ), + // Über `HeroArtwork`, nicht von Hand: hier standen 900×1200 — + // hochkant — für ein Bild, das im 16:9-Rahmen landet. Zu sehen war + // davon ein Streifen aus der Mitte. Derselbe Fehler war im + // Aufmacher schon einmal behoben worden, nur hier nicht. + artworkURL: session.backend.flatMap { + HeroArtwork.backdropURL(for: item, backend: $0, compact: sizeClass == .compact) + }, logoURL: session.backend?.rawImageURL(path: item.clearLogoPath) ) { Button { diff --git a/Flixy/Views/Detail/SeriesDetailView.swift b/Flixy/Views/Detail/SeriesDetailView.swift index f775c08..1f890f0 100644 --- a/Flixy/Views/Detail/SeriesDetailView.swift +++ b/Flixy/Views/Detail/SeriesDetailView.swift @@ -10,6 +10,7 @@ struct SeriesDetailView: View { let item: MediaItem @Environment(FlixySession.self) private var session + @Environment(\.horizontalSizeClass) private var sizeClass @State private var children: LoadState<[MediaItem]> = .loading var body: some View { @@ -44,11 +45,13 @@ struct SeriesDetailView: View { title: item.rowTitle, metadata: metadataLine, summary: nil, - artworkURL: session.backend?.artworkURL( - path: item.art ?? item.thumb, - width: 900, - height: 1200 - ), + // Über `HeroArtwork`, nicht von Hand: hier standen 900×1200 — + // hochkant — für ein Bild, das im 16:9-Rahmen landet. Zu sehen war + // davon ein Streifen aus der Mitte. Derselbe Fehler war im + // Aufmacher schon einmal behoben worden, nur hier nicht. + artworkURL: session.backend.flatMap { + HeroArtwork.backdropURL(for: item, backend: $0, compact: sizeClass == .compact) + }, logoURL: session.backend?.rawImageURL(path: item.clearLogoPath) ) { DownloadAllButton(container: item) diff --git a/web/frontend/index.html b/web/frontend/index.html index c4d3243..460ddcc 100644 --- a/web/frontend/index.html +++ b/web/frontend/index.html @@ -30,6 +30,11 @@ --s-tight: 4px; --s-snug: 8px; --s-regular: 12px; --s-loose: 16px; --s-section: 24px; --s-margin: 16px; + /* Die Kachelbreite. Der Regler in den Einstellungen verstellt genau diese + eine Zahl — Reihen, Raster und Querformat rechnen sich daraus ab. */ + --card-w: 150px; + --sidebar-w: 235px; + --font: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI Variable Text", "Segoe UI", system-ui, Roboto, sans-serif; } @@ -141,7 +146,7 @@ img { display: block; } padding: 0 var(--s-margin) var(--s-section); display: flex; flex-direction: column; gap: var(--s-snug); } -#hero .logo { max-width: 260px; max-height: 76px; object-fit: contain; } +#hero .logo { max-width: 286px; max-height: 84px; object-fit: contain; } /* 10 % über der App-Größe, auf Wunsch */ #hero h2 { font-size: 34px; font-weight: 700; margin: 0; letter-spacing: -.02em; } #hero .meta { color: var(--text-2); font-size: 13px; } #hero .summary { @@ -149,12 +154,27 @@ img { display: block; } display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } #hero .actions { display: flex; gap: var(--s-regular); margin-top: var(--s-tight); } -#hero .dots { display: flex; gap: var(--s-snug); justify-content: center; padding: var(--s-regular) 0; } -#hero .dots i { +.dots { display: flex; gap: var(--s-snug); justify-content: center; padding: var(--s-regular) 0 var(--s-section); } +.dots button { width: 6px; height: 6px; border-radius: 999px; background: rgba(255,255,255,.35); - transition: width .25s ease, background .25s ease; + padding: 0; transition: width .25s ease, background .25s ease; } -#hero .dots i.on { width: 18px; background: var(--accent); } +.dots button:hover { background: rgba(255,255,255,.6); } +.dots button.on { width: 18px; background: var(--accent); } + +/* Blättern von Hand. Die Pfeile liegen auf dem Bild und halten sich zurück, + bis der Zeiger da ist — auf einem Zeigegerät. Wer wischt, braucht sie nicht. */ +.hero-nav { + position: absolute; top: 50%; transform: translateY(-50%); z-index: 2; + width: 44px; height: 44px; border-radius: 999px; padding: 0; + background: rgba(20,20,23,.55); color: var(--text); font-size: 20px; + backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); + border: 1px solid var(--separator); opacity: 0; transition: opacity .2s ease; +} +#hero:hover .hero-nav { opacity: 1; } +.hero-nav.prev { left: var(--s-margin); } +.hero-nav.next { right: var(--s-margin); } +@media (hover: none) { .hero-nav { display: none; } } /* ---- Reihen -------------------------------------------------------------- */ .row { margin-bottom: var(--s-section); } @@ -169,8 +189,8 @@ img { display: block; } } .strip::-webkit-scrollbar { display: none; } -.card { flex: 0 0 auto; width: 150px; scroll-snap-align: start; background: none; padding: 0; text-align: left; } -.card.wide { width: 260px; } +.card { flex: 0 0 auto; width: var(--card-w); scroll-snap-align: start; background: none; padding: 0; text-align: left; } +.card.wide { width: calc(var(--card-w) * 1.73); } .card .art { width: 100%; aspect-ratio: 2/3; border-radius: var(--r-card); background: var(--placeholder) center/cover no-repeat; @@ -187,7 +207,7 @@ img { display: block; } /* ---- Raster (Mediathek) -------------------------------------------------- */ .grid { - display: grid; grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); + display: grid; grid-template-columns: repeat(auto-fill, minmax(calc(var(--card-w) * .75), 1fr)); gap: var(--s-loose) var(--s-regular); padding: 0 var(--s-margin) var(--s-section); } .grid .card { width: auto; } @@ -208,6 +228,58 @@ img { display: block; } #sheet p { color: var(--text-2); font-size: 14px; } #sheet video { width: 100%; border-radius: var(--r-card); background: #000; margin-top: var(--s-regular); } +/* ---- Die Navigation an der Seite ----------------------------------------- + Dieselbe Wahl wie in der App: die Leiste sitzt oben als Insel oder links + als Spalte. Links bleibt sie stehen, statt beim Scrollen wegzufahren — + dort nimmt sie niemandem etwas weg. */ +#app.side #chrome { + top: 0; bottom: 0; left: 0; right: auto; width: calc(var(--sidebar-w) + 24px); + align-items: stretch; padding: 12px; transform: none !important; opacity: 1 !important; +} +#app.side .island { + flex-direction: column; align-items: stretch; gap: 2px; + width: 100%; max-width: none; overflow-y: auto; overflow-x: hidden; + padding: var(--s-regular) var(--s-snug); +} +#app.side .island button { text-align: left; border-radius: var(--r-control); } +#app.side .island .sep { width: auto; height: 1px; margin: var(--s-snug); } +#app.side #content { margin-left: calc(var(--sidebar-w) + 24px); } +#app.side .pad-top { padding-top: var(--s-loose); } + +.island .brand { + display: none; font-size: 20px; font-weight: 700; letter-spacing: -.02em; + padding: var(--s-snug) var(--s-loose) var(--s-regular); color: var(--text); +} +#app.side .island .brand { display: block; } +.island .group-label { + display: none; font-size: 12px; color: var(--text-3); + padding: var(--s-regular) var(--s-loose) var(--s-tight); +} +#app.side .island .group-label { display: block; } + +/* ---- Einstellungen -------------------------------------------------------- */ +.settings { max-width: 620px; margin: 0 auto; padding: 0 var(--s-margin) var(--s-section); } +.settings h2 { font-size: 28px; letter-spacing: -.02em; margin: 0 0 var(--s-section); } +.setting { + background: var(--surface); border: 1px solid var(--separator); + border-radius: var(--r-sheet); padding: var(--s-loose); margin-bottom: var(--s-regular); +} +.setting h4 { margin: 0 0 var(--s-snug); font-size: 15px; font-weight: 600; } +.setting p { margin: 0 0 var(--s-regular); color: var(--text-2); font-size: 13px; } +.swatches { display: flex; flex-wrap: wrap; gap: var(--s-regular); } +.swatch { + width: 34px; height: 34px; border-radius: 999px; padding: 0; + border: 2px solid transparent; transition: transform .12s ease; +} +.swatch:hover { transform: scale(1.12); } +.swatch.on { border-color: var(--text); } +.seg { display: flex; gap: var(--s-snug); } +.seg button { flex: 1; background: var(--elevated); color: var(--text-2); border-radius: var(--r-control); padding: 10px; } +.seg button.on { background: var(--accent); color: #fff; } +input[type=range] { width: 100%; accent-color: var(--accent); } +.range-row { display: flex; align-items: center; gap: var(--s-regular); } +.range-row output { min-width: 56px; text-align: right; color: var(--text-2); font-size: 13px; } + /* ---- Zustände ------------------------------------------------------------ */ #content { padding-bottom: var(--s-section); } .pad-top { padding-top: calc(84px + env(safe-area-inset-top)); } @@ -357,12 +429,47 @@ function signOut() { location.reload(); } +/* ============================================================================ + Einstellungen + ---------------------------------------------------------------------------- + Im Browser gespeichert, nicht auf dem Server: es sind Fragen der Ansicht + auf *diesem* Gerät. Wer am Fernseher größere Kacheln will als am Laptop, + soll das haben, ohne es dem anderen mitzuteilen. + ========================================================================= */ +const PALETTE = [ + ['Ember','#E5484D'], ['Tangerine','#F76B15'], ['Amber','#FFB224'], ['Lime','#99D52A'], + ['Mint','#3DD68C'], ['Teal','#12A594'], ['Sky','#52A9FF'], ['Indigo','#7C77E8'], + ['Orchid','#C04AC9'], ['Rose','#F76190'], +]; + +const P = { + accent: localStorage.getItem('flixy.accent') || '#C04AC9', + nav: localStorage.getItem('flixy.nav') || 'top', // 'top' | 'side' + cardW: +(localStorage.getItem('flixy.cardW') || 150), +}; + +function applyPrefs() { + const r = document.documentElement.style; + r.setProperty('--accent', P.accent); + r.setProperty('--card-w', P.cardW + 'px'); + el('app').classList.toggle('side', P.nav === 'side'); + // An der Seite fährt nichts weg — die Regel gilt nur für die Insel oben. + if (P.nav === 'side') el('chrome').classList.remove('away'); +} + +function setPref(key, value) { + P[key] = value; + localStorage.setItem('flixy.' + key, value); + applyPrefs(); +} + /* ---------- Navigation ---------- */ let views = [], current = null; async function start() { el('login').classList.add('hidden'); el('app').classList.remove('hidden'); + applyPrefs(); el('content').innerHTML = '
'; try { const r = await api(`/Users/${S.userId}/Views`); @@ -383,11 +490,26 @@ function buildIsland() { b.textContent = label; b.dataset.key = key; b.onclick = () => go(key); isl.appendChild(b); + return b; }; + const bit = (cls, text) => { + const d = document.createElement('div'); + d.className = cls; if (text) d.textContent = text; + isl.appendChild(d); + }; + + // Die Marke steht nur in der Spalte — oben nähme sie Platz weg, den die + // Einträge brauchen. + bit('brand', 'Flixy'); add('Start', 'home'); - if (views.length) isl.appendChild(Object.assign(document.createElement('div'), { className: 'sep' })); - views.forEach((v) => add(v.Name, 'lib:' + v.Id)); - isl.appendChild(Object.assign(document.createElement('div'), { className: 'sep' })); + add('Einstellungen', 'settings'); + + if (views.length) { + bit('sep'); + bit('group-label', 'Mediatheken'); + views.forEach((v) => add(v.Name, 'lib:' + v.Id)); + } + bit('sep'); add('Abmelden', 'out'); } @@ -397,7 +519,71 @@ function go(key) { [...el('island').querySelectorAll('button')].forEach((b) => b.classList.toggle('active', b.dataset.key === key)); window.scrollTo({ top: 0 }); - key === 'home' ? renderHome() : renderLibrary(key.slice(4)); + el('content').classList.remove('pad-top'); + if (key === 'home') renderHome(); + else if (key === 'settings') renderSettings(); + else renderLibrary(key.slice(4)); +} + +/* ---------- Einstellungen ---------- */ +function renderSettings() { + const c = el('content'); + c.classList.add('pad-top'); + c.innerHTML = ` +Dieselbe Palette wie in der App.
+ +Oben als schwebende Insel oder links als Spalte. Oben blendet sie + beim Scrollen aus, an der Seite bleibt sie stehen.
+ +Wie groß Plakate und Vorschaubilder erscheinen.
+