Deine Einstellungs-Bilder sind eine lange Liste. Ich habe sie durchgegangen
und getrennt: was etwas steuert, ist gebaut; was eine Einstellung für eine
Sache wäre, die es nicht gibt, ist nicht gebaut. Ein Schalter, der nichts
bewirkt, ist schlimmer als kein Schalter.
**Die Einstellungen sind jetzt eine Übersicht.** Vorher stand alles
untereinander auf einem Bildschirm — Serverangaben, Qualität, Wiedergabe, drei
Verweise, Downloads, Version, Abmelden. Das war beim Bauen bequem und beim
Suchen anstrengend. Jetzt führt jede Zeile in einen Bildschirm, der zu einer
Frage gehört: Server, Erscheinungsbild, Startseite, Wiedergabe, Ton &
Untertitel, Downloads, Diagnose.
**Neu, und jedes davon greift irgendwo:**
- Zurück- und Vorspulen getrennt einstellbar. Die Knöpfe im Player und die
Doppeltipp-Flächen tragen danach auch das passende Symbol.
- Startgeschwindigkeit — die Engine kann es längst, gefragt wurde nie.
- „Im Querformat sperren" abschaltbar. Für die allermeisten Filme richtig, für
den, der im Bett auf der Seite liegt, das Gegenteil.
- „Gewählte Qualität merken": die Stufe gilt dann auch für den nächsten Titel.
Aufbewahrt wird eine **Zahl**, keine Sprosse — welche Sprossen es gibt, hängt
von der Quelle ab, und eine 720p-Datei kennt keine Zwanzig-Megabit-Stufe.
Dann kommt die nächstgelegene, nicht „automatisch".
- „Bitrate im Qualitätsmenü zeigen".
- Gleichzeitige Downloads. Der Wert gab es schon in der Engine, nur ohne
Bedienung und ohne dass er einen Neustart überlebte.
- Aufmacher: wie viele Titel, und ob sie aus Angefangenem oder nur aus
Neuzugängen kommen.
- „Weiterschauen und Als Nächstes zusammenlegen" und „Gesehenes aus
Neuzugängen ausblenden" — beides in der Ansicht, nicht im Server: das sind
Fragen der Darstellung, und so gelten sie für Plex und Jellyfin gleich.
**Verkleinert herunterladen geht jetzt auch bei Jellyfin.** Da hatte ich zu
früh aufgegeben: ich hatte nur nach der Adresse zum *Abspielen* gesucht, und
die ist eine Abspielliste mit hunderten Schnipseln — als Datei unbrauchbar.
`/Videos/{id}/stream.mp4` gibt dieselbe Umrechnung als eine Datei heraus. Die
Größe bleibt eine Schätzung; eine Umrechnung, die erst beim Abruf beginnt,
kennt ihre eigene Länge nicht.
**Die Kontozeile ist jetzt eine Karte, die nach oben aufklappt**, kein
Systemmenü mehr. Ein Menü legt sich als eigene Ebene über alles und springt
dorthin, wo Platz ist; die Karte bleibt, wo sie ist, und lässt die Zeile mit
dem Namen stehen — man sieht also weiter, um wessen Konto es geht.
**Nicht gebaut, mit Grund:** Trailer im Karussell, Poster- und Sammlungsstil,
Untertitel-Aussehen, sekundäre Untertitel, lokale Intros, Modular Home,
App-Symbol. Für all das fehlt das, was die Einstellung steuern würde — es gibt
in Flixy weder Trailer noch Sammlungen, und der Player zeichnet Untertitel
nicht selbst.
263 Tests grün. **Nicht angesehen**: der Simulator-Tunnel ist weiterhin
abgestürzt und ließ sich nicht wieder aufbauen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
77 lines
2.8 KiB
Swift
77 lines
2.8 KiB
Swift
import SwiftUI
|
|
import UIKit
|
|
|
|
/// Bestimmt, in welche Richtungen sich die App drehen darf.
|
|
///
|
|
/// In reinem SwiftUI gibt es dafür keinen Weg: `requestGeometryUpdate` dreht
|
|
/// zwar, aber das System dreht sofort zurück, wenn der Bildschirm es nicht
|
|
/// auch erlaubt. Erlaubt wird es über
|
|
/// `application(_:supportedInterfaceOrientationsFor:)` — und das gibt es nur
|
|
/// am klassischen App-Delegaten. Diese Klasse ist die Brücke dazwischen.
|
|
@MainActor
|
|
final class OrientationLock {
|
|
static let shared = OrientationLock()
|
|
|
|
private(set) var mask: UIInterfaceOrientationMask = .portrait
|
|
|
|
private init() {
|
|
// Auf dem iPad bleibt alles erlaubt: dort ist das Querformat beim
|
|
// Blättern der Normalfall, nicht die Ausnahme.
|
|
if UIDevice.current.userInterfaceIdiom == .pad {
|
|
mask = .all
|
|
}
|
|
}
|
|
|
|
/// Dreht ins Querformat und hält es dort.
|
|
func lockLandscape() {
|
|
guard UIDevice.current.userInterfaceIdiom != .pad else { return }
|
|
apply(.landscape, rotatingTo: .landscape)
|
|
}
|
|
|
|
/// Gibt die Ausrichtung wieder frei und dreht zurück ins Hochformat.
|
|
func releaseLandscape() {
|
|
guard UIDevice.current.userInterfaceIdiom != .pad else { return }
|
|
apply(.portrait, rotatingTo: .portrait)
|
|
}
|
|
|
|
private func apply(
|
|
_ newMask: UIInterfaceOrientationMask,
|
|
rotatingTo target: UIInterfaceOrientationMask
|
|
) {
|
|
mask = newMask
|
|
|
|
guard let scene = UIApplication.shared.connectedScenes
|
|
.compactMap({ $0 as? UIWindowScene })
|
|
.first(where: { $0.activationState == .foregroundActive })
|
|
else { return }
|
|
|
|
// Erst die neue Erlaubnis bekanntgeben, dann drehen. In der anderen
|
|
// Reihenfolge lehnt das System die Drehung ab, weil die alte Maske
|
|
// noch gilt — und das sah aus wie „dreht sich nicht richtig".
|
|
scene.keyWindow?.rootViewController?.setNeedsUpdateOfSupportedInterfaceOrientations()
|
|
scene.requestGeometryUpdate(.iOS(interfaceOrientations: target))
|
|
}
|
|
}
|
|
|
|
/// Dreht beim Erscheinen ins Querformat und beim Verschwinden zurück.
|
|
struct LandscapeWhileVisible: ViewModifier {
|
|
/// Ob überhaupt gesperrt wird.
|
|
///
|
|
/// Für die allermeisten Filme ist das Querformat richtig — und für den,
|
|
/// der im Bett auf der Seite liegt, das Gegenteil davon.
|
|
let isEnabled: Bool
|
|
|
|
func body(content: Content) -> some View {
|
|
content
|
|
.onAppear { if isEnabled { OrientationLock.shared.lockLandscape() } }
|
|
.onDisappear { OrientationLock.shared.releaseLandscape() }
|
|
}
|
|
}
|
|
|
|
extension View {
|
|
/// Für den Vollbildplayer: Film ist Querformat.
|
|
func landscapeWhileVisible(_ isEnabled: Bool = true) -> some View {
|
|
modifier(LandscapeWhileVisible(isEnabled: isEnabled))
|
|
}
|
|
}
|