Ein Kontextmenü auf jeder Kachel — mitsamt Bildauswahl

Gesehen, Favorit und Weglegen gab es ausschliesslich auf der Detailseite.
In einer Übersicht mit zwanzig Titeln hiess "die hab ich alle gesehen"
vierzig Seitenwechsel. Im ganzen Quelltext stand kein einziger
contextmenu-Zuhörer.

Drei Wege hinein, weil es drei Arten gibt zu bedienen: Rechtsklick für die
Maus, Shift+F10 und die Kontextmenü-Taste für die Tastatur, langer Druck
für den Finger. Nur den Rechtsklick anzubieten hiesse, das Menü auf dem
Telefon zu verstecken. Der lange Druck bricht bei jeder Bewegung ab, sonst
löst Scrollen ihn aus.

Die Aktionen wirken auf der Kachel selbst. Die Liste neu zu laden verlöre
Bildlauf und Filter — für einen Haken.

Dazu die Bildauswahl. Es gab genau einen Bild-Endpunkt, und der lieferte
nur aus: wählte TMDB ein schlechtes Plakat, blieb es. Jetzt stehen Plakat,
Hintergrund und Schriftzug zur Wahl, dazu eigene Bilder bis 10 MB. Die
Kandidaten kosten nichts — TMDB liefert sie beim Einzelabruf ohnehin mit,
und bisher wurde alles ausser dem Schriftzug weggeworfen.

Drei Merker je Titel, nicht einer: man kann ein eigenes Plakat wollen und
trotzdem automatische Hintergründe. Ohne sie überschriebe das nächste
Auffrischen jede Handauswahl, und wer einmal ein besseres Plakat gesucht
hat, suchte es jedes Mal neu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-04 12:43:52 +02:00
co-authored by Claude Opus 5
parent e1399c808f
commit eb63271e92
16 changed files with 1759 additions and 11 deletions
+30 -2
View File
@@ -78,7 +78,7 @@ public sealed class MediaFile
public string? VersionLabel { get; set; }
}
public sealed class Movie
public sealed class Movie : Flaro.Core.Metadata.IBebildert
{
public Guid Id { get; init; } = Guid.CreateVersion7();
public required Guid LibraryId { get; init; }
@@ -110,6 +110,20 @@ public sealed class Movie
/// <summary>Ohne führenden Artikel — siehe <see cref="SortTitle"/>.</summary>
public required string SortTitleValue { get; set; }
/// <summary>Von Hand gewählt — ein Auffrischen überschreibt es nicht.</summary>
///
/// <remarks>
/// <b>Getrennt je Bild, nicht als ein Schalter.</b> Man kann ein eigenes
/// Plakat wollen und trotzdem automatische Hintergründe; ein gemeinsamer
/// Merker zwänge zu einer Entscheidung für alle drei.
///
/// Ohne das überschreibt der nächste Lauf von
/// <c>EnrichAsync(…, erneut: true)</c> jede Handauswahl wieder.
/// </remarks>
public bool PosterPinned { get; set; }
public bool BackdropPinned { get; set; }
public bool LogoPinned { get; set; }
/// <summary>Der Titel in der Sprache des Originals.</summary>
///
/// <remarks>
@@ -185,7 +199,7 @@ public sealed class Movie
public DateTimeOffset AddedAt { get; init; } = DateTimeOffset.UtcNow;
}
public sealed class Series
public sealed class Series : Flaro.Core.Metadata.IBebildert
{
public Guid Id { get; init; } = Guid.CreateVersion7();
public required Guid LibraryId { get; init; }
@@ -215,6 +229,20 @@ public sealed class Series
public required string Title { get; set; }
public required string SortTitleValue { get; set; }
/// <summary>Von Hand gewählt — ein Auffrischen überschreibt es nicht.</summary>
///
/// <remarks>
/// <b>Getrennt je Bild, nicht als ein Schalter.</b> Man kann ein eigenes
/// Plakat wollen und trotzdem automatische Hintergründe; ein gemeinsamer
/// Merker zwänge zu einer Entscheidung für alle drei.
///
/// Ohne das überschreibt der nächste Lauf von
/// <c>EnrichAsync(…, erneut: true)</c> jede Handauswahl wieder.
/// </remarks>
public bool PosterPinned { get; set; }
public bool BackdropPinned { get; set; }
public bool LogoPinned { get; set; }
/// <summary>Der Titel in der Sprache des Originals.</summary>
///
/// <remarks>
@@ -174,6 +174,38 @@ public interface IImageStore
{
/// <summary>Holt das Bild und gibt seinen Schlüssel zurück — oder <c>null</c>.</summary>
Task<string?> StoreAsync(RemoteImage bild, CancellationToken ct = default);
/// <summary>Legt ein hochgeladenes Bild ab.</summary>
///
/// <remarks>
/// Für eigene Plakate. Dieselbe Ablage wie für geholte — damit ein Bild
/// unter derselben Adresse liegt, egal woher es stammt.
/// </remarks>
/// <remarks>
/// <b>Mit Vorgabe.</b> Eine Ablage, die nur Geholtes kennt, lehnt das
/// Hochladen ab — das ist ehrlicher als eine Pflichtmethode, die jeder
/// Test-Doppelgänger nachbauen müsste, obwohl er sie nie braucht.
/// </remarks>
Task<string?> StoreStreamAsync(Stream inhalt, string? medientyp, CancellationToken ct = default) =>
Task.FromResult<string?>(null);
}
/// <summary>Was Bilder trägt — Film oder Serie.</summary>
///
/// <remarks>
/// <b>Damit das Setzen eines Plakats nur einmal geschrieben werden muss.</b>
/// Film und Serie tragen dieselben sechs Eigenschaften; ohne gemeinsame
/// Schnittstelle stünde derselbe Code zweimal da und liefe irgendwann
/// auseinander.
/// </remarks>
public interface IBebildert
{
string? PosterImage { get; set; }
string? BackdropImage { get; set; }
string? LogoImage { get; set; }
bool PosterPinned { get; set; }
bool BackdropPinned { get; set; }
bool LogoPinned { get; set; }
}
/// <summary>Ein Bild aus der Ablage wieder herausgeben.</summary>
@@ -186,3 +218,12 @@ public interface IImageReader
{
Task<Stream?> OpenAsync(string schlüssel, CancellationToken ct = default);
}
/// <summary>Ein Bild zur Auswahl — Plakat, Hintergrund oder Schriftzug.</summary>
///
/// <remarks>
/// <b>Mit Vorschau und Sprache.</b> Wer ein Plakat aussucht, sucht meist eins
/// in seiner Sprache, und er will es sehen, bevor er es nimmt — nicht danach.
/// </remarks>
public sealed record Bildkandidat(
string Kind, string Url, string ThumbUrl, string? Language, double? Vote);