From d54954b9c2707ff2ae61b9592b08c83ac3966177 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Sun, 13 Sep 2026 15:45:13 +0200 Subject: [PATCH] =?UTF-8?q?macOS:=20die=20App=20l=C3=A4uft=20jetzt=20auf?= =?UTF-8?q?=20dem=20Mac?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- Flixy/FlixyApp.swift | 13 +- Flixy/Info-macOS.plist | 44 +++++++ Flixy/Session/OrientationLock.swift | 13 ++ Flixy/Session/PlexWebAuthenticator.swift | 16 ++- Flixy/Views/Detail/MediaDetailView.swift | 4 +- Flixy/Views/Downloads/DownloadsView.swift | 2 +- Flixy/Views/Home/HeroCarousel.swift | 80 ++++++------ Flixy/Views/Home/HomeView.swift | 6 +- Flixy/Views/Library/LibraryFilterSheet.swift | 4 +- Flixy/Views/Library/LibraryHubView.swift | 2 +- Flixy/Views/Library/LibraryView.swift | 6 +- .../Views/LiveTV/ChannelSelectionSheet.swift | 4 +- Flixy/Views/LiveTV/LiveTVView.swift | 20 +-- Flixy/Views/LiveTV/ProgramGuide.swift | 2 +- .../Views/Placeholder/PlaceholderScreen.swift | 4 +- Flixy/Views/Player/AirPlayButton.swift | 23 ++++ Flixy/Views/Player/PlayerModel.swift | 14 ++- Flixy/Views/Player/PlayerView.swift | 4 + Flixy/Views/Player/QualitySheet.swift | 4 +- Flixy/Views/Player/TrackPickerSheet.swift | 4 +- Flixy/Views/Player/VideoSurface.swift | 115 +++++++++++++++--- Flixy/Views/Search/SearchView.swift | 4 +- Flixy/Views/Settings/AppearanceView.swift | 5 +- Flixy/Views/Settings/DiagnosticsView.swift | 6 +- .../Views/Settings/DownloadSettingsView.swift | 2 +- Flixy/Views/Settings/HomeLayoutView.swift | 6 +- Flixy/Views/Settings/LanguagesView.swift | 2 +- .../Views/Settings/PlaybackSettingsView.swift | 2 +- Flixy/Views/Settings/ServerSettingsView.swift | 2 +- Flixy/Views/Settings/SettingsView.swift | 2 +- Flixy/Views/SignIn/JellyfinSignInView.swift | 9 +- .../Sources/FlixyDesign/PlatformShims.swift | 110 +++++++++++++++++ project.yml | 57 +++++++++ 33 files changed, 480 insertions(+), 111 deletions(-) create mode 100644 Flixy/Info-macOS.plist create mode 100644 Packages/FlixyKit/Sources/FlixyDesign/PlatformShims.swift diff --git a/Flixy/FlixyApp.swift b/Flixy/FlixyApp.swift index 062350f..95a8b92 100644 --- a/Flixy/FlixyApp.swift +++ b/Flixy/FlixyApp.swift @@ -1,5 +1,8 @@ import SwiftData import SwiftUI +#if os(iOS) +import UIKit +#endif import FlixyDesign import FlixyDownloads import FlixyPlayback @@ -9,10 +12,14 @@ import PlexKit @main struct FlixyApp: App { + #if os(iOS) /// Nimmt das Signal entgegen, mit dem iOS die App zum Abschluss von /// Hintergrund-Downloads aufweckt. Dafür gibt es in SwiftUI keinen - /// eigenen Weg — nur den klassischen App-Delegaten. + /// eigenen Weg — nur den klassischen App-Delegaten. Auf dem Mac wird eine + /// App nicht aus dem Speicher geworfen, während sie lädt; dort gibt es + /// weder das Signal noch den Bedarf. @UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate + #endif /// Leben hier, nicht in einer View: sie gelten für die gesamte App, und /// ein Neuaufbau der Wurzelansicht darf weder die Anmeldung noch laufende @@ -82,7 +89,9 @@ struct FlixyApp: App { // Absicherung für SwiftUI-Vorschauen, die sie nicht lesen. .preferredColorScheme(.dark) .task { + #if os(iOS) appDelegate.downloads = downloads + #endif await downloads.restore() // Beim Start einmal prüfen: während die App weg war, // können Folgen gesehen oder gelöscht worden sein. @@ -96,6 +105,7 @@ struct FlixyApp: App { } } +#if os(iOS) /// Der klassische App-Delegat, für das eine, was SwiftUI nicht abdeckt. @MainActor final class AppDelegate: NSObject, UIApplicationDelegate { @@ -140,3 +150,4 @@ final class AppDelegate: NSObject, UIApplicationDelegate { } } } +#endif diff --git a/Flixy/Info-macOS.plist b/Flixy/Info-macOS.plist new file mode 100644 index 0000000..988b4c7 --- /dev/null +++ b/Flixy/Info-macOS.plist @@ -0,0 +1,44 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Flixy + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleURLTypes + + + CFBundleURLName + com.scarriffleservices.flixy.auth + CFBundleURLSchemes + + com.scarriffleservices.flixy + + + + CFBundleVersion + 1 + LSApplicationCategoryType + public.app-category.entertainment + NSAppTransportSecurity + + NSAllowsLocalNetworking + + + NSHumanReadableCopyright + + + diff --git a/Flixy/Session/OrientationLock.swift b/Flixy/Session/OrientationLock.swift index 1150ca8..176bd47 100644 --- a/Flixy/Session/OrientationLock.swift +++ b/Flixy/Session/OrientationLock.swift @@ -1,3 +1,4 @@ +#if os(iOS) import SwiftUI import UIKit @@ -74,3 +75,15 @@ extension View { modifier(LandscapeWhileVisible(isEnabled: isEnabled)) } } +#else +import SwiftUI + +/// Auf dem Mac dreht sich nichts. +/// +/// Ein Fenster hat keine Ausrichtung, die sich sperren ließe — der Nutzer +/// bestimmt seine Größe. Die Stelle bleibt trotzdem bestehen, damit der +/// Player auf beiden Plattformen dieselbe Zeile schreiben kann. +extension View { + func landscapeWhileVisible(_ isEnabled: Bool = true) -> some View { self } +} +#endif diff --git a/Flixy/Session/PlexWebAuthenticator.swift b/Flixy/Session/PlexWebAuthenticator.swift index 3f79c0c..d45c6b3 100644 --- a/Flixy/Session/PlexWebAuthenticator.swift +++ b/Flixy/Session/PlexWebAuthenticator.swift @@ -1,6 +1,10 @@ import AuthenticationServices import Foundation +#if canImport(UIKit) import UIKit +#else +import AppKit +#endif /// Öffnet die Anmeldeseite von Plex in einem Fenster innerhalb der App. /// @@ -94,13 +98,19 @@ extension PlexWebAuthenticator { extension PlexWebAuthenticator: ASWebAuthenticationPresentationContextProviding { func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { - // Das Fenster der App. Über die verbundenen Szenen gesucht, weil eine - // SwiftUI-App keinen festen Verweis darauf hat — und ein frisch - // erzeugtes Fenster erschiene hinter dem sichtbaren. + // Das Fenster der App. Gesucht statt gehalten, weil eine SwiftUI-App + // keinen festen Verweis darauf hat — und ein frisch erzeugtes Fenster + // erschiene hinter dem sichtbaren. + #if canImport(UIKit) let scene = UIApplication.shared.connectedScenes .compactMap { $0 as? UIWindowScene } .first { $0.activationState == .foregroundActive } return scene?.keyWindow ?? ASPresentationAnchor() + #else + return NSApplication.shared.keyWindow + ?? NSApplication.shared.windows.first + ?? ASPresentationAnchor() + #endif } } diff --git a/Flixy/Views/Detail/MediaDetailView.swift b/Flixy/Views/Detail/MediaDetailView.swift index cda7d50..d06a4f3 100644 --- a/Flixy/Views/Detail/MediaDetailView.swift +++ b/Flixy/Views/Detail/MediaDetailView.swift @@ -39,10 +39,10 @@ struct MediaDetailView: View { } .background(Color.flixyCanvas) .navigationTitle(state.value?.title ?? route.title) - .navigationBarTitleDisplayMode(.inline) + .flixyInlineTitle() .toolbar { if let item = state.value { - ToolbarItem(placement: .topBarTrailing) { + ToolbarItem(placement: .flixyTrailing) { ItemActionsMenu(item: item) { await load() } } } diff --git a/Flixy/Views/Downloads/DownloadsView.swift b/Flixy/Views/Downloads/DownloadsView.swift index 997c13d..efaffa0 100644 --- a/Flixy/Views/Downloads/DownloadsView.swift +++ b/Flixy/Views/Downloads/DownloadsView.swift @@ -26,7 +26,7 @@ struct DownloadsView: View { } .background(Color.flixyCanvas) .navigationTitle(Text("sidebar.downloads")) - .navigationBarTitleDisplayMode(.inline) + .flixyInlineTitle() .confirmationDialog( Text("downloads.removeConfirm"), isPresented: Binding( diff --git a/Flixy/Views/Home/HeroCarousel.swift b/Flixy/Views/Home/HeroCarousel.swift index e8fa92f..87fb664 100644 --- a/Flixy/Views/Home/HeroCarousel.swift +++ b/Flixy/Views/Home/HeroCarousel.swift @@ -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 } } } diff --git a/Flixy/Views/Home/HomeView.swift b/Flixy/Views/Home/HomeView.swift index 116472c..25eb0d2 100644 --- a/Flixy/Views/Home/HomeView.swift +++ b/Flixy/Views/Home/HomeView.swift @@ -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() } } diff --git a/Flixy/Views/Library/LibraryFilterSheet.swift b/Flixy/Views/Library/LibraryFilterSheet.swift index a643ada..8466de3 100644 --- a/Flixy/Views/Library/LibraryFilterSheet.swift +++ b/Flixy/Views/Library/LibraryFilterSheet.swift @@ -73,7 +73,7 @@ struct LibraryFilterSheet: View { .scrollContentBackground(.hidden) .background(Color.flixyCanvas) .navigationTitle(Text("filter.title")) - .navigationBarTitleDisplayMode(.inline) + .flixyInlineTitle() .toolbar { ToolbarItem(placement: .cancellationAction) { Button("action.cancel") { dismiss() } @@ -87,7 +87,7 @@ struct LibraryFilterSheet: View { } .task { await loadGenres() } } - .presentationDetents([.medium, .large]) + .flixyResizableSheet() } private func loadGenres() async { diff --git a/Flixy/Views/Library/LibraryHubView.swift b/Flixy/Views/Library/LibraryHubView.swift index 218f3c8..247ba99 100644 --- a/Flixy/Views/Library/LibraryHubView.swift +++ b/Flixy/Views/Library/LibraryHubView.swift @@ -47,7 +47,7 @@ struct LibraryHubView: View { .scrollContentBackground(.hidden) .background(Color.flixyCanvas) .navigationTitle(Text("sidebar.library")) - .navigationBarTitleDisplayMode(.inline) + .flixyInlineTitle() } private func symbol(for kind: MediaLibrary.Kind) -> String { diff --git a/Flixy/Views/Library/LibraryView.swift b/Flixy/Views/Library/LibraryView.swift index 76549ea..cf418a0 100644 --- a/Flixy/Views/Library/LibraryView.swift +++ b/Flixy/Views/Library/LibraryView.swift @@ -51,7 +51,7 @@ struct LibraryView: View { } .background(Color.flixyCanvas) .navigationTitle(library.title) - .navigationBarTitleDisplayMode(.inline) + .flixyInlineTitle() .toolbar { toolbar } .sheet(isPresented: $showsFilters) { LibraryFilterSheet(query: $query, libraryKind: library.kind) @@ -120,7 +120,7 @@ struct LibraryView: View { @ToolbarContentBuilder private var toolbar: some ToolbarContent { - ToolbarItem(placement: .topBarTrailing) { + ToolbarItem(placement: .flixyTrailing) { Button { showsFilters = true } label: { @@ -131,7 +131,7 @@ struct LibraryView: View { .accessibilityLabel(Text("library.filters")) } - ToolbarItem(placement: .topBarTrailing) { + ToolbarItem(placement: .flixyTrailing) { Button { layout = layout == .grid ? .list : .grid } label: { diff --git a/Flixy/Views/LiveTV/ChannelSelectionSheet.swift b/Flixy/Views/LiveTV/ChannelSelectionSheet.swift index e67c9ad..2a5f6a4 100644 --- a/Flixy/Views/LiveTV/ChannelSelectionSheet.swift +++ b/Flixy/Views/LiveTV/ChannelSelectionSheet.swift @@ -28,9 +28,9 @@ struct ChannelSelectionSheet: View { } } .navigationTitle(Text("liveTV.selection")) - .navigationBarTitleDisplayMode(.inline) + .flixyInlineTitle() .toolbar { - ToolbarItem(placement: .topBarLeading) { + ToolbarItem(placement: .flixyLeading) { // „Alle" statt „keine": eine leere Auswahl bedeutet // ohnehin alle, und ein Knopf, der scheinbar nichts tut, // wäre verwirrend. diff --git a/Flixy/Views/LiveTV/LiveTVView.swift b/Flixy/Views/LiveTV/LiveTVView.swift index de11248..1c461c7 100644 --- a/Flixy/Views/LiveTV/LiveTVView.swift +++ b/Flixy/Views/LiveTV/LiveTVView.swift @@ -96,10 +96,10 @@ struct LiveTVView: View { } .background(Color.flixyCanvas) .navigationTitle(Text("sidebar.liveTV")) - .navigationBarTitleDisplayMode(.inline) + .flixyInlineTitle() .toolbar { if case .available = availability, !channels.isEmpty { - ToolbarItem(placement: .topBarTrailing) { + ToolbarItem(placement: .flixyTrailing) { Button { showsSelection = true } label: { @@ -107,7 +107,7 @@ struct LiveTVView: View { } .accessibilityLabel(Text("liveTV.selection")) } - ToolbarItem(placement: .topBarTrailing) { + ToolbarItem(placement: .flixyTrailing) { Picker("liveTV.mode", selection: $showsGuide) { Image(systemName: "calendar.day.timeline.left").tag(true) Image(systemName: "list.bullet").tag(false) @@ -167,9 +167,11 @@ struct LiveTVView: View { .frame(maxHeight: 200) Button { - UIPasteboard.general.string = trace - .map { "\($0.path) → \($0.result)" } - .joined(separator: "\n") + FlixyClipboard.copy( + trace + .map { "\($0.path) → \($0.result)" } + .joined(separator: "\n") + ) } label: { Label("diagnostics.copy", systemImage: "doc.on.doc") .font(.flixyCaption) @@ -386,7 +388,7 @@ private struct TuneFailureSheet: View { } Button { - UIPasteboard.general.string = failure.trace.joined(separator: "\n") + FlixyClipboard.copy(failure.trace.joined(separator: "\n")) } label: { Label("diagnostics.copy", systemImage: "doc.on.doc") .font(.flixyCaption) @@ -398,13 +400,13 @@ private struct TuneFailureSheet: View { } .background(Color.flixyCanvas) .navigationTitle(Text("liveTV.tuneFailed")) - .navigationBarTitleDisplayMode(.inline) + .flixyInlineTitle() .toolbar { ToolbarItem(placement: .confirmationAction) { Button("action.close") { dismiss() } } } } - .presentationDetents([.medium, .large]) + .flixyResizableSheet() } } diff --git a/Flixy/Views/LiveTV/ProgramGuide.swift b/Flixy/Views/LiveTV/ProgramGuide.swift index e25246a..6c6b0b9 100644 --- a/Flixy/Views/LiveTV/ProgramGuide.swift +++ b/Flixy/Views/LiveTV/ProgramGuide.swift @@ -348,6 +348,6 @@ private struct ProgramDetailSheet: View { } } } - .presentationDetents([.medium, .large]) + .flixyResizableSheet() } } diff --git a/Flixy/Views/Placeholder/PlaceholderScreen.swift b/Flixy/Views/Placeholder/PlaceholderScreen.swift index e3b5e37..887962f 100644 --- a/Flixy/Views/Placeholder/PlaceholderScreen.swift +++ b/Flixy/Views/Placeholder/PlaceholderScreen.swift @@ -17,7 +17,7 @@ struct PlaceholderScreen: View { } .background(Color.flixyCanvas) .navigationTitle(destination.title) - .navigationBarTitleDisplayMode(.inline) - .toolbarBackground(Color.flixySurface, for: .navigationBar) + .flixyInlineTitle() + } } diff --git a/Flixy/Views/Player/AirPlayButton.swift b/Flixy/Views/Player/AirPlayButton.swift index 0e596c2..3df55f5 100644 --- a/Flixy/Views/Player/AirPlayButton.swift +++ b/Flixy/Views/Player/AirPlayButton.swift @@ -1,5 +1,10 @@ import AVKit import SwiftUI +#if canImport(UIKit) +import UIKit +#else +import AppKit +#endif /// Der Knopf, der die Empfängerliste öffnet. /// @@ -7,6 +12,7 @@ import SwiftUI /// die das System dafür vorsieht, und sie kennt Geräte, die eine App nie /// aufzählen könnte — Apple TVs, AirPlay-Lautsprecher, HomePods im Verbund. /// Sie nachzubauen hieße, eine unvollständige Liste zu zeigen. +#if canImport(UIKit) struct AirPlayButton: UIViewRepresentable { let tint: Color @@ -25,3 +31,20 @@ struct AirPlayButton: UIViewRepresentable { view.activeTintColor = UIColor(tint) } } +#else +struct AirPlayButton: NSViewRepresentable { + let tint: Color + + func makeNSView(context: Context) -> AVRoutePickerView { + let view = AVRoutePickerView() + view.isRoutePickerButtonBordered = false + view.setRoutePickerButtonColor(.white, for: .normal) + view.setRoutePickerButtonColor(NSColor(tint), for: .active) + return view + } + + func updateNSView(_ view: AVRoutePickerView, context: Context) { + view.setRoutePickerButtonColor(NSColor(tint), for: .active) + } +} +#endif diff --git a/Flixy/Views/Player/PlayerModel.swift b/Flixy/Views/Player/PlayerModel.swift index c5a0371..6dbfa7a 100644 --- a/Flixy/Views/Player/PlayerModel.swift +++ b/Flixy/Views/Player/PlayerModel.swift @@ -4,7 +4,9 @@ import MediaKit import Observation import FlixyDownloads import FlixyPlayback +#if canImport(FlixyVLC) import FlixyVLC +#endif import PlexKit /// Führt einen Abspielwunsch aus. @@ -712,9 +714,12 @@ final class PlayerModel { self?.restoreFromPictureInPicture?() } let controller = AVPictureInPictureController(playerLayer: surface.playerLayer) + #if os(iOS) // Beim Verlassen der App ins Bild-in-Bild wechseln, statt die - // Wiedergabe anzuhalten. + // Wiedergabe anzuhalten. Auf dem Mac verschwindet eine App nicht + // in den Hintergrund — dort startet Bild-in-Bild von Hand. controller?.canStartPictureInPictureAutomaticallyFromInline = true + #endif controller?.delegate = bridge pictureInPictureDelegate = bridge pictureInPicture = controller @@ -730,12 +735,19 @@ final class PlayerModel { } private func makeEngine(for decision: PlaybackDecision) -> any VideoPlayerEngine { + #if canImport(FlixyVLC) switch decision.engine { case .vlc: VLCPlayerEngine() // Auch der Transcode-Weg endet beim Systemplayer: er liefert HLS, und // genau darin wechselt AVPlayer die Bitrate von selbst. case .avPlayer, .none: AVPlayerEngine() } + #else + // Auf dem Mac gibt es VLC (noch) nicht: das Binärpaket ist auf iOS + // festgelegt. Was der Systemplayer nicht öffnen kann, scheitert dort + // mit einer Meldung, statt still schwarz zu bleiben. + AVPlayerEngine() + #endif } private func observe(_ engine: any VideoPlayerEngine) { diff --git a/Flixy/Views/Player/PlayerView.swift b/Flixy/Views/Player/PlayerView.swift index 3388d4a..bf34867 100644 --- a/Flixy/Views/Player/PlayerView.swift +++ b/Flixy/Views/Player/PlayerView.swift @@ -101,7 +101,11 @@ struct PlayerView: View { // Die Uhrzeit über einem laufenden Film hat dort nichts zu suchen — // und im Querformat erst recht nicht. .animation(.easeInOut(duration: 0.2), value: model.isBuffering) +#if os(iOS) + // Die Statusleiste gehört nicht über einen Film. Auf dem Mac gibt es + // keine. .statusBarHidden(true) + #endif .persistentSystemOverlays(showsControls ? .automatic : .hidden) .landscapeWhileVisible(settings.locksLandscape) .contentShape(.rect) diff --git a/Flixy/Views/Player/QualitySheet.swift b/Flixy/Views/Player/QualitySheet.swift index e49706c..7d45d0c 100644 --- a/Flixy/Views/Player/QualitySheet.swift +++ b/Flixy/Views/Player/QualitySheet.swift @@ -54,14 +54,14 @@ struct QualitySheet: View { } } .navigationTitle(Text("player.quality")) - .navigationBarTitleDisplayMode(.inline) + .flixyInlineTitle() .toolbar { ToolbarItem(placement: .confirmationAction) { Button("action.done") { dismiss() } } } } - .presentationDetents([.medium, .large]) + .flixyResizableSheet() } /// Was gerade läuft — gemessen, nicht gewünscht. diff --git a/Flixy/Views/Player/TrackPickerSheet.swift b/Flixy/Views/Player/TrackPickerSheet.swift index f7ffee3..458d44a 100644 --- a/Flixy/Views/Player/TrackPickerSheet.swift +++ b/Flixy/Views/Player/TrackPickerSheet.swift @@ -69,14 +69,14 @@ struct TrackPickerSheet: View { .scrollContentBackground(.hidden) .background(Color.flixyCanvas) .navigationTitle(Text(kind.title)) - .navigationBarTitleDisplayMode(.inline) + .flixyInlineTitle() .toolbar { ToolbarItem(placement: .confirmationAction) { Button("action.close") { dismiss() } } } } - .presentationDetents([.medium]) + .flixyMediumSheet() } private func row(title: String, isSelected: Bool, action: @escaping () -> Void) -> some View { diff --git a/Flixy/Views/Player/VideoSurface.swift b/Flixy/Views/Player/VideoSurface.swift index 51a79ff..03f01ff 100644 --- a/Flixy/Views/Player/VideoSurface.swift +++ b/Flixy/Views/Player/VideoSurface.swift @@ -1,7 +1,24 @@ import AVKit import SwiftUI import FlixyPlayback +#if canImport(UIKit) +import UIKit +#else +import AppKit +#endif +#if canImport(FlixyVLC) import FlixyVLC +#endif + +/// Die Ansichtsklasse der jeweiligen Plattform. +/// +/// UIKit und AppKit heißen verschieden und meinen dasselbe. Ein Name dafür +/// spart in dieser Datei ein Dutzend `#if`. +#if canImport(UIKit) +typealias PlatformView = UIView +#else +typealias PlatformView = NSView +#endif /// Die Bildfläche des laufendie Engine. /// @@ -22,14 +39,21 @@ struct VideoSurface: View { let surface: PlayerLayerView var body: some View { - switch engine { - case is AVPlayerEngine: - UIViewContainer(view: surface) - case let vlcEngine as VLCPlayerEngine: - UIViewContainer(view: vlcEngine.videoView) - default: + #if canImport(FlixyVLC) + if let vlcEngine = engine as? VLCPlayerEngine { + SurfaceContainer(view: vlcEngine.videoView) + } else if engine is AVPlayerEngine { + SurfaceContainer(view: surface) + } else { Color.black } + #else + if engine is AVPlayerEngine { + SurfaceContainer(view: surface) + } else { + Color.black + } + #endif } } @@ -38,10 +62,31 @@ struct VideoSurface: View { /// Der übliche Weg: eine Ebene von Hand als Unterebene einzuhängen bedeutet, /// ihre Größe bei jeder Drehung selbst nachzuführen. Als Hauptebene macht das /// UIKit. +#if canImport(UIKit) final class PlayerLayerView: UIView { override static var layerClass: AnyClass { AVPlayerLayer.self } var playerLayer: AVPlayerLayer { layer as! AVPlayerLayer } } +#else +/// Auf dem Mac gibt es kein `layerClass`: eine `NSView` bekommt ihre Ebene +/// zugewiesen, statt ihre Art vorzugeben. +final class PlayerLayerView: NSView { + let playerLayer = AVPlayerLayer() + + override init(frame: NSRect) { + super.init(frame: frame) + wantsLayer = true + layer = playerLayer + // Ohne das bleibt die Ebene beim Vergrößern des Fensters stehen, wo + // sie war, und das Bild sitzt in der Ecke. + layer?.needsDisplayOnBoundsChange = true + autoresizingMask = [.width, .height] + } + + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError("nicht aus einem Storyboard") } +} +#endif /// Hängt eine bestehende `UIView` in den SwiftUI-Baum. /// @@ -58,25 +103,57 @@ final class PlayerLayerView: UIView { /// /// Mit eigener Hülle gehört jeder Ansicht ihre eigene, und `updateUIView` holt /// die Fläche zurück, falls sie zwischenzeitlich woanders eingehängt wurde. -private struct UIViewContainer: UIViewRepresentable { - let view: UIView +#if canImport(UIKit) +private struct SurfaceContainer: UIViewRepresentable { + let view: PlatformView - func makeUIView(context: Context) -> UIView { + func makeUIView(context: Context) -> PlatformView { let host = UIView() host.backgroundColor = .black - adopt(into: host) + adopt(view, into: host) return host } - func updateUIView(_ host: UIView, context: Context) { + func updateUIView(_ host: PlatformView, context: Context) { guard view.superview !== host else { return } - adopt(into: host) - } - - private func adopt(into host: UIView) { - view.translatesAutoresizingMaskIntoConstraints = true - view.frame = host.bounds - view.autoresizingMask = [.flexibleWidth, .flexibleHeight] - host.addSubview(view) + adopt(view, into: host) } } +#else +private struct SurfaceContainer: NSViewRepresentable { + let view: PlatformView + + func makeNSView(context: Context) -> PlatformView { + let host = NSView() + host.wantsLayer = true + host.layer?.backgroundColor = NSColor.black.cgColor + adopt(view, into: host) + return host + } + + func updateNSView(_ host: PlatformView, context: Context) { + guard view.superview !== host else { return } + adopt(view, into: host) + } +} +#endif + +/// Hängt die Fläche in ihre Hülle. +/// +/// Sie wird **nicht** unmittelbar an SwiftUI zurückgegeben, sondern in eine +/// eigene Hülle gehängt. Der Unterschied zählt genau in dem Moment, in dem es +/// Kachel und Vollbild kurz gleichzeitig gibt: eine Ansicht hat nur einen +/// Vorgesetzten, und wer sie zuletzt bekommt, gewinnt. Gab SwiftUI sie +/// unmittelbar heraus, nahm das Abräumen der einen der anderen das Bild weg. +private func adopt(_ view: PlatformView, into host: PlatformView) { + #if canImport(UIKit) + view.translatesAutoresizingMaskIntoConstraints = true + view.frame = host.bounds + view.autoresizingMask = [.flexibleWidth, .flexibleHeight] + #else + view.translatesAutoresizingMaskIntoConstraints = true + view.frame = host.bounds + view.autoresizingMask = [.width, .height] + #endif + host.addSubview(view) +} diff --git a/Flixy/Views/Search/SearchView.swift b/Flixy/Views/Search/SearchView.swift index 34fe00f..cd3215c 100644 --- a/Flixy/Views/Search/SearchView.swift +++ b/Flixy/Views/Search/SearchView.swift @@ -26,8 +26,8 @@ struct SearchView: View { } .background(Color.flixyCanvas) .navigationTitle(Text("search.title")) - .navigationBarTitleDisplayMode(.inline) - .searchable(text: $text, placement: .navigationBarDrawer(displayMode: .always)) + .flixyInlineTitle() + .searchable(text: $text) // Jeder Tastendruck löst eine Anfrage aus — aber erst nach einer // kurzen Pause. Ohne sie schickt „Blade Runner" zwölf Anfragen los, // von denen elf verworfen werden. diff --git a/Flixy/Views/Settings/AppearanceView.swift b/Flixy/Views/Settings/AppearanceView.swift index efdb63f..d447d07 100644 --- a/Flixy/Views/Settings/AppearanceView.swift +++ b/Flixy/Views/Settings/AppearanceView.swift @@ -35,8 +35,7 @@ struct AppearanceView: View { if showsCustomField { HStack { TextField("appearance.hexPlaceholder", text: $customHex) - .textInputAutocapitalization(.characters) - .autocorrectionDisabled() + .flixyPlainEntry() .onSubmit(applyCustomColor) Button("action.apply", action: applyCustomColor) @@ -71,7 +70,7 @@ struct AppearanceView: View { .scrollContentBackground(.hidden) .background(Color.flixyCanvas) .navigationTitle(Text("appearance.title")) - .navigationBarTitleDisplayMode(.inline) + .flixyInlineTitle() .onAppear { if theme.selection.id == AccentOption.customIdentifier { customHex = theme.selection.color.hexString diff --git a/Flixy/Views/Settings/DiagnosticsView.swift b/Flixy/Views/Settings/DiagnosticsView.swift index a809caa..a8e8482 100644 --- a/Flixy/Views/Settings/DiagnosticsView.swift +++ b/Flixy/Views/Settings/DiagnosticsView.swift @@ -60,7 +60,7 @@ struct DiagnosticsView: View { Section { Button { - UIPasteboard.general.string = report(for: attempt) + FlixyClipboard.copy(report(for: attempt)) } label: { Label("diagnostics.copy", systemImage: "doc.on.doc") } @@ -92,7 +92,7 @@ struct DiagnosticsView: View { .textSelection(.enabled) Button { - UIPasteboard.general.string = liveTVStructure + FlixyClipboard.copy(liveTVStructure) } label: { Label("diagnostics.copy", systemImage: "doc.on.doc") } @@ -118,7 +118,7 @@ struct DiagnosticsView: View { .scrollContentBackground(.hidden) .background(Color.flixyCanvas) .navigationTitle(Text("diagnostics.title")) - .navigationBarTitleDisplayMode(.inline) + .flixyInlineTitle() } /// Fragt den Server, wie seine Antwort auf `/livetv/dvrs` aufgebaut ist. diff --git a/Flixy/Views/Settings/DownloadSettingsView.swift b/Flixy/Views/Settings/DownloadSettingsView.swift index b2da1d6..44d4e28 100644 --- a/Flixy/Views/Settings/DownloadSettingsView.swift +++ b/Flixy/Views/Settings/DownloadSettingsView.swift @@ -76,7 +76,7 @@ struct DownloadSettingsView: View { .scrollContentBackground(.hidden) .background(Color.flixyCanvas) .navigationTitle(Text("sidebar.downloads")) - .navigationBarTitleDisplayMode(.inline) + .flixyInlineTitle() } private var storageLimitBinding: Binding { diff --git a/Flixy/Views/Settings/HomeLayoutView.swift b/Flixy/Views/Settings/HomeLayoutView.swift index 17f1f2d..a6a825e 100644 --- a/Flixy/Views/Settings/HomeLayoutView.swift +++ b/Flixy/Views/Settings/HomeLayoutView.swift @@ -15,8 +15,12 @@ struct HomeLayoutView: View { list .background(Color.flixyCanvas) .navigationTitle(Text("homeLayout.title")) - .navigationBarTitleDisplayMode(.inline) + .flixyInlineTitle() + // Auf dem Mac gibt es den Knopf nicht — dort zieht man Zeilen ohne + // vorherigen Moduswechsel. + #if os(iOS) .toolbar { EditButton() } + #endif .task { await load() } } diff --git a/Flixy/Views/Settings/LanguagesView.swift b/Flixy/Views/Settings/LanguagesView.swift index 90f64f3..21b314f 100644 --- a/Flixy/Views/Settings/LanguagesView.swift +++ b/Flixy/Views/Settings/LanguagesView.swift @@ -62,7 +62,7 @@ struct LanguagesView: View { .scrollContentBackground(.hidden) .background(Color.flixyCanvas) .navigationTitle(Text("languages.title")) - .navigationBarTitleDisplayMode(.inline) + .flixyInlineTitle() } /// Der Name der Sprache in der Sprache des Geräts. diff --git a/Flixy/Views/Settings/PlaybackSettingsView.swift b/Flixy/Views/Settings/PlaybackSettingsView.swift index 494ff81..50487a4 100644 --- a/Flixy/Views/Settings/PlaybackSettingsView.swift +++ b/Flixy/Views/Settings/PlaybackSettingsView.swift @@ -89,7 +89,7 @@ struct PlaybackSettingsView: View { .scrollContentBackground(.hidden) .background(Color.flixyCanvas) .navigationTitle(Text("settings.playback")) - .navigationBarTitleDisplayMode(.inline) + .flixyInlineTitle() } private func rateLabel(_ rate: Double) -> String { diff --git a/Flixy/Views/Settings/ServerSettingsView.swift b/Flixy/Views/Settings/ServerSettingsView.swift index 25f2c61..99b3a4e 100644 --- a/Flixy/Views/Settings/ServerSettingsView.swift +++ b/Flixy/Views/Settings/ServerSettingsView.swift @@ -49,7 +49,7 @@ struct ServerSettingsView: View { .scrollContentBackground(.hidden) .background(Color.flixyCanvas) .navigationTitle(Text("settings.server")) - .navigationBarTitleDisplayMode(.inline) + .flixyInlineTitle() } private var kindLabel: String { diff --git a/Flixy/Views/Settings/SettingsView.swift b/Flixy/Views/Settings/SettingsView.swift index 2d919d7..353c5e5 100644 --- a/Flixy/Views/Settings/SettingsView.swift +++ b/Flixy/Views/Settings/SettingsView.swift @@ -83,7 +83,7 @@ struct SettingsView: View { .scrollContentBackground(.hidden) .background(Color.flixyCanvas) .navigationTitle(Text("sidebar.settings")) - .navigationBarTitleDisplayMode(.inline) + .flixyInlineTitle() .confirmationDialog( Text("settings.signOutConfirm"), isPresented: $showsSignOutConfirmation, diff --git a/Flixy/Views/SignIn/JellyfinSignInView.swift b/Flixy/Views/SignIn/JellyfinSignInView.swift index 2aaf012..79df354 100644 --- a/Flixy/Views/SignIn/JellyfinSignInView.swift +++ b/Flixy/Views/SignIn/JellyfinSignInView.swift @@ -32,7 +32,7 @@ struct JellyfinSignInView: View { .scrollDismissesKeyboard(.interactively) .background(Color.flixyCanvas) .navigationTitle(Text("signIn.jellyfin.title")) - .navigationBarTitleDisplayMode(.inline) + .flixyInlineTitle() .toolbar { ToolbarItem(placement: .cancellationAction) { Button("action.cancel") { @@ -102,9 +102,7 @@ struct JellyfinSignInView: View { TextField("signIn.jellyfin.addressPlaceholder", text: $model.address) .textFieldStyle(.plain) .textContentType(.URL) - .keyboardType(.URL) - .textInputAutocapitalization(.never) - .autocorrectionDisabled() + .flixyPlainEntry(isAddress: true) .submitLabel(.go) .focused($focusedField, equals: .address) .onSubmit { Task { await model.findServer() } } @@ -188,8 +186,7 @@ struct JellyfinSignInView: View { TextField("signIn.jellyfin.userName", text: $model.userName) .textContentType(.username) - .textInputAutocapitalization(.never) - .autocorrectionDisabled() + .flixyPlainEntry() .submitLabel(.next) .focused($focusedField, equals: .userName) .onSubmit { focusedField = .password } diff --git a/Packages/FlixyKit/Sources/FlixyDesign/PlatformShims.swift b/Packages/FlixyKit/Sources/FlixyDesign/PlatformShims.swift new file mode 100644 index 0000000..e07bc84 --- /dev/null +++ b/Packages/FlixyKit/Sources/FlixyDesign/PlatformShims.swift @@ -0,0 +1,110 @@ +import SwiftUI +#if canImport(UIKit) +import UIKit +#else +import AppKit +#endif + +/// Was auf iPhone und Mac verschieden heißt — an einer Stelle. +/// +/// Nicht jede Ansicht soll `#if os(iOS)` tragen. Ein Formular, das auf beiden +/// Plattformen dasselbe meint, soll auf beiden gleich aussehen; wo die +/// Systeme verschiedene Namen dafür haben, steht der Unterschied hier und +/// nirgends sonst. Sonst wandert er in zwanzig Dateien und wird beim +/// nächsten Bildschirm vergessen. +extension View { + /// Eine schmale Titelzeile — auf dem Mac gibt es sie nicht. + public func flixyInlineTitle() -> some View { + #if os(iOS) + navigationBarTitleDisplayMode(.inline) + #else + self + #endif + } + + /// Ein Blatt, das die halbe Höhe einnimmt. + /// + /// Auf dem Mac ist ein Blatt ein Fenster: dort zählt keine Rastung, + /// sondern eine vernünftige Mindestgröße. + public func flixyMediumSheet() -> some View { + #if os(iOS) + presentationDetents([.medium]) + #else + frame(minWidth: 420, minHeight: 460) + #endif + } + + /// Dasselbe, aber auf Wunsch bis zur vollen Höhe aufziehbar. + public func flixyResizableSheet() -> some View { + #if os(iOS) + presentationDetents([.medium, .large]) + #else + frame(minWidth: 460, minHeight: 520) + #endif + } + + /// Eingabefelder für Adressen und Namen: keine Großschreibung, keine + /// Korrektur — auf dem Mac gibt es beides ohnehin nicht als Tastatur. + public func flixyPlainEntry(isAddress: Bool = false) -> some View { + #if os(iOS) + self + .keyboardType(isAddress ? .URL : .default) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + #else + autocorrectionDisabled() + #endif + } +} + +extension View { + /// Keine eigene Fläche hinter der Titelzeile. + /// + /// Auf der Startseite liegt darunter der Aufmacher — eine weichgezeichnete + /// Leiste verschleierte die Hälfte davon. Auf dem Mac gibt es die Leiste in + /// dieser Form nicht. + public func flixyClearToolbarBackground() -> some View { + #if os(iOS) + self + .toolbarBackground(.hidden, for: .navigationBar) + .toolbarColorScheme(.dark, for: .navigationBar) + #else + self + #endif + } +} + +/// Etwas in die Zwischenablage legen. +/// +/// UIKit und AppKit heißen verschieden und meinen dasselbe; die Diagnose soll +/// deswegen nicht mit `#if` beginnen. +public enum FlixyClipboard { + public static func copy(_ text: String) { + #if canImport(UIKit) + UIPasteboard.general.string = text + #else + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(text, forType: .string) + #endif + } +} + +extension ToolbarItemPlacement { + /// Rechts in der Titelzeile. + public static var flixyTrailing: ToolbarItemPlacement { + #if os(iOS) + .topBarTrailing + #else + .primaryAction + #endif + } + + /// Links in der Titelzeile. + public static var flixyLeading: ToolbarItemPlacement { + #if os(iOS) + .topBarLeading + #else + .navigation + #endif + } +} diff --git a/project.yml b/project.yml index 9b8fb2b..f82ea31 100644 --- a/project.yml +++ b/project.yml @@ -4,6 +4,7 @@ options: bundleIdPrefix: com.scarriffleservices deploymentTarget: iOS: "26.0" + macOS: "26.0" createIntermediateGroups: true groupSortPosition: top developmentLanguage: en @@ -147,6 +148,62 @@ targets: # Die Live Activity für Downloads. Eine eigene Erweiterung, weil iOS die # Darstellung im Sperrbildschirm in einem getrennten Prozess zeichnet — die # App selbst läuft zu dem Zeitpunkt meist gar nicht. + # Dieselben Quellen, eigenes Ziel. + # + # Ein gemeinsames Target für beide Plattformen wäre weniger Text, aber die + # Info.plist ist voller iOS-Angaben — Startbildschirm, erlaubte + # Ausrichtungen, Live Activities. Die müssten dann alle bedingt werden, und + # jede spätere Änderung an der iOS-Seite ginge über eine Stelle, die auch + # den Mac betrifft. Zwei Ziele sind ein paar Zeilen mehr und ein Sorge + # weniger. + # + # Ohne DownloadWidget (Live Activities gibt es auf dem Mac nicht) und ohne + # FlixyVLC: das Binärpaket ist auf iOS festgelegt. Der Player fällt dort + # allein auf den Systemplayer zurück — was er nicht öffnen kann, sagt er. + FlixyMac: + type: application + platform: macOS + sources: + - path: Flixy + excludes: + - "AppIcon.icon" + dependencies: + - package: FlixyKit + product: MediaKit + - package: FlixyKit + product: PlexKit + - package: FlixyKit + product: JellyfinKit + - package: FlixyKit + product: FlixyDesign + - package: FlixyKit + product: FlixyPlayback + - package: FlixyKit + product: FlixyDownloads + settings: + base: + PRODUCT_BUNDLE_IDENTIFIER: com.scarriffleservices.flixy + PRODUCT_NAME: Flixy + # Selbst signiert, ohne Entwicklerkonto: ein Build zum Ansehen soll + # nicht jedes Mal nach dem Schlüsselbund-Passwort fragen. + CODE_SIGN_STYLE: Manual + CODE_SIGN_IDENTITY: "-" + DEVELOPMENT_TEAM: "" + ENABLE_HARDENED_RUNTIME: NO + COMBINE_HIDPI_IMAGES: YES + info: + path: Flixy/Info-macOS.plist + properties: + CFBundleDisplayName: Flixy + LSApplicationCategoryType: public.app-category.entertainment + NSHumanReadableCopyright: "" + CFBundleURLTypes: + - CFBundleURLName: com.scarriffleservices.flixy.auth + CFBundleURLSchemes: + - com.scarriffleservices.flixy + NSAppTransportSecurity: + NSAllowsLocalNetworking: true + DownloadWidget: type: app-extension platform: iOS