name: Flaro options: bundleIdPrefix: com.scarriffleservices deploymentTarget: iOS: "26.0" macOS: "26.0" tvOS: "26.0" createIntermediateGroups: true groupSortPosition: top developmentLanguage: en settings: base: DEVELOPMENT_TEAM: PP34X97WS3 SWIFT_VERSION: "6.0" MARKETING_VERSION: "0.1.0" CURRENT_PROJECT_VERSION: "1" SWIFT_STRICT_CONCURRENCY: complete packages: FlaroKit: path: Packages/FlaroKit targets: Flaro: type: application platform: iOS # Ausdrücklich aufgeschrieben, statt Xcode raten zu lassen. # # Xcode legt für jedes Ziel von selbst ein Schema an — aber im # Benutzerordner, und `xcodegen generate` räumt den mit weg. Danach # findet `xcodebuild` kein Schema mehr und kann die Pakete nicht # auflösen. Hier steht es im Projekt und überlebt jedes Neuerzeugen. scheme: testTargets: - FlaroUITests sources: # Das Icon ist ein Bündel, also technisch ein Ordner. Ohne diesen # Ausschluss läuft XcodeGen hinein und trägt icon.json und play.svg # einzeln ein — dann fehlt das Icon, ohne dass irgendwo ein Fehler # auftaucht. # - path: Flaro excludes: - "AppIcon.icon" - path: Flaro/AppIcon.icon type: file dependencies: - target: DownloadWidget # Die neutrale Grundschicht. Ausdrücklich genannt, obwohl sie über # JellyfinKit ohnehin im Baum hängt: die App soll sie direkt benutzen und # nicht davon abhängen, dass ein bestimmter Server sie mitschleppt. - package: FlaroKit product: MediaKit # Der Server. Steht auf der neutralen Schicht, nicht darin — kommt ein # zweiter dazu, steht er daneben. - package: FlaroKit product: JellyfinKit - package: FlaroKit product: FlaroServerKit - package: FlaroKit product: FlaroDesign - package: FlaroKit product: FlaroPlayback - package: FlaroKit product: FlaroDownloads settings: base: PRODUCT_BUNDLE_IDENTIFIER: com.scarriffleservices.flaro PRODUCT_NAME: Flaro # Verweist auf AppIcon.icon. Der frühere leere AppIcon.appiconset im # Asset-Katalog ist entfernt — zwei Dinge namens AppIcon, und Xcode # nimmt das falsche. ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon # iPhone und iPad aus demselben Target. TARGETED_DEVICE_FAMILY: "1,2" CODE_SIGN_STYLE: Automatic info: path: Flaro/Info.plist properties: CFBundleDisplayName: Flaro UILaunchScreen: UIColorName: LaunchBackground # Der harte Schalter für „nur dunkel". Eine Ansicht mit # .preferredColorScheme(.dark) allein reicht nicht: Systemdialoge, # Statusleiste und Tastatur richten sich danach nicht. UIUserInterfaceStyle: Dark # Alle drei bleiben eingetragen: die Info.plist sagt, was die App # grundsätzlich kann. Was davon gerade gilt, entscheidet zur Laufzeit # OrientationLock über den App-Delegaten — Hochformat beim Blättern, # Querformat im Player. UISupportedInterfaceOrientations: - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad: - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight # Ohne diesen Modus hält iOS die Wiedergabe an, sobald die App in den # Hintergrund geht — und Bild-in-Bild gäbe es gar nicht. UIBackgroundModes: - audio # Die Ausfuhr-Erklärung zur Verschlüsselung. # # `false` heißt: die App nutzt ausschließlich Verschlüsselung, die # unter die Ausnahmeregelung fällt. Das trifft hier zu — HTTPS über # URLSession und der Schlüsselbund für die Tokens, beides vom # Betriebssystem gestellt. Eigene Algorithmen gibt es keine. # # Ohne diesen Eintrag fragt App Store Connect bei JEDEM Build erneut # („Missing Compliance"), und der Build lässt sich bis dahin nicht # verteilen. # # Zu beachten, falls sich das ändert: solange nur Dateien vom eigenen # Server abgespielt werden, bleibt es bei Standardverfahren. Käme # irgendwann etwas mit eigener Verschlüsselung dazu — DRM etwa —, # gehört dieser Wert überprüft. ITSAppUsesNonExemptEncryption: false # Ohne diesen Schlüssel lehnt ActivityKit jede Anfrage ab — und # zwar wortlos, was die Suche nach dem Grund unangenehm macht. NSSupportsLiveActivities: true NSAppTransportSecurity: # Medienserver im Heimnetz laufen überwiegend auf blankem HTTP — # Jellyfin ab Werk auf Port 8096. Diese Ausnahme erlaubt genau das, # ohne die Transportsicherheit für das offene Netz abzuschalten. NSAllowsLocalNetworking: true # Seit iOS 14 verlangt jeder Zugriff auf Adressen im eigenen Netz eine # Freigabe. Fehlt der Text, verweigert das System sie kommentarlos und # die Serversuche findet dauerhaft nichts. NSLocalNetworkUsageDescription: >- Flaro benötigt Zugriff auf das lokale Netzwerk, um deinen Jellyfin-Server zu erreichen und Medien direkt von ihm zu laden. # Die Fassung für den Fernseher. # # **Eigene Ansichten, geteilte Modelle.** Die Quellenpfade sind die ganze # Aussage dieses Blocks: `FlaroTV/` gehört nur hierher, `Flaro/Session/` # gehört beiden. Keine Datei muss dafür umziehen — XcodeGen nimmt Pfade je # Ziel, und die iOS-Fassung sieht von der ganzen Sache nichts. FlaroTV: type: application platform: tvOS # Aus demselben Grund ausgeschrieben wie bei Flaro — siehe dort. scheme: {} sources: # Die eigenen Ansichten. - path: FlaroTV # Die Modelle. - path: Flaro/Session excludes: # Ein Fernseher dreht sich nicht. - "OrientationLock.swift" # Heruntergeladen wird auf dem Fernseher nichts. # # `DownloadRefiller.swift` und `DownloadService.swift` kommen # trotzdem mit: `PlayerModel` nennt den Nachfüller in einem Zweig, # der auf tvOS nie läuft (`downloads` ist dort `nil`), und der # Nachfüller wiederum den Dienst. Sie auszuschliessen hiesse, eine # Plattformfrage mitten in zweitausend Zeilen Wiedergabelogik zu # tragen — teurer als zwei Dateien mitzuübersetzen, die niemand # aufruft. Beide sind reine Übersetzer ohne UIKit. # Reines Modell trotz des Ordnernamens: LoadState, ErrorText und # MediaRoute liegen zusammen in einer Datei. - path: Flaro/Views/LoadState.swift type: file # Der Player. **Ohne PlayerView** — die zeichnet auf iOS das Bild und # alle Bedienelemente selbst; auf dem Fernseher macht das Apples # AVPlayerViewController. Das Modell darunter ist dasselbe. - path: Flaro/Views/Player/PlayerModel.swift type: file - path: Flaro/Views/Player/VideoSurface.swift type: file - path: Flaro/Branding.swift type: file # Die Texte. Dieselben Schlüssel, dieselben Übersetzungen. - path: Flaro/Resources/Localizable.xcstrings type: file dependencies: - package: FlaroKit product: MediaKit - package: FlaroKit product: JellyfinKit - package: FlaroKit product: FlaroServerKit - package: FlaroKit product: FlaroDesign - package: FlaroKit product: FlaroPlayback # Nur damit `PlayerModel` den Typ nennen darf. Angelegt wird nie einer: # FlaroTVApp reicht `nil` durch, und damit gibt es auf tvOS auch keinen # ModelContainer und kein SwiftData. - package: FlaroKit product: FlaroDownloads - target: FlaroTopShelf settings: base: PRODUCT_BUNDLE_IDENTIFIER: com.scarriffleservices.flaro.tv PRODUCT_NAME: Flaro # Nur Apple TV. TARGETED_DEVICE_FAMILY: "3" # Kein AppIcon.icon: Icon Composer deckt iOS, macOS und watchOS ab, # tvOS nicht. Der Name zeigt auf die Brand-Assets-Gruppe im Katalog, # und der Katalog liegt unter FlaroTV/Resources. ASSETCATALOG_COMPILER_APPICON_NAME: "App Icon & Top Shelf Image" CODE_SIGN_STYLE: Automatic CODE_SIGN_ENTITLEMENTS: FlaroTV/Flaro.entitlements info: path: FlaroTV/Info.plist properties: CFBundleDisplayName: Flaro UIUserInterfaceStyle: Dark ITSAppUsesNonExemptEncryption: false # Über dieses Schema schickt das Regal zurück in die App. Ohne den # Eintrag kennt tvOS es nicht, und ein Druck auf ein Plakat tut # nichts — ohne Fehler, ohne Hinweis. CFBundleURLTypes: - CFBundleURLName: com.scarriffleservices.flaro.topshelf CFBundleURLSchemes: - flaro # Was hier **nicht** steht, und warum: # # UILaunchScreen — tvOS hat kein Startbild dieser Art. # UISupportedInterfaceOrientations — ein Fernseher dreht sich nicht. # NSSupportsLiveActivities — ActivityKit fehlt im tvOS-SDK. # CFBundleURLTypes — die Anmeldung braucht kein Rückkehr-Schema. # UIBackgroundModes: audio — ein Apple TV schickt keine App in den # Hintergrund, aus dem sie weiterspielen müsste. NSAppTransportSecurity: NSAllowsLocalNetworking: true NSLocalNetworkUsageDescription: >- Flaro benötigt Zugriff auf das lokale Netzwerk, um deinen Jellyfin-Server zu erreichen. # Das Regal über der App im tvOS-Menü. # # Eine eigene Erweiterung, weil tvOS sie zeichnet, während die App **nicht** # läuft — und mit weniger als einer Sekunde Zeit. Sie liest deshalb nur eine # Datei aus der App-Gruppe, die die App beim Laden der Startseite ablegt: # kein Netz, kein Schlüsselbund, kein Blättern. FlaroTopShelf: type: tv-app-extension platform: tvOS sources: - path: FlaroTopShelf dependencies: - package: FlaroKit product: MediaKit settings: base: PRODUCT_BUNDLE_IDENTIFIER: com.scarriffleservices.flaro.tv.topshelf PRODUCT_NAME: FlaroTopShelf CODE_SIGN_STYLE: Automatic CODE_SIGN_ENTITLEMENTS: FlaroTopShelf/FlaroTopShelf.entitlements SKIP_INSTALL: YES info: path: FlaroTopShelf/Info.plist properties: CFBundleDisplayName: Flaro # **Die Erweiterung hat ihre eigene Info.plist.** # # Und damit ihre eigene Transportsicherheit: die Ausnahme in der # Info.plist der App gilt hier nicht. Ohne sie blockt das System jedes # Plakat weg, das über blankes HTTP vom Jellyfin im Heimnetz kommt — # lautlos, und das Regal bleibt leer. Genau so sieht „geht halt nicht" # aus. NSAppTransportSecurity: NSAllowsLocalNetworking: true NSExtension: NSExtensionPointIdentifier: com.apple.tv-top-shelf NSExtensionPrincipalClass: $(PRODUCT_MODULE_NAME).TopShelfProvider # Nur zum Hinsehen: fährt die App mit der Attrappe in einen bestimmten # Zustand und hält sie dort an, damit von außen fotografiert werden kann. # Kein Teil der App, wird nie mitgeliefert. FlaroUITests: type: bundle.ui-testing platform: iOS sources: - FlaroUITests dependencies: - target: Flaro settings: base: PRODUCT_BUNDLE_IDENTIFIER: com.scarriffleservices.flaro.uitests TEST_TARGET_NAME: Flaro TARGETED_DEVICE_FAMILY: "1,2" CODE_SIGN_STYLE: Automatic GENERATE_INFOPLIST_FILE: YES # 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, dasselbe Aussehen. DownloadWidget: type: app-extension platform: iOS sources: - path: DownloadWidget dependencies: - package: FlaroKit product: FlaroDownloads - package: FlaroKit product: FlaroDesign settings: base: PRODUCT_BUNDLE_IDENTIFIER: com.scarriffleservices.flaro.downloadwidget PRODUCT_NAME: DownloadWidget CODE_SIGN_STYLE: Automatic SKIP_INSTALL: YES # Von XcodeGen erzeugt statt von Hand geschrieben. Eine eigene Datei ohne # CFBundleIdentifier führt zu einer Erweiterung ganz ohne Kennung — und # der Fehler lautet dann „not prefixed with the parent app's bundle # identifier", was auf eine falsche Fährte führt. info: path: DownloadWidget/Info.plist properties: CFBundleDisplayName: Flaro Downloads NSExtension: NSExtensionPointIdentifier: com.apple.widgetkit-extension