**Antippen startet, langes Drücken führt auf die Detailseite.** Am Telefon und am iPad; am Mac bleibt der Klick die Seite, denn dort gibt es den Abspielknopf beim Überfahren und langes Drücken ist keine übliche Geste. Die Hürde war nicht die Geste, sondern das Schieben: ein `NavigationLink` schiebt selbst, ein Menüeintrag kann es nicht. Der Stapel ist deshalb ein eigener Typ mit Pfad (`MediaStack`) und reicht eine Schiebefunktion über die Umgebung nach innen — denselben Weg nimmt schon die Meldung, dass sich am Server etwas geändert hat. **Zwei Knöpfe übereinander auf der Detailseite.** Das Monogramm hing als Überzug an `mediaStack` — also über **jedem** Bildschirm dieses Stapels, auch den geschobenen, und dort über deren Drei-Punkte-Menü. Es gilt jetzt für die Startseite und endet mit ihr. **Der Kopf der Detailseite war viel zu flach.** Dort stand kein Seitenverhältnis, also galt die Vorgabe 16∶9 — während `HeroArtwork` auf einem schmalen Gerät ein **Plakat** anfordert, also hochkant. Zu sehen war ein Streifen aus der Mitte. Der Aufmacher auf der Startseite macht es seit Langem richtig; die beiden Detailseiten nicht. **Die Mediathekenleiste stand linksbündig in einer halbleeren Kapsel.** Jetzt ist sie so breit wie ihr Inhalt und mittig. `ViewThatFits` nimmt die ungescrollte Reihe, solange sie passt, sonst die scrollbare — damit steht nirgends eine Zahl, ab wann es kippt. **Und der Grund, warum die Mediathekenauswahl beim Nutzer nie griff:** Plex. Dessen Hubs sagten nicht, aus welcher Mediathek sie stammen, also warf der Filter jede Reihe weg und fiel auf „alles" zurück — ein Schalter, der nichts tut. `PlexHub` liest jetzt `librarySectionID`, als Zahl oder als Zeichenkette, je nachdem was der Server liefert. Vier Tests dazu. Das ist **kein** Wiederaufnehmen der Plex-Arbeit: es repariert eine Funktion, die dort gelogen hat. Neues kommt weiterhin nicht dazu. iOS und tvOS bauen, 476 Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
145 lines
5.6 KiB
Swift
145 lines
5.6 KiB
Swift
import Foundation
|
|
|
|
/// Eine Mediathek auf dem Server.
|
|
public struct PlexLibrary: Sendable, Equatable, Decodable, Identifiable {
|
|
public enum Kind: String, Sendable, Codable {
|
|
case movie, show, artist, photo
|
|
/// Live-TV und Aufnahmen.
|
|
case livetv
|
|
}
|
|
|
|
/// Die Nummer des Abschnitts, als Zeichenkette. Steckt in fast jedem
|
|
/// weiteren Pfad.
|
|
public let key: String
|
|
public let title: String
|
|
public let type: String
|
|
public let uuid: String?
|
|
public let art: String?
|
|
public let thumb: String?
|
|
public let updatedAt: Int?
|
|
|
|
public var id: String { key }
|
|
public var kind: Kind? { Kind(rawValue: type) }
|
|
|
|
/// Ob Flixy diese Mediathek darstellen kann. Musik und Fotos sind laut
|
|
/// Plan nicht vorgesehen — sie erscheinen deshalb gar nicht erst in der
|
|
/// Seitenleiste, statt dort auf einen leeren Bildschirm zu führen.
|
|
public var isSupported: Bool {
|
|
kind == .movie || kind == .show
|
|
}
|
|
|
|
public init(
|
|
key: String,
|
|
title: String,
|
|
type: String,
|
|
uuid: String? = nil,
|
|
art: String? = nil,
|
|
thumb: String? = nil,
|
|
updatedAt: Int? = nil
|
|
) {
|
|
self.key = key
|
|
self.title = title
|
|
self.type = type
|
|
self.uuid = uuid
|
|
self.art = art
|
|
self.thumb = thumb
|
|
self.updatedAt = updatedAt
|
|
}
|
|
}
|
|
|
|
/// Eine Reihe auf der Startseite.
|
|
///
|
|
/// Plex nennt sie „Hub" und liefert sie fertig zusammengestellt — Weiterschauen,
|
|
/// Zuletzt hinzugefügt, Sammlungen. Die App muss diese Reihen also nicht selbst
|
|
/// bauen, sondern nur darstellen.
|
|
public struct PlexHub: Sendable, Equatable, Decodable, Identifiable {
|
|
/// Stabiler Bezeichner wie `home.continue` oder `movie.recentlyadded`.
|
|
/// Danach richtet sich die Sortierung in den Einstellungen.
|
|
public let hubIdentifier: String
|
|
public let title: String
|
|
public let type: String?
|
|
public let key: String?
|
|
public let size: Int?
|
|
/// Ob es mehr Einträge gibt, als die Reihe zeigt — Grundlage für ein
|
|
/// „Alle anzeigen" am Ende.
|
|
public let more: Bool
|
|
/// Aus welcher Mediathek diese Reihe stammt.
|
|
///
|
|
/// `nil` bei den übergreifenden Reihen — „Fortsetzen" geht quer durch
|
|
/// alle, und Plex nennt dort keine Nummer.
|
|
///
|
|
/// **Warum es überhaupt gelesen wird.** Ohne diese Angabe kann die App
|
|
/// eine Reihe keiner Mediathek zuordnen, und die Auswahl „welche
|
|
/// Mediatheken speisen den Aufmacher" greift ins Leere: sie wirft dann
|
|
/// jede Reihe weg, fällt auf „alles" zurück und sieht aus wie ein
|
|
/// Schalter, der nichts tut. Genau so war es.
|
|
public let librarySectionID: String?
|
|
public let items: [PlexItem]
|
|
|
|
public var id: String { hubIdentifier }
|
|
|
|
/// Ob diese Reihe Angefangenes zeigt.
|
|
///
|
|
/// Entscheidet über die Form der Kacheln: Weiterschauen lebt vom
|
|
/// Szenenbild — man erkennt die Stelle wieder, an der man aufgehört hat.
|
|
/// Überall sonst ist das Plakat richtig, denn danach sucht man einen Film
|
|
/// aus.
|
|
///
|
|
/// Geprüft wird die Kennung, nicht der Titel: der ist übersetzt und hieße
|
|
/// auf Deutsch „Weiterschauen", auf Englisch „Continue Watching".
|
|
public var isContinueWatching: Bool {
|
|
let identifier = hubIdentifier.lowercased()
|
|
// `ondeck` ist die ältere Bezeichnung; Server unterschiedlicher
|
|
// Fassungen liefern die eine oder die andere.
|
|
return identifier.contains("continue") || identifier.contains("ondeck")
|
|
}
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case hubIdentifier, title, type, key, size, more, librarySectionID
|
|
case items = "Metadata"
|
|
}
|
|
|
|
public init(from decoder: any Decoder) throws {
|
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
|
self.title = try values.decode(String.self, forKey: .title)
|
|
// Ältere Serverfassungen liefern keinen `hubIdentifier`. Der Titel ist
|
|
// dann der einzige verfügbare Schlüssel — schlechter, aber besser als
|
|
// eine Antwort, die gar nicht lesbar ist.
|
|
self.hubIdentifier = try values.decodeIfPresent(String.self, forKey: .hubIdentifier) ?? title
|
|
self.type = try values.decodeIfPresent(String.self, forKey: .type)
|
|
self.key = try values.decodeIfPresent(String.self, forKey: .key)
|
|
self.size = try values.decodeIfPresent(Int.self, forKey: .size)
|
|
self.more = try values.decodeIfPresent(Bool.self, forKey: .more) ?? false
|
|
// **Zahl oder Zeichenkette.** Plex liefert die Abschnittsnummer je
|
|
// nach Endpunkt mal als `3`, mal als `"3"`. Nur eines davon zu lesen
|
|
// hiesse, dass es auf manchen Servern geht und auf anderen nicht —
|
|
// und zwar lautlos, denn eine fehlende Nummer ist ein gültiger Fall.
|
|
if let zahl = try? values.decodeIfPresent(Int.self, forKey: .librarySectionID) {
|
|
self.librarySectionID = String(zahl)
|
|
} else {
|
|
self.librarySectionID = try values.decodeIfPresent(String.self, forKey: .librarySectionID)
|
|
}
|
|
self.items = try values.decodeIfPresent([PlexItem].self, forKey: .items) ?? []
|
|
}
|
|
|
|
public init(
|
|
hubIdentifier: String,
|
|
title: String,
|
|
type: String? = nil,
|
|
key: String? = nil,
|
|
size: Int? = nil,
|
|
more: Bool = false,
|
|
librarySectionID: String? = nil,
|
|
items: [PlexItem] = []
|
|
) {
|
|
self.hubIdentifier = hubIdentifier
|
|
self.librarySectionID = librarySectionID
|
|
self.title = title
|
|
self.type = type
|
|
self.key = key
|
|
self.size = size
|
|
self.more = more
|
|
self.items = items
|
|
}
|
|
}
|