diff --git a/Flixy/Resources/Localizable.xcstrings b/Flixy/Resources/Localizable.xcstrings index 04d6d4e..f80dfa5 100644 --- a/Flixy/Resources/Localizable.xcstrings +++ b/Flixy/Resources/Localizable.xcstrings @@ -7398,6 +7398,91 @@ } } } + }, + "topShelf.noFile": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Noch nichts abgelegt." + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Nothing stored yet." + } + } + } + }, + "topShelf.noGroup": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Keine App-Gruppe — das Regal kann nichts lesen." + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "No app group — the shelf cannot read anything." + } + } + } + }, + "topShelf.title": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Regal über der App" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Top Shelf" + } + } + } + }, + "topShelf.unreadable": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Datei liegt da, lässt sich aber nicht lesen." + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "File is there but cannot be read." + } + } + } + }, + "topShelf.written %lld %lld %@": { + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "%lld Reihen, %lld Einträge, zuletzt %@" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "%lld rows, %lld entries, last %@" + } + } + } } }, "version": "1.1" diff --git a/Flixy/Session/TopShelfWriter.swift b/Flixy/Session/TopShelfWriter.swift index 6290ce1..2c801d2 100644 --- a/Flixy/Session/TopShelfWriter.swift +++ b/Flixy/Session/TopShelfWriter.swift @@ -86,5 +86,27 @@ enum TopShelfWriter { // wenn sie gefragt wird. TVTopShelfContentProvider.topShelfContentDidChange() } + + /// Was zuletzt abgelegt wurde — in einem Satz, zum Hinsehen. + /// + /// **Weil man dem Regal sonst nichts ansieht.** Bleibt es leer, gibt es + /// genau drei Möglichkeiten: die App-Gruppe fehlt, die Datei ist nicht da, + /// oder sie ist da und das System fragt trotzdem nicht. Von aussen sehen + /// alle drei gleich aus — nämlich nach dem grossen Plakat aus dem + /// Asset-Katalog. Diese Zeile unterscheidet sie. + static var status: String { + guard let ort = TopShelfStore.location(group: group) else { + return String(localized: "topShelf.noGroup") + } + guard let abzug = TopShelfStore.read(group: group) else { + let daIst = FileManager.default.fileExists(atPath: ort.path) + return daIst + ? String(localized: "topShelf.unreadable") + : String(localized: "topShelf.noFile") + } + let einträge = abzug.sections.reduce(0) { $0 + $1.entries.count } + let zeit = abzug.savedAt.formatted(date: .omitted, time: .shortened) + return String(localized: "topShelf.written \(abzug.sections.count) \(einträge) \(zeit)") + } } #endif diff --git a/FlixyTV/Views/Home/TVHomeView.swift b/FlixyTV/Views/Home/TVHomeView.swift index 257ba97..bda7e2d 100644 --- a/FlixyTV/Views/Home/TVHomeView.swift +++ b/FlixyTV/Views/Home/TVHomeView.swift @@ -38,9 +38,6 @@ struct TVHomeView: View { .navigationDestination(for: MediaRoute.self) { route in TVDetailView(route: route) } - .navigationDestination(for: LibraryRoute.self) { route in - TVLibraryView(libraryID: route.id) - } } .task(id: session.serverName) { await feed.load(from: session.backend) } .onChange(of: deepLinkTarget) { _, neu in @@ -96,10 +93,6 @@ struct TVHomeView: View { onFocus: { aufmacher = $0 } ) } - - if !session.libraries.isEmpty { - mediatheken - } } .padding(.bottom, FlixySpacing.screenMargin) } @@ -108,40 +101,34 @@ struct TVHomeView: View { // Aufmacher muss bis an die Bildschirmkante reichen; ein Bild, das // achtzig Punkte davor aufhört, sieht nach Fehler aus. .ignoresSafeArea() + // Als Überlagerung und nicht als letzte Zeile der Liste: dorthin + // müsste man erst an allen Reihen vorbeiscrollen, und wer nachsieht, + // warum das Regal leer ist, soll es sofort sehen. + .overlay(alignment: .bottom) { regalStatus } } - /// Die Mediatheken als eigene Reihe. + /// Was das Regal über der App zuletzt bekommen hat. /// - /// Unten und nicht oben: wer den Fernseher einschaltet, will meistens - /// weiterschauen, nicht stöbern. Und als Reihe statt als Registerkarten, - /// weil eine Reihe am Fokus hängt und Registerkarten eine zweite Bedienart - /// wären. - private var mediatheken: some View { - VStack(alignment: .leading, spacing: FlixySpacing.regular) { - Text("library.title") - .font(.flixyRowHeader) - .foregroundStyle(Color.flixyTextPrimary) - .padding(.horizontal, FlixySpacing.screenMargin) - - ScrollView(.horizontal) { - LazyHStack(spacing: FlixySpacing.regular) { - ForEach(session.libraries, id: \.id) { mediathek in - Button { - pfad.append(LibraryRoute(id: mediathek.id)) - } label: { - Text(mediathek.title) - .font(.flixyRowHeader) - .frame(width: 360, height: 180) - } - .buttonStyle(.card) - } - } - .padding(.horizontal, FlixySpacing.screenMargin) - .padding(.vertical, FlixySpacing.regular) - } - .scrollClipDisabled() - .scrollIndicators(.never) + /// **Nur im Entwicklerbau.** Es gehört nicht in die fertige App — aber + /// solange das Regal leer bleibt, sieht man von aussen nicht, woran es + /// liegt: fehlende App-Gruppe, fehlende Datei und ein System, das nicht + /// fragt, sehen alle gleich aus. Diese Zeile unterscheidet sie. + @ViewBuilder + private var regalStatus: some View { + #if DEBUG + VStack(alignment: .leading, spacing: FlixySpacing.tight) { + Text("topShelf.title") + .font(.flixyCaption) + .foregroundStyle(Color.flixyTextMuted) + Text(TopShelfWriter.status) + .font(.flixyCaption) + .foregroundStyle(Color.flixyTextSecondary) } + .padding(.horizontal, FlixySpacing.screenMargin) + .padding(.bottom, FlixySpacing.regular) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color.black.opacity(0.6)) + #endif } // MARK: - Was ein Druck auslöst diff --git a/FlixyTV/Views/TVShell.swift b/FlixyTV/Views/TVShell.swift index 9e21ec9..9b57e51 100644 --- a/FlixyTV/Views/TVShell.swift +++ b/FlixyTV/Views/TVShell.swift @@ -1,22 +1,21 @@ import SwiftUI import FlixyDesign +import MediaKit -/// Start und Suche nebeneinander. +/// Start, die Mediatheken und die Suche nebeneinander. /// -/// **Ein `TabView`, und erst jetzt.** Bis hierher gab es genau einen -/// Bildschirm, und eine Leiste mit einem einzigen Eintrag wäre Zierrat -/// gewesen. Mit der Suche gibt es zwei Orte, zwischen denen man wechselt — -/// und dafür ist die Leiste oben die Bedienung, die jeder vom Fernseher -/// kennt: sie erscheint, wenn man vom obersten Element aus nach oben wischt, -/// und verschwindet wieder. +/// **Die Mediatheken als Reiter, nicht als Reihe.** Sie standen unten auf der +/// Startseite, und dorthin musste man erst scrollen — an allen Vorschlägen +/// vorbei, obwohl „ich will in die Filme" genau der Fall ist, in dem einem +/// die Vorschläge egal sind. Oben sind sie einen Wisch entfernt. /// -/// **Kein Reiter für die Mediatheken.** Die stehen als Reihe auf der -/// Startseite. Ein dritter Reiter dafür verspräche einen eigenen Bereich, und -/// dahinter läge dasselbe Raster, das eine Reihe tiefer ohnehin erreichbar -/// ist. +/// Damit fällt die Reihe unten weg: zwei Wege zum selben Raster sind auf +/// einem Fernseher kein Komfort, sondern eine Frage, welcher der richtige ist. struct TVShell: View { @Binding var deepLinkTarget: MediaRoute? + @Environment(FlixySession.self) private var session + /// Welcher Reiter offen ist. /// /// Ausdrücklich gehalten, nicht dem System überlassen: ein Link aus dem @@ -24,7 +23,11 @@ struct TVShell: View { /// Suche offen, passierte sichtbar nichts. @State private var reiter = Reiter.start - private enum Reiter: Hashable { case start, suche } + private enum Reiter: Hashable { + case start + case mediathek(String) + case suche + } var body: some View { TabView(selection: $reiter) { @@ -34,9 +37,23 @@ struct TVShell: View { Text("sidebar.home") } + ForEach(session.libraries, id: \.id) { mediathek in + Tab(value: Reiter.mediathek(mediathek.id)) { + // Jeder Reiter bekommt seinen eigenen Stapel. Ein + // gemeinsamer hiesse, dass ein Wechsel des Reiters die + // Detailseite des vorigen mitnimmt. + NavigationStack { + TVLibraryView(libraryID: mediathek.id) + } + } label: { + Text(mediathek.title) + } + } + // **`role: .search` und nicht ein gewöhnlicher Reiter.** tvOS - // stellt ihn dadurch ans Ende der Leiste und gibt ihm das - // Lupensymbol — dieselbe Stelle wie in Apples eigenen Apps. + // setzt ihn dadurch dorthin, wo er in jeder anderen App auch + // steht, und gibt ihm die Lupe. Die Stelle bestimmt also Apple, + // nicht diese Datei. Tab(value: Reiter.suche, role: .search) { TVSearchView() }