Ein Knopf, der nichts tut, ist schlimmer als keiner
Der geplante Film zeigte weiter „Abspielen" und „Herunterladen", und wer
drückte, bekam „Dieser Titel ist ab dem 6.10. um 18 Uhr verfügbar". Die
Sperre im Server war richtig; die Oberfläche wusste nur nichts davon.
Gemessen: die Liste führte `PublishAt` seit jeher mit, die Einzelabfrage
`GET /api/v1/movies/{id}` nicht — und die Detailseite entscheidet genau
daran. `publishAt` kam also immer als `null` an, die Prüfung lief ins
Leere. Dasselbe fehlte bei `GET /api/v1/series/{id}` und bei den Folgen.
Jetzt steht dort, wo sonst der Knopf stünde: „Bald ist es soweit", mit
dem Termin darunter. Kein Versprechen, das beim Klicken zurückgenommen
wird.
Der Test prüfte bisher nur die Liste — darum fiel es nicht auf. Er fragt
jetzt die Detailseite und schlägt ohne die Korrektur fehl.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
255eb7c2f0
commit
343b0cdcfb
@@ -429,7 +429,8 @@ public sealed record SeriesResponse(
|
||||
public sealed record EpisodeResponse(
|
||||
Guid Id, int Season, int Episode, string? Title, string? Overview, string? StillImage,
|
||||
IReadOnlyList<MediaFileResponse> Files,
|
||||
ItemStateResponse? State);
|
||||
ItemStateResponse? State,
|
||||
DateTimeOffset? PublishAt = null);
|
||||
|
||||
public sealed record Page<T>(IReadOnlyList<T> Items, int Total);
|
||||
|
||||
|
||||
@@ -627,7 +627,8 @@ public static class LibraryContentEndpoints
|
||||
OriginalTitle: film.OriginalTitle,
|
||||
TitlePinned: film.TitlePinned,
|
||||
SortTitle: film.SortTitleValue,
|
||||
SortTitlePinned: film.SortTitlePinned));
|
||||
SortTitlePinned: film.SortTitlePinned,
|
||||
PublishAt: film.PublishAt));
|
||||
}).RequireAuthorization();
|
||||
|
||||
app.MapGet("/api/v1/series/{id:guid}", async (
|
||||
@@ -640,7 +641,7 @@ public static class LibraryContentEndpoints
|
||||
|
||||
var folgen = await db.Episodes.AsNoTracking()
|
||||
.Where(f => f.SeriesId == id)
|
||||
.Select(f => new { f.Id, f.SeasonNumber })
|
||||
.Select(f => new { f.Id, f.SeasonNumber, f.PublishAt })
|
||||
.ToListAsync();
|
||||
var gesehen = await GeseheneFolgen(db, wer, folgen.Select(f => f.Id).ToList());
|
||||
|
||||
@@ -661,7 +662,8 @@ public static class LibraryContentEndpoints
|
||||
State: (await ZuständeZu(db, wer, serien: [id])).GetValueOrDefault(id),
|
||||
TitlePinned: serie.TitlePinned,
|
||||
SortTitle: serie.SortTitleValue,
|
||||
SortTitlePinned: serie.SortTitlePinned));
|
||||
SortTitlePinned: serie.SortTitlePinned,
|
||||
PublishAt: folgen.Min(f => f.PublishAt)));
|
||||
}).RequireAuthorization();
|
||||
|
||||
// **Die nächste Folge nach dieser.** Reihenfolge ist Staffel, dann
|
||||
@@ -681,7 +683,7 @@ public static class LibraryContentEndpoints
|
||||
.Where(f => f.SeasonNumber > diese.SeasonNumber
|
||||
|| (f.SeasonNumber == diese.SeasonNumber && f.EpisodeNumber > diese.EpisodeNumber))
|
||||
.OrderBy(f => f.SeasonNumber).ThenBy(f => f.EpisodeNumber)
|
||||
.Select(f => new { f.Id, f.SeasonNumber, f.EpisodeNumber, f.Title, f.Overview, f.StillImage })
|
||||
.Select(f => new { f.Id, f.SeasonNumber, f.EpisodeNumber, f.Title, f.Overview, f.StillImage, f.PublishAt })
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
// Keine weitere Folge ist kein Fehler — es war die letzte.
|
||||
@@ -706,7 +708,8 @@ public static class LibraryContentEndpoints
|
||||
naechste.Id, naechste.SeasonNumber, naechste.EpisodeNumber,
|
||||
naechste.Title, naechste.Overview, naechste.StillImage ?? serienBild,
|
||||
dateien.Select(d => Datei(d.Id, d.Path, d.SizeBytes, d.Container, d.Duration, spuren, d.VersionLabel)).ToList(),
|
||||
stände.GetValueOrDefault(naechste.Id)));
|
||||
stände.GetValueOrDefault(naechste.Id),
|
||||
naechste.PublishAt));
|
||||
}).RequireAuthorization();
|
||||
|
||||
app.MapGet("/api/v1/series/{id:guid}/episodes", async (
|
||||
@@ -719,7 +722,7 @@ public static class LibraryContentEndpoints
|
||||
|
||||
var folgen = await abfrage
|
||||
.OrderBy(f => f.SeasonNumber).ThenBy(f => f.EpisodeNumber)
|
||||
.Select(f => new { f.Id, f.SeasonNumber, f.EpisodeNumber, f.Title, f.Overview, f.StillImage })
|
||||
.Select(f => new { f.Id, f.SeasonNumber, f.EpisodeNumber, f.Title, f.Overview, f.StillImage, f.PublishAt })
|
||||
.ToListAsync();
|
||||
|
||||
var wurzel = await MediathekspfadFuer(db, wer,
|
||||
@@ -743,7 +746,8 @@ public static class LibraryContentEndpoints
|
||||
f.StillImage ?? serienBild,
|
||||
dateien.Where(d => d.EpisodeId == f.Id)
|
||||
.Select(d => Datei(d.Id, d.Path, d.SizeBytes, d.Container, d.Duration, spuren, d.VersionLabel, wurzel)).ToList(),
|
||||
stände.GetValueOrDefault(f.Id))).ToList();
|
||||
stände.GetValueOrDefault(f.Id),
|
||||
f.PublishAt)).ToList();
|
||||
|
||||
return Results.Ok(new Page<EpisodeResponse>(items, items.Count));
|
||||
}).RequireAuthorization();
|
||||
|
||||
@@ -165,6 +165,16 @@ public class FreigabeTests
|
||||
Assert.That(gestroemt.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden));
|
||||
});
|
||||
|
||||
// **Und die Detailseite muss es wissen.** Sie entscheidet daran, ob
|
||||
// sie „Abspielen" zeigt oder den Termin. Die Liste führte `PublishAt`
|
||||
// von Anfang an mit, die Einzelabfrage nicht — der Knopf stand also
|
||||
// da, und wer ihn drückte, bekam eine Absage. Ein Knopf, der nichts
|
||||
// tut, ist schlimmer als keiner.
|
||||
var einzeln = await gast.GetFromJsonAsync<MovieResponse>(
|
||||
$"/api/v1/movies/{filme.Items.Single().Id}");
|
||||
Assert.That(einzeln!.PublishAt, Is.Not.Null,
|
||||
"die Detailseite braucht den Termin, sonst zeigt sie Abspielen an");
|
||||
|
||||
// **Auch für Administratoren.** Eine Hintertür für eine Rolle wäre
|
||||
// eine Regel, die manchmal gilt; wer vorziehen will, hat den Knopf.
|
||||
var alsAdminGeladen = await alsAdmin.GetAsync($"/api/v1/files/{datei.Id}/download");
|
||||
|
||||
@@ -87,6 +87,8 @@ export type Folge = {
|
||||
id: string; season: number; episode: number;
|
||||
title: string | null; overview: string | null; stillImage: string | null; files: Datei[];
|
||||
state: Stand | null;
|
||||
/** Wann sie freigegeben ist. Liegt das in der Zukunft, gibt es sie noch nicht. */
|
||||
publishAt?: string | null;
|
||||
};
|
||||
|
||||
/** Eine Kachel in „Weiterschauen" oder „Nächste Folge" — Film wie Folge. */
|
||||
|
||||
@@ -1644,11 +1644,13 @@ body:has(.auswahlleiste:not(.hidden)) .hinweise { bottom: 104px; }
|
||||
.pruef-gruppe strong { font-size: 14px; }
|
||||
.pruef-gruppe .winzig { flex: 1 1 240px; }
|
||||
|
||||
/* „Verfügbar ab …" steht dort, wo sonst Abspielen stünde. */
|
||||
/* Steht dort, wo sonst Abspielen stünde — kein Knopf, der nichts tut. */
|
||||
.verfuegbar-ab {
|
||||
display: inline-flex; align-items: center; gap: var(--s-snug);
|
||||
padding: 10px 16px; border-radius: var(--r-control);
|
||||
display: inline-flex; align-items: center; gap: var(--s-regular);
|
||||
padding: 10px 18px; border-radius: var(--r-control);
|
||||
background: var(--raised); border: 1px solid var(--line);
|
||||
font-size: 14px; font-weight: 600; color: var(--text-dim);
|
||||
}
|
||||
.verfuegbar-ab .icon svg { width: 18px; height: 18px; }
|
||||
.verfuegbar-ab .icon svg { width: 22px; height: 22px; color: var(--accent); }
|
||||
.verfuegbar-text { display: flex; flex-direction: column; line-height: 1.3; }
|
||||
.verfuegbar-text strong { font-size: 14px; }
|
||||
.verfuegbar-text span { font-size: 13px; color: var(--text-dim); }
|
||||
|
||||
@@ -251,9 +251,18 @@ export function detailseite(art: Art, id: string, zurueck: () => void): HTMLElem
|
||||
if (nochNicht) {
|
||||
const hinweisZeile = document.createElement('div');
|
||||
hinweisZeile.className = 'verfuegbar-ab';
|
||||
hinweisZeile.append(icon('uhr'), document.createTextNode(
|
||||
`Verfügbar ab ${geplantAb!.toLocaleString('de-CH',
|
||||
{ dateStyle: 'long', timeStyle: 'short' })}`));
|
||||
|
||||
const text = document.createElement('div');
|
||||
text.className = 'verfuegbar-text';
|
||||
const oben = document.createElement('strong');
|
||||
oben.textContent = 'Bald ist es soweit';
|
||||
const unten = document.createElement('span');
|
||||
unten.textContent = `Ab ${geplantAb!.toLocaleString('de-CH',
|
||||
{ weekday: 'long', day: 'numeric', month: 'long', year: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit' })} Uhr`;
|
||||
text.append(oben, unten);
|
||||
|
||||
hinweisZeile.append(icon('uhr'), text);
|
||||
reihe.appendChild(hinweisZeile);
|
||||
}
|
||||
|
||||
@@ -585,7 +594,8 @@ export function detailseite(art: Art, id: string, zurueck: () => void): HTMLElem
|
||||
block.appendChild(welche);
|
||||
}
|
||||
block.appendChild(aktionsreihe(
|
||||
{ ...({} as Film), id: folge.id, files: folge.files } as Film, folge.state, datei));
|
||||
{ ...({} as Film), id: folge.id, files: folge.files,
|
||||
publishAt: folge.publishAt ?? null } as Film, folge.state, datei));
|
||||
});
|
||||
return block;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user