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