macOS: die App läuft jetzt auf dem Mac
Und dein Argument dafür war das beste: auf dem Mac kann ich selbst hinsehen. Kein Simulator-Tunnel, der mitten im Prüfen abstürzt — `screencapture` auf das Fenster der App, und zwar nur auf ihres, nicht auf den Bildschirm. **Ein eigenes Ziel, nicht ein gemeinsames.** Die Info.plist der iOS-App ist voller iOS-Angaben: Startbildschirm, erlaubte Ausrichtungen, Live Activities. Die alle bedingt zu machen hieße, dass jede spätere Änderung an der iOS-Seite über eine Stelle geht, die auch den Mac betrifft. Zwei Ziele sind ein paar Zeilen mehr und eine Sorge weniger. **Was den Unterschied ausmacht, steht an einer Stelle.** Ein neuer `PlatformShims` in FlixyDesign übersetzt, was auf beiden Seiten verschieden heißt: schmale Titelzeile, halbhohes Blatt, Eingabefelder ohne Großschreibung, die Plätze in der Werkzeugleiste, die Zwischenablage. Zwanzig Ansichten benutzen ihn, statt zwanzigmal `#if os(iOS)` zu tragen — sonst wandert die Fallunterscheidung in jede neue Datei und wird beim übernächsten Bildschirm vergessen. Wirklich verschieden sind nur fünf Stellen: die Bildfläche (`UIView` gegen `NSView`), der AirPlay-Knopf, die Ausrichtungssperre (auf dem Mac gibt es nichts zu sperren), der App-Delegat für Hintergrund-Downloads und das Anmeldefenster, das ein anderes Fenster als Anker braucht. **Das Karussell blättert jetzt mit einem einrastenden Streifen** statt mit `TabView(.page)` — den Seitenstil gibt es auf dem Mac nicht. Das ist nebenbei die bessere Lösung: das verschachtelte TabView hatte die Eigenschaft, beim Wechsel kurz zwei Seiten gleichzeitig zu zeigen. **Was auf dem Mac fehlt und warum:** VLC. Das Binärpaket ist auf iOS festgelegt; sein xcframework enthält zwar Mac-Teile, aber das ist eine eigene Runde. Solange fällt die Wiedergabe dort auf den Systemplayer zurück — MKV und DTS gehen also noch nicht. Live Activities gibt es auf dem Mac nicht, Bild-in- Bild startet von Hand statt beim Verlassen der App. Gesehen: die App startet, der Anmeldebildschirm steht, beide Wege sind da. Angemeldet habe ich mich auf dem Mac **nicht** — das ginge nur, indem ich einen Token in deinen Schlüsselbund schreibe, und dann fragt macOS dich beim ersten Zugriff nach dem Passwort. Das machst du mit einem Klick selbst. iOS baut unverändert, 263 Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
21c5a8b46b
commit
d54954b9c2
@@ -4,14 +4,19 @@ import MediaKit
|
||||
|
||||
/// Der Aufmacher oben auf der Startseite — mehrere Titel, die sich abwechseln.
|
||||
///
|
||||
/// Ein einzelnes Bild sagt jedes Mal dasselbe. Hier wandert der Aufmacher
|
||||
/// alle paar Sekunden weiter und lässt sich auch von Hand durchblättern; die
|
||||
/// Punkte darunter zeigen, wie viele es sind und wo man gerade ist.
|
||||
/// Ein einzelnes Bild sagt jedes Mal dasselbe. Hier wandert der Aufmacher alle
|
||||
/// paar Sekunden weiter und lässt sich auch von Hand durchblättern; die Punkte
|
||||
/// darunter zeigen, wie viele es sind und wo man gerade ist.
|
||||
///
|
||||
/// Das Weiterwandern hält an, sobald jemand selbst blättert — der Zeitgeber
|
||||
/// hängt an der aktuellen Seite und fängt bei jedem Wechsel von vorn an.
|
||||
/// Sonst rutschte der Aufmacher genau in dem Moment weiter, in dem man sich
|
||||
/// einen Titel ansieht.
|
||||
/// Das Weiterwandern hört auf, sobald jemand die Fläche berührt. Wer hier die
|
||||
/// Hand hat, fährt selbst — sonst rutscht der Aufmacher genau in der Sekunde
|
||||
/// weiter, in der man nach „Abspielen" greift, und man startet den falschen
|
||||
/// Film. Das ist mir beim Ausprobieren selbst passiert.
|
||||
///
|
||||
/// Geblättert wird mit einem einrastenden Streifen, nicht mit `TabView`: den
|
||||
/// Seitenstil gibt es auf dem Mac nicht, und ein TabView im TabView hatte
|
||||
/// nebenbei die Eigenschaft, beim Wechsel kurz zwei Seiten gleichzeitig zu
|
||||
/// zeigen.
|
||||
struct HeroCarousel: View {
|
||||
let items: [MediaItem]
|
||||
|
||||
@@ -21,47 +26,48 @@ struct HeroCarousel: View {
|
||||
/// Sekunden ist der Satz zu Ende, bevor man ihn gelesen hat.
|
||||
private static let dwell: Duration = .seconds(7)
|
||||
|
||||
@State private var index = 0
|
||||
@State private var shownID: String?
|
||||
/// Ob noch von selbst weitergewandert wird.
|
||||
///
|
||||
/// Sobald jemand die Fläche berührt, hört es auf — und zwar für gut. Wer
|
||||
/// hier die Hand hat, fährt selbst. Andernfalls rutscht der Aufmacher
|
||||
/// genau in der Sekunde weiter, in der man nach „Abspielen" greift, und
|
||||
/// man startet den falschen Film. Das ist mir beim Ausprobieren selbst
|
||||
/// passiert.
|
||||
@State private var isAdvancing = true
|
||||
/// Bewegung, die von selbst losgeht, ist für manche Menschen unangenehm
|
||||
/// bis unbrauchbar. Wer sie abgestellt hat, blättert von Hand.
|
||||
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
||||
@Environment(\.flixyAccent) private var accent
|
||||
|
||||
private var currentIndex: Int {
|
||||
items.firstIndex { $0.id == shownID } ?? 0
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: FlixySpacing.regular) {
|
||||
TabView(selection: $index) {
|
||||
ForEach(Array(items.enumerated()), id: \.element.id) { position, item in
|
||||
HeroSection(item: item)
|
||||
.tag(position)
|
||||
ScrollView(.horizontal) {
|
||||
LazyHStack(spacing: 0) {
|
||||
ForEach(items) { item in
|
||||
HeroSection(item: item)
|
||||
.containerRelativeFrame(.horizontal)
|
||||
.id(item.id)
|
||||
}
|
||||
}
|
||||
.scrollTargetLayout()
|
||||
}
|
||||
.tabViewStyle(.page(indexDisplayMode: .never))
|
||||
// Die eigene Anzeige des TabView läge über dem Bild und damit über
|
||||
// den Knöpfen. Die Höhe kommt aus dem Seitenverhältnis, weil ein
|
||||
// TabView sonst nimmt, was es kriegen kann.
|
||||
.scrollTargetBehavior(.paging)
|
||||
.scrollPosition(id: $shownID)
|
||||
.scrollIndicators(.hidden)
|
||||
.aspectRatio(FlixyAspectRatio.hero, contentMode: .fit)
|
||||
// Gleichzeitig und ohne Mindestweg: fängt jede Berührung ab, ohne
|
||||
// dem Blättern des TabView oder den Knöpfen darin etwas
|
||||
// wegzunehmen.
|
||||
// Jede Berührung hält das Weiterwandern an — auch die, die nur ein
|
||||
// Antippen wird. Gleichzeitig und ohne Mindestweg, damit weder dem
|
||||
// Blättern noch den Knöpfen etwas weggenommen wird.
|
||||
.simultaneousGesture(
|
||||
DragGesture(minimumDistance: 0).onChanged { _ in isAdvancing = false }
|
||||
)
|
||||
|
||||
if items.count > 1 { indicator }
|
||||
}
|
||||
.task(id: index) { await advance() }
|
||||
.onChange(of: items.map(\.id)) { _, ids in
|
||||
// Nach dem Aktualisieren kann die Liste kürzer sein als die
|
||||
// Stelle, an der man gerade war.
|
||||
if index >= ids.count { index = 0 }
|
||||
.task(id: shownID) { await advance() }
|
||||
.onChange(of: items.map(\.id), initial: true) { _, ids in
|
||||
// Beim ersten Zeichnen und nach jedem Aktualisieren: steht die
|
||||
// gezeigte Kennung nicht mehr in der Liste, beginnt es von vorn.
|
||||
if shownID == nil || !ids.contains(shownID ?? "") { shownID = ids.first }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,20 +75,22 @@ struct HeroCarousel: View {
|
||||
HStack(spacing: FlixySpacing.snug) {
|
||||
ForEach(items.indices, id: \.self) { position in
|
||||
Capsule()
|
||||
.fill(position == index ? accent : Color.flixyTextMuted.opacity(0.4))
|
||||
.frame(width: position == index ? 18 : 6, height: 6)
|
||||
.fill(position == currentIndex ? accent : Color.flixyTextMuted.opacity(0.4))
|
||||
.frame(width: position == currentIndex ? 18 : 6, height: 6)
|
||||
}
|
||||
}
|
||||
.animation(.snappy(duration: 0.25), value: index)
|
||||
.animation(.snappy(duration: 0.25), value: currentIndex)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
|
||||
private func advance() async {
|
||||
guard items.count > 1, !reduceMotion, isAdvancing else { return }
|
||||
guard items.count > 1, !reduceMotion, isAdvancing, shownID != nil else { return }
|
||||
try? await Task.sleep(for: Self.dwell)
|
||||
guard !Task.isCancelled else { return }
|
||||
guard !Task.isCancelled, isAdvancing else { return }
|
||||
|
||||
let next = (currentIndex + 1) % items.count
|
||||
withAnimation(.easeInOut(duration: 0.5)) {
|
||||
index = (index + 1) % items.count
|
||||
shownID = items[next].id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,14 +41,12 @@ struct HomeView: View {
|
||||
// über jeden Bildschirm. Darunter liegt der Aufmacher, und der sagt
|
||||
// ohnehin mehr.
|
||||
.navigationTitle("")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.flixyInlineTitle()
|
||||
// Gar kein Hintergrund statt eines weichgezeichneten: die Knöpfe
|
||||
// bringen ihre eigene Glasfläche mit und bleiben auch über dem Bild
|
||||
// lesbar. `.ultraThinMaterial` verschleierte dagegen den halben
|
||||
// Aufmacher.
|
||||
.toolbarBackground(.hidden, for: .navigationBar)
|
||||
// Helle Symbole, weil darunter ein Bild liegt und kein heller Grund.
|
||||
.toolbarColorScheme(.dark, for: .navigationBar)
|
||||
.flixyClearToolbarBackground()
|
||||
.task(id: session.serverName) { await load() }
|
||||
.refreshable { await load() }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user