diff --git a/server/src/Flaro.Api/Aufmacherhoehe.cs b/server/src/Flaro.Api/Aufmacherhoehe.cs new file mode 100644 index 0000000..baf935b --- /dev/null +++ b/server/src/Flaro.Api/Aufmacherhoehe.cs @@ -0,0 +1,47 @@ +namespace Flaro.Api; + +/// Wie hoch der Aufmacher auf der Startseite steht. +/// +/// +/// +/// Vier Stufen, keine Zahl. Eine Pixelzahl einzustellen hiesse, sie +/// für jeden Bildschirm neu einzustellen — der Aufmacher misst sich an der +/// Fensterhöhe, nicht an einer festen Grösse. Vier Wörter sind das, was man +/// dazu zu sagen hat. +/// +/// +/// Was die Stufen bedeuten, steht im Regelwerk, nicht hier: der Server +/// weiss nichts von Bildpunkten, er merkt sich nur die Wahl. Nachzulesen bei +/// .hero in style.css. +/// +/// +/// Serverweit. Dieselbe Linie wie bei den Wiedergaberegeln: eine +/// Entscheidung des Gastgebers, nicht des Gastes. +/// +/// +public static class Aufmacherhoehe +{ + public const string Schlüssel = "darstellung.aufmacher.hoehe"; + + /// Die Mitte — das, was vorher fest verdrahtet war. + public const string Vorgabe = "mittel"; + + private static readonly string[] Stufen = ["niedrig", "mittel", "hoch", "sehrhoch"]; + + /// Macht aus dem Gespeicherten eine der vier Stufen. + /// + /// + /// Unbekanntes wird zur Vorgabe statt zu einem Fehler: in der Spalte + /// steht, was irgendwann einmal hineingeschrieben wurde, und eine + /// Startseite, die daran scheitert, wäre die schlechtere Antwort. + /// + public static string Lesen(string? wert) + { + var blank = wert?.Trim().ToLowerInvariant() ?? ""; + return Stufen.Contains(blank) ? blank : Vorgabe; + } + + /// Ob das eine Stufe ist, die es gibt. + public static bool Kennt(string? wert) => + wert is not null && Stufen.Contains(wert.Trim().ToLowerInvariant()); +} diff --git a/server/src/Flaro.Api/Endpoints/Contracts.cs b/server/src/Flaro.Api/Endpoints/Contracts.cs index c1b313c..31ab5c8 100644 --- a/server/src/Flaro.Api/Endpoints/Contracts.cs +++ b/server/src/Flaro.Api/Endpoints/Contracts.cs @@ -6,7 +6,16 @@ namespace Flaro.Api.Endpoints; public sealed record SystemInfo( string Version, string Build, string ServerName, bool NeedsSetup, - IReadOnlyList Attributions); + IReadOnlyList Attributions, + /// Wie hoch der Aufmacher steht: „niedrig", „mittel", „hoch", „sehrhoch". + /// + /// + /// Hier und nicht in einer eigenen Abfrage. Diese Auskunft holt + /// der Client ohnehin als Erstes, noch vor der Anmeldung. Käme die Höhe + /// später, stünde der Aufmacher einen Augenblick in der Vorgabe und + /// spränge dann — genau das, was eine einstellbare Höhe vermeiden soll. + /// + string HeroHeight = "mittel"); /// Ein Ablageort des Servers, mitsamt Belegung. /// @@ -121,6 +130,9 @@ public sealed record CreateUserRequest(string UserName, string Password, bool Is /// Adminrecht geben oder nehmen. public sealed record RechtRequest(bool IsAdministrator); +/// Eine der vier Stufen für den Aufmacher. +public sealed record HoeheRequest(string Height); + /// Eine Bibliothek, wie die Oberfläche sie zeigt. /// /// diff --git a/server/src/Flaro.Api/Endpoints/SystemEndpoints.cs b/server/src/Flaro.Api/Endpoints/SystemEndpoints.cs index ee4ae61..9164e07 100644 --- a/server/src/Flaro.Api/Endpoints/SystemEndpoints.cs +++ b/server/src/Flaro.Api/Endpoints/SystemEndpoints.cs @@ -48,7 +48,9 @@ public static class SystemEndpoints Attributions: [ new AttributionResponse(Attribution.Tmdb, Attribution.TmdbLogoUrl, Attribution.TmdbUrl), - ]))) + ], + HeroHeight: Aufmacherhoehe.Lesen((await db.Settings.AsNoTracking() + .FirstOrDefaultAsync(e => e.Key == Aufmacherhoehe.Schlüssel))?.Value)))) .AllowAnonymous(); // **Wer schaut gerade was.** @@ -190,6 +192,26 @@ public static class SystemEndpoints return Results.NoContent(); }).RequireAuthorization(FlaroPolicies.Administrator); + // **Wie hoch der Aufmacher steht.** Gelesen wird er mit der + // Systemauskunft weiter oben — die holt der Client ohnehin als + // Erstes, und so steht die Höhe schon beim ersten Zeichnen fest. + app.MapPut("/api/v1/system/hero-height", async ( + HoeheRequest anfrage, FlaroContext db, CancellationToken ct) => + { + if (!Aufmacherhoehe.Kennt(anfrage.Height)) + return Einwand.Bitte("Unbekannte Stufe. Möglich sind: niedrig, mittel, hoch, sehrhoch."); + + var wert = anfrage.Height.Trim().ToLowerInvariant(); + var vorhanden = await db.Settings + .FirstOrDefaultAsync(e => e.Key == Aufmacherhoehe.Schlüssel, ct); + if (vorhanden is null) + db.Settings.Add(new ServerSetting { Key = Aufmacherhoehe.Schlüssel, Value = wert }); + else vorhanden.Value = wert; + + await db.SaveChangesAsync(ct); + return Results.NoContent(); + }).RequireAuthorization(FlaroPolicies.Administrator); + app.MapPost("/api/v1/system/setup", async (SetupRequest anfrage, FlaroContext db) => { if (string.IsNullOrWhiteSpace(anfrage.UserName) || anfrage.Password.Length < 8) diff --git a/web/app/src/main.ts b/web/app/src/main.ts index c484a7a..324e031 100644 --- a/web/app/src/main.ts +++ b/web/app/src/main.ts @@ -37,6 +37,10 @@ async function hochfahren(): Promise { const auskunft = await systemauskunft(); hinweise = auskunft.attributions; serverName = auskunft.serverName ?? 'Flaro'; + // **So früh wie möglich.** Die Höhe des Aufmachers steht als Marke am + // ``; käme sie später, stünde er einen Augenblick in der Vorgabe + // und spränge dann — genau das, was eine einstellbare Höhe vermeiden soll. + document.documentElement.dataset.aufmacher = auskunft.heroHeight ?? 'mittel'; if (auskunft.needsSetup) { zeigeAnmeldung(einrichtung(() => location.reload())); return; } diff --git a/web/app/src/style.css b/web/app/src/style.css index 6a941cf..7adafcf 100644 --- a/web/app/src/style.css +++ b/web/app/src/style.css @@ -498,6 +498,33 @@ svg { width: 20px; height: 20px; display: block; flex: none; } .strip::-webkit-scrollbar { display: none; } .strip > * { scroll-snap-align: start; } +/* **Ein Text, der beim Überfahren einen anderen zeigt.** + * + * Für die Auflösung: in Ruhe steht „Full HD" da, beim Überfahren „1080p · + * 1920 × 800". Ein Tooltip täte dasselbe, aber verzögert, über der + * Nachbarzeile und am Finger gar nicht. + * + * **Beide liegen übereinander, nicht nacheinander.** Das Feld ist so breit + * wie der breitere der beiden — sonst rückte beim Überfahren die halbe + * Tabelle. Gerastert statt absolut positioniert, damit die Zeilenhöhe von + * selbst stimmt. */ +.wechseltext { display: inline-grid; } +.wechseltext > span { grid-area: 1 / 1; transition: opacity .15s ease; } +.wechseltext > .genau { opacity: 0; color: var(--text-dim); } + +@media (hover: hover) { + .wechseltext:hover > span { opacity: 0; } + .wechseltext:hover > .genau { opacity: 1; } +} + +/* Ohne Zeiger lässt sich nichts überfahren — dort steht das Genaue + dauerhaft daneben, statt unerreichbar zu sein. */ +@media (hover: none) { + .wechseltext { display: inline; } + .wechseltext > .genau { opacity: 1; } + .wechseltext > .genau::before { content: ' · '; } +} + /* ---------- Detailseite ---------- */ .detail { position: relative; } @@ -1137,9 +1164,24 @@ body:has(.pl.pl-klein) .auswahlleiste { bottom: 92px; } * Breite ist der grösste Einzelunterschied im Eindruck — und ein Layout-, * kein Farbproblem. */ +/* **Vier Stufen, serverweit eingestellt.** + * + * Eine Pixelzahl einzustellen hiesse, sie für jeden Bildschirm neu + * einzustellen — der Aufmacher misst sich an der Fensterhöhe. Vier Wörter + * sind das, was man dazu zu sagen hat, und `main.ts` setzt sie als Marke am + * ``, sobald die Systemauskunft da ist. + * + * Die Mitte ist, was vorher fest verdrahtet war. Oben ist Schluss, bevor die + * erste Plakatreihe ganz unter den Falz rutscht: sie soll zeigen, dass unten + * noch etwas kommt. */ +:root { --hero-hoehe: min(72vh, 640px); } +html[data-aufmacher="niedrig"] { --hero-hoehe: min(62vh, 560px); } +html[data-aufmacher="hoch"] { --hero-hoehe: min(82vh, 720px); } +html[data-aufmacher="sehrhoch"] { --hero-hoehe: min(92vh, 800px); } + .hero { position: relative; - height: min(72vh, 640px); + height: var(--hero-hoehe); /* Randlos: der Inhaltsbereich hat seitlich Polster, das Bild soll es nicht haben. Negative Ränder statt eines eigenen Containers. */ margin: calc(var(--s-page) * -1) calc(var(--s-page) * -1) var(--s-section); @@ -1334,6 +1376,9 @@ body:has(.pl.pl-klein) .auswahlleiste { bottom: 92px; } @media (max-width: 860px) { .hero { + /* Am Telefon eine Stufe tiefer als gewählt: dort ist der Aufmacher im + Verhältnis zum Bildschirm ohnehin höher, und über 60 vh bleibt von der + ersten Reihe nichts mehr übrig. */ height: min(52vh, 420px); margin: calc(var(--s-loose) * -1) calc(var(--s-loose) * -1) var(--s-section); } diff --git a/web/app/src/views/einstellungen/uebersicht.ts b/web/app/src/views/einstellungen/uebersicht.ts index 3b52e2e..2d66c40 100644 --- a/web/app/src/views/einstellungen/uebersicht.ts +++ b/web/app/src/views/einstellungen/uebersicht.ts @@ -1,5 +1,5 @@ import { - doppelteZusammenlegen, metadatenEinstellung, metadatenSetzen, servernameSetzen, + doppelteZusammenlegen, metadatenEinstellung, metadatenSetzen, servernameSetzen, aufmacherhoeheSetzen, systemauskunft, laufendeWiedergaben, serverzustand, bildAdresse, speicherorte, bildspeicherLeeren, type Wiedergabe, } from '../../api'; @@ -144,6 +144,30 @@ export function serverbereich(geaendert: () => void): HTMLElement { tun: async wert => { await servernameSetzen(wert('name')); location.reload(); }, }))); + const stufen = [ + { wert: 'niedrig', text: 'Niedrig' }, + { wert: 'mittel', text: 'Mittel' }, + { wert: 'hoch', text: 'Hoch' }, + { wert: 'sehrhoch', text: 'Sehr hoch' }, + ]; + const jetzige = auskunft.heroHeight ?? 'mittel'; + liste.appendChild(zeile( + 'Höhe des Aufmachers', + stufen.find(s => s.wert === jetzige)?.text ?? 'Mittel', + 'Das grosse Bild oben auf der Startseite. Gemessen wird in ' + + 'Fensterhöhe, nicht in Bildpunkten — sonst müsste man es an jedem ' + + 'Bildschirm neu einstellen. Am Telefon bleibt es flacher: dort ' + + 'wäre von der ersten Plakatreihe sonst nichts mehr zu sehen.', + () => dialog({ + titel: 'Höhe des Aufmachers', + text: 'Gilt für alle auf diesem Server.', + felder: [{ + name: 'stufe', beschriftung: 'Höhe', auswahl: stufen, wert: jetzige, + }], + bestaetigen: 'Übernehmen', + tun: async wert => { await aufmacherhoeheSetzen(wert('stufe')); location.reload(); }, + }))); + // **Wartung, nicht Alltag.** Nach jedem Scan passiert das von selbst; // dieser Knopf ist für den Fall, dass doch wieder Dubletten auftauchen — // etwa nachdem eine Zuordnung von Hand geändert wurde.