react-native-audio-browser 0.2.0-next.3 → 0.2.0-next.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (989) hide show
  1. package/README.md +36 -22
  2. package/android/build.gradle +35 -12
  3. package/android/gradle.properties +2 -2
  4. package/android/src/main/AndroidManifest.xml +3 -1
  5. package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +541 -278
  6. package/android/src/main/java/com/audiobrowser/AudioBrowserLifecycleModule.kt +38 -0
  7. package/android/src/main/java/com/audiobrowser/AudioBrowserPackage.kt +18 -2
  8. package/android/src/main/java/com/audiobrowser/ServiceBinding.kt +52 -0
  9. package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +315 -213
  10. package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +27 -9
  11. package/android/src/main/java/com/audiobrowser/browser/InertStyleDiagnostic.kt +288 -0
  12. package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +97 -58
  13. package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +70 -29
  14. package/android/src/main/java/com/audiobrowser/browser/StyleResolver.kt +60 -0
  15. package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +20 -7
  16. package/android/src/main/java/com/audiobrowser/extension/TappedTrack.kt +20 -0
  17. package/android/src/main/java/com/audiobrowser/extension/TrackIdentity.kt +15 -0
  18. package/android/src/main/java/com/audiobrowser/http/HttpClient.kt +20 -3
  19. package/android/src/main/java/com/audiobrowser/model/IcyTitle.kt +24 -0
  20. package/android/src/main/java/com/audiobrowser/model/PlaybackMetadata.kt +10 -6
  21. package/android/src/main/java/com/audiobrowser/model/PlayerUpdateOptions.kt +12 -12
  22. package/android/src/main/java/com/audiobrowser/player/CapabilityControls.kt +111 -58
  23. package/android/src/main/java/com/audiobrowser/player/CrossOriginHeaderStripper.kt +77 -0
  24. package/android/src/main/java/com/audiobrowser/player/MediaFactory.kt +28 -10
  25. package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +509 -230
  26. package/android/src/main/java/com/audiobrowser/player/MediaSessionCommandManager.kt +218 -252
  27. package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +21 -25
  28. package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +113 -78
  29. package/android/src/main/java/com/audiobrowser/player/Player.kt +81 -38
  30. package/android/src/main/java/com/audiobrowser/player/PlayerCapabilitiesExt.kt +2 -15
  31. package/android/src/main/java/com/audiobrowser/player/PlayerListener.kt +1 -1
  32. package/android/src/main/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicy.kt +2 -1
  33. package/android/src/main/java/com/audiobrowser/player/ShuffleOrderLedBy.kt +30 -0
  34. package/android/src/main/java/com/audiobrowser/tls/AiaCertChaser.kt +67 -11
  35. package/android/src/main/java/com/audiobrowser/tls/AiaChasingExtendedTrustManager.kt +168 -0
  36. package/android/src/main/java/com/audiobrowser/tls/AiaChasingTrustManager.kt +58 -14
  37. package/android/src/main/java/com/audiobrowser/tls/AiaTls.kt +35 -7
  38. package/android/src/main/java/com/audiobrowser/tls/CachingCertificateFetcher.kt +278 -59
  39. package/android/src/main/java/com/audiobrowser/util/ArtworkVariant.kt +7 -9
  40. package/android/src/main/java/com/audiobrowser/util/BrowserPathHelper.kt +62 -40
  41. package/android/src/main/java/com/audiobrowser/util/MediaExtrasBuilder.kt +28 -23
  42. package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +110 -0
  43. package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +56 -52
  44. package/android/src/main/res/values/strings.xml +25 -5
  45. package/android/src/test/java/com/audiobrowser/ServiceBindingTest.kt +100 -0
  46. package/android/src/test/java/com/audiobrowser/TestFixtures.kt +119 -33
  47. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerFavoritesTest.kt +153 -0
  48. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerLayerResolutionTest.kt +15 -0
  49. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +346 -8
  50. package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +40 -2
  51. package/android/src/test/java/com/audiobrowser/browser/InertStyleDiagnosticTest.kt +301 -0
  52. package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +3 -2
  53. package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +168 -0
  54. package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +128 -55
  55. package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +160 -0
  56. package/android/src/test/java/com/audiobrowser/extension/TappedTrackTest.kt +43 -0
  57. package/android/src/test/java/com/audiobrowser/extension/TrackIdentityTest.kt +41 -0
  58. package/android/src/test/java/com/audiobrowser/model/IcyTitleTest.kt +49 -0
  59. package/android/src/test/java/com/audiobrowser/player/BrowseTilesTest.kt +385 -0
  60. package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +101 -58
  61. package/android/src/test/java/com/audiobrowser/player/CrossOriginHeaderStripperTest.kt +94 -0
  62. package/android/src/test/java/com/audiobrowser/player/MediaSessionSlotMappingTest.kt +45 -0
  63. package/android/src/test/java/com/audiobrowser/player/NowPlayingUpdaterTest.kt +42 -2
  64. package/android/src/test/java/com/audiobrowser/player/PlaybackErrorClassifierTest.kt +3 -4
  65. package/android/src/test/java/com/audiobrowser/player/PlaybackStateStoreTrackJsonTest.kt +55 -0
  66. package/android/src/test/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicyTest.kt +3 -3
  67. package/android/src/test/java/com/audiobrowser/player/ShuffleOrderLedByTest.kt +79 -0
  68. package/android/src/test/java/com/audiobrowser/tls/AiaCertChaserTest.kt +197 -7
  69. package/android/src/test/java/com/audiobrowser/tls/AiaChasingTrustManagerTest.kt +325 -4
  70. package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherCallSiteTest.kt +322 -0
  71. package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherTest.kt +83 -0
  72. package/android/src/test/java/com/audiobrowser/util/BrowserPathHelperTest.kt +55 -16
  73. package/android/src/test/java/com/audiobrowser/util/MediaExtrasBuilderTest.kt +162 -0
  74. package/android/src/test/java/com/audiobrowser/util/TrackFactoryMediaIdTest.kt +7 -7
  75. package/ios/AVPlayerEngine.swift +330 -0
  76. package/ios/AVPlayerEngineCallbacks.swift +15 -0
  77. package/ios/Browser/BrowseGate.swift +58 -0
  78. package/ios/Browser/BrowserConfig.swift +117 -78
  79. package/ios/Browser/BrowserManager.swift +209 -603
  80. package/ios/Browser/BrowserPathHelper.swift +50 -46
  81. package/ios/Browser/InertStyleDiagnostic.swift +296 -0
  82. package/ios/Browser/JsonModels.swift +264 -106
  83. package/ios/Browser/NavigationErrorState.swift +41 -0
  84. package/ios/Browser/PageResolver.swift +227 -0
  85. package/ios/Browser/RequestLayer.swift +90 -0
  86. package/ios/Browser/{BrowserManager+URLResolution.swift → RequestPipeline.swift} +193 -121
  87. package/ios/Browser/SearchDrillIn.swift +7 -7
  88. package/ios/Browser/SectionScope.swift +58 -24
  89. package/ios/Browser/StyleResolver.swift +58 -0
  90. package/ios/Browser/TabBarEntries.swift +9 -3
  91. package/ios/Browser/TrackIdTemplate.swift +37 -0
  92. package/ios/CarPlay/CPInterfaceController+Safe.swift +100 -43
  93. package/ios/CarPlay/CarPlayArtworkResolver.swift +36 -83
  94. package/ios/CarPlay/CarPlayController.swift +42 -1296
  95. package/ios/CarPlay/CarPlayImageLoader.swift +78 -97
  96. package/ios/CarPlay/CarPlayListItemFactory.swift +303 -92
  97. package/ios/CarPlay/CarPlayNowPlayingManager.swift +129 -136
  98. package/ios/CarPlay/CarPlaySession+Navigator.swift +114 -0
  99. package/ios/CarPlay/CarPlaySession+PageLoader.swift +92 -0
  100. package/ios/CarPlay/CarPlaySession+Refresher.swift +43 -0
  101. package/ios/CarPlay/CarPlaySession+TabBar.swift +193 -0
  102. package/ios/CarPlay/CarPlaySession.swift +209 -0
  103. package/ios/CarPlay/CarPlayTemplates.swift +63 -0
  104. package/ios/CarPlay/HybridAudioBrowser+IntentFavorite.swift +20 -0
  105. package/ios/{Player → CarPlay}/MediaIntentCriteria.swift +17 -9
  106. package/ios/CarPlay/MediaIntentPlayback.swift +74 -0
  107. package/ios/CarPlay/RNABMediaAddHandler.swift +8 -24
  108. package/ios/CarPlay/RNABMediaAffinityHandler.swift +7 -23
  109. package/ios/CarPlay/RNABMediaIntentHandler.swift +18 -19
  110. package/ios/CarPlay/SectionPresentation.swift +77 -0
  111. package/ios/CarPlay/TopTemplateReplacer.swift +96 -0
  112. package/ios/Extension/Capability+RemoteCommand.swift +1 -1
  113. package/ios/Extension/NavigationError+Conversion.swift +14 -4
  114. package/ios/Extension/PlayerCapabilities+Favorite.swift +1 -15
  115. package/ios/Extension/ResolvedTrack+Copying.swift +13 -15
  116. package/ios/Extension/Section+Copying.swift +29 -0
  117. package/ios/Extension/String+IcyCharset.swift +21 -0
  118. package/ios/Extension/TimedMetadata+AVFoundation.swift +6 -4
  119. package/ios/Extension/Track+Copying.swift +8 -12
  120. package/ios/Extension/Track+Identity.swift +18 -0
  121. package/ios/Extension/UIApplication+ActiveScene.swift +27 -0
  122. package/ios/Extension/UIImage+Adaptive.swift +12 -0
  123. package/ios/Extension/URLError+Connectivity.swift +15 -0
  124. package/ios/Http/HttpClient.swift +66 -142
  125. package/ios/HybridAudioBrowser+Browser.swift +153 -0
  126. package/ios/HybridAudioBrowser+Options.swift +144 -0
  127. package/ios/HybridAudioBrowser+Player.swift +397 -0
  128. package/ios/HybridAudioBrowser+PlayerCallbacks.swift +190 -0
  129. package/ios/HybridAudioBrowser+Stubs.swift +46 -0
  130. package/ios/HybridAudioBrowser.swift +153 -1610
  131. package/ios/Model/NitroTypeStubs.swift +146 -24
  132. package/ios/Model/PlayerUpdateOptions.swift +8 -1
  133. package/ios/Model/RemoteCommand.swift +18 -0
  134. package/ios/Model/TrackPlayerError.swift +2 -2
  135. package/ios/NowPlayingInfo/NowPlayingInfoController.swift +18 -53
  136. package/ios/Observer/ItemObserver.swift +90 -0
  137. package/ios/Observer/PlayerObserver.swift +59 -0
  138. package/ios/Option/SessionCategories.swift +42 -92
  139. package/ios/Player/AudioSessionController.swift +177 -0
  140. package/ios/Player/DeferredSeek.swift +70 -0
  141. package/ios/Player/ErrorStatus.swift +27 -0
  142. package/ios/Player/MediaLoader.swift +62 -35
  143. package/ios/Player/NowPlayingUpdater.swift +197 -50
  144. package/ios/Player/PlaybackFailure.swift +125 -0
  145. package/ios/Player/PlaybackPersistence.swift +73 -0
  146. package/ios/Player/PlaybackTimer.swift +8 -2
  147. package/ios/Player/Player.swift +1078 -0
  148. package/ios/Player/{PlaybackCoordinatorCallbacks.swift → PlayerCallbacks.swift} +3 -4
  149. package/ios/Player/PlayerEngine.swift +87 -0
  150. package/ios/Player/PlayerStatusTypes.swift +2 -6
  151. package/ios/Player/QueueManager.swift +93 -67
  152. package/ios/Player/RetryLoop.swift +261 -0
  153. package/ios/Player/ShuffleOrder.swift +33 -0
  154. package/ios/Player/SleepTimerManager.swift +1 -1
  155. package/ios/RemoteCommand/RemoteCommandCallbacks.swift +46 -0
  156. package/ios/RemoteCommand/RemoteCommandController.swift +87 -331
  157. package/ios/Tests/BrowserPathHelperTests.swift +56 -15
  158. package/ios/Tests/CarPlayArtworkResolverTests.swift +60 -373
  159. package/ios/Tests/DeferredSeekTests.swift +70 -0
  160. package/ios/Tests/EmitterTests.swift +29 -0
  161. package/ios/Tests/GenerationMemoTests.swift +121 -0
  162. package/ios/Tests/InertStyleDiagnosticTests.swift +209 -0
  163. package/ios/Tests/JsonModelsTests.swift +164 -1
  164. package/ios/Tests/LRUCacheTests.swift +110 -0
  165. package/ios/Tests/MediaIntentCriteriaTests.swift +2 -2
  166. package/ios/Tests/MediaLoaderTests.swift +12 -48
  167. package/ios/Tests/ObserverTests.swift +88 -0
  168. package/ios/Tests/PlaybackErrorTests.swift +76 -0
  169. package/ios/Tests/PlaybackFailureTests.swift +189 -0
  170. package/ios/Tests/PlaybackPersistenceTests.swift +93 -0
  171. package/ios/Tests/PlayerPolicyTests.swift +420 -0
  172. package/ios/Tests/{PlaybackCoordinatorTests.swift → PlayerTests.swift} +300 -248
  173. package/ios/Tests/PlayingStateTests.swift +42 -0
  174. package/ios/Tests/QueueManagerTests.swift +196 -115
  175. package/ios/Tests/ReadyValueTests.swift +73 -0
  176. package/ios/Tests/RemoteCommandControllerTests.swift +152 -0
  177. package/ios/Tests/RemoteCommandTests.swift +65 -0
  178. package/ios/Tests/RequestLayerTests.swift +112 -0
  179. package/ios/Tests/ResolvedTrackCopyingTests.swift +39 -35
  180. package/ios/Tests/RetryLoopTests.swift +270 -0
  181. package/ios/Tests/SearchDrillInTests.swift +16 -16
  182. package/ios/Tests/SectionPresentationTests.swift +98 -0
  183. package/ios/Tests/SectionScopeTests.swift +87 -55
  184. package/ios/Tests/StyleResolverTests.swift +132 -0
  185. package/ios/Tests/Support/TestTypes.swift +108 -104
  186. package/ios/Tests/TabBarEntriesTests.swift +19 -19
  187. package/ios/Tests/TimedMetadataAVFoundationTests.swift +42 -0
  188. package/ios/Tests/TopTemplateReplacerTests.swift +172 -0
  189. package/ios/Tests/TrackIdTemplateTests.swift +67 -0
  190. package/ios/Tests/TrackIdentityTests.swift +20 -0
  191. package/ios/Tests/TrackSelectorTests.swift +168 -33
  192. package/ios/Tests/URLErrorConnectivityTests.swift +20 -0
  193. package/ios/TrackSelector.swift +46 -31
  194. package/ios/Util/ArtworkImageFetcher.swift +58 -1
  195. package/ios/Util/Emitter.swift +42 -0
  196. package/ios/Util/GenerationMemo.swift +36 -0
  197. package/ios/Util/LRUCache.swift +23 -159
  198. package/ios/Util/NetworkMonitor.swift +43 -26
  199. package/ios/Util/ReadyValue.swift +56 -0
  200. package/ios/Util/SFSymbolRenderer.swift +59 -31
  201. package/ios/Util/SVGProcessor.swift +9 -12
  202. package/lib/commonjs/AudioBrowser.js +8 -8
  203. package/lib/commonjs/AudioBrowser.js.map +1 -1
  204. package/lib/commonjs/CLAUDE.md +52 -0
  205. package/lib/commonjs/features/browser-config.js +12 -8
  206. package/lib/commonjs/features/browser-config.js.map +1 -1
  207. package/lib/commonjs/features/browser.js +11 -3
  208. package/lib/commonjs/features/browser.js.map +1 -1
  209. package/lib/commonjs/features/carConnection.js +1 -1
  210. package/lib/commonjs/features/equalizer.js +1 -1
  211. package/lib/commonjs/features/errors.js +3 -3
  212. package/lib/commonjs/features/favorites.js +14 -11
  213. package/lib/commonjs/features/favorites.js.map +1 -1
  214. package/lib/commonjs/features/gate.js +16 -1
  215. package/lib/commonjs/features/gate.js.map +1 -1
  216. package/lib/commonjs/features/metadata.js +20 -3
  217. package/lib/commonjs/features/metadata.js.map +1 -1
  218. package/lib/commonjs/features/network.js +1 -1
  219. package/lib/commonjs/features/nowPlaying.js +1 -1
  220. package/lib/commonjs/features/output.js +1 -1
  221. package/lib/commonjs/features/playback/controls.js +4 -2
  222. package/lib/commonjs/features/playback/controls.js.map +1 -1
  223. package/lib/commonjs/features/playback/playWhenReady.js +1 -1
  224. package/lib/commonjs/features/playback/playing.js +1 -1
  225. package/lib/commonjs/features/playback/progress.js +3 -2
  226. package/lib/commonjs/features/playback/progress.js.map +1 -1
  227. package/lib/commonjs/features/playback/rate.js +8 -1
  228. package/lib/commonjs/features/playback/rate.js.map +1 -1
  229. package/lib/commonjs/features/playback/state.js +3 -4
  230. package/lib/commonjs/features/playback/state.js.map +1 -1
  231. package/lib/commonjs/features/playback/trackPlaybackTime.js +0 -6
  232. package/lib/commonjs/features/playback/trackPlaybackTime.js.map +1 -1
  233. package/lib/commonjs/features/playback/volume.js +1 -1
  234. package/lib/commonjs/features/player/options.js +48 -34
  235. package/lib/commonjs/features/player/options.js.map +1 -1
  236. package/lib/commonjs/features/player/setup.js +18 -9
  237. package/lib/commonjs/features/player/setup.js.map +1 -1
  238. package/lib/commonjs/features/player/validateOptions.js +23 -0
  239. package/lib/commonjs/features/player/validateOptions.js.map +1 -0
  240. package/lib/commonjs/features/queue/activeTrack.js +15 -4
  241. package/lib/commonjs/features/queue/activeTrack.js.map +1 -1
  242. package/lib/commonjs/features/queue/queue.js +13 -6
  243. package/lib/commonjs/features/queue/queue.js.map +1 -1
  244. package/lib/commonjs/features/queue/repeatMode.js +1 -1
  245. package/lib/commonjs/features/queue/shuffle.js +1 -1
  246. package/lib/commonjs/features/remoteControls.js +13 -92
  247. package/lib/commonjs/features/remoteControls.js.map +1 -1
  248. package/lib/commonjs/features/sleepTimer.js +13 -1
  249. package/lib/commonjs/features/sleepTimer.js.map +1 -1
  250. package/lib/commonjs/test-utils/reactNativeStub.js +65 -0
  251. package/lib/commonjs/test-utils/reactNativeStub.js.map +1 -0
  252. package/lib/commonjs/utils/LazyNativeEmitter.js +28 -14
  253. package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
  254. package/lib/commonjs/utils/NativeUpdatedValue.js +31 -13
  255. package/lib/commonjs/utils/NativeUpdatedValue.js.map +1 -1
  256. package/lib/commonjs/utils/getTrackIdentity.js +24 -0
  257. package/lib/commonjs/utils/getTrackIdentity.js.map +1 -0
  258. package/lib/commonjs/utils/useNativeValueRefreshedBy.js +33 -0
  259. package/lib/commonjs/utils/useNativeValueRefreshedBy.js.map +1 -0
  260. package/lib/commonjs/web/NativeAudioBrowser.js +201 -76
  261. package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
  262. package/lib/commonjs/web/TrackPlayer/LoadAttempt.js +37 -0
  263. package/lib/commonjs/web/TrackPlayer/LoadAttempt.js.map +1 -0
  264. package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js +18 -2
  265. package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js.map +1 -1
  266. package/lib/commonjs/web/TrackPlayer/Player.js +254 -39
  267. package/lib/commonjs/web/TrackPlayer/Player.js.map +1 -1
  268. package/lib/commonjs/web/TrackPlayer/PlayingStateFactory.js +1 -1
  269. package/lib/commonjs/web/TrackPlayer/QueueManager.js +14 -0
  270. package/lib/commonjs/web/TrackPlayer/QueueManager.js.map +1 -1
  271. package/lib/commonjs/web/TrackPlayer/QueuePlayer.js +33 -12
  272. package/lib/commonjs/web/TrackPlayer/QueuePlayer.js.map +1 -1
  273. package/lib/commonjs/web/browser/BrowserManager.js +175 -100
  274. package/lib/commonjs/web/browser/BrowserManager.js.map +1 -1
  275. package/lib/commonjs/web/browser/FavoriteManager.js +30 -21
  276. package/lib/commonjs/web/browser/FavoriteManager.js.map +1 -1
  277. package/lib/commonjs/web/browser/NavigationErrorManager.js +3 -1
  278. package/lib/commonjs/web/browser/NavigationErrorManager.js.map +1 -1
  279. package/lib/commonjs/web/browser/SearchManager.js +18 -23
  280. package/lib/commonjs/web/browser/SearchManager.js.map +1 -1
  281. package/lib/commonjs/web/browser/assertBrowsePageShape.js +2 -2
  282. package/lib/commonjs/web/browser/assertBrowsePageShape.js.map +1 -1
  283. package/lib/commonjs/web/browser/classifyTrackNavigation.js +9 -9
  284. package/lib/commonjs/web/browser/classifyTrackNavigation.js.map +1 -1
  285. package/lib/commonjs/web/browser/sections.js +89 -0
  286. package/lib/commonjs/web/browser/sections.js.map +1 -0
  287. package/lib/commonjs/web/http/RequestConfigBuilder.js +110 -27
  288. package/lib/commonjs/web/http/RequestConfigBuilder.js.map +1 -1
  289. package/lib/commonjs/web/http/followMediaRedirect.js +219 -0
  290. package/lib/commonjs/web/http/followMediaRedirect.js.map +1 -0
  291. package/lib/commonjs/web/player/NowPlayingManager.js +4 -3
  292. package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
  293. package/lib/commonjs/web/util/BrowserPathHelper.js +62 -11
  294. package/lib/commonjs/web/util/BrowserPathHelper.js.map +1 -1
  295. package/lib/module/AudioBrowser.js +2 -1
  296. package/lib/module/AudioBrowser.js.map +1 -1
  297. package/lib/module/CLAUDE.md +52 -0
  298. package/lib/module/features/browser-config.js +12 -8
  299. package/lib/module/features/browser-config.js.map +1 -1
  300. package/lib/module/features/browser.js +11 -3
  301. package/lib/module/features/browser.js.map +1 -1
  302. package/lib/module/features/carConnection.js +1 -1
  303. package/lib/module/features/equalizer.js +1 -1
  304. package/lib/module/features/errors.js +3 -3
  305. package/lib/module/features/favorites.js +14 -11
  306. package/lib/module/features/favorites.js.map +1 -1
  307. package/lib/module/features/gate.js +16 -1
  308. package/lib/module/features/gate.js.map +1 -1
  309. package/lib/module/features/metadata.js +20 -3
  310. package/lib/module/features/metadata.js.map +1 -1
  311. package/lib/module/features/network.js +1 -1
  312. package/lib/module/features/nowPlaying.js +1 -1
  313. package/lib/module/features/output.js +1 -1
  314. package/lib/module/features/playback/controls.js +4 -2
  315. package/lib/module/features/playback/controls.js.map +1 -1
  316. package/lib/module/features/playback/playWhenReady.js +1 -1
  317. package/lib/module/features/playback/playing.js +1 -1
  318. package/lib/module/features/playback/progress.js +3 -2
  319. package/lib/module/features/playback/progress.js.map +1 -1
  320. package/lib/module/features/playback/rate.js +8 -1
  321. package/lib/module/features/playback/rate.js.map +1 -1
  322. package/lib/module/features/playback/state.js +3 -4
  323. package/lib/module/features/playback/state.js.map +1 -1
  324. package/lib/module/features/playback/trackPlaybackTime.js +0 -5
  325. package/lib/module/features/playback/trackPlaybackTime.js.map +1 -1
  326. package/lib/module/features/playback/volume.js +1 -1
  327. package/lib/module/features/player/options.js +47 -32
  328. package/lib/module/features/player/options.js.map +1 -1
  329. package/lib/module/features/player/setup.js +17 -8
  330. package/lib/module/features/player/setup.js.map +1 -1
  331. package/lib/module/features/player/validateOptions.js +19 -0
  332. package/lib/module/features/player/validateOptions.js.map +1 -0
  333. package/lib/module/features/queue/activeTrack.js +15 -4
  334. package/lib/module/features/queue/activeTrack.js.map +1 -1
  335. package/lib/module/features/queue/queue.js +13 -6
  336. package/lib/module/features/queue/queue.js.map +1 -1
  337. package/lib/module/features/queue/repeatMode.js +1 -1
  338. package/lib/module/features/queue/shuffle.js +1 -1
  339. package/lib/module/features/remoteControls.js +13 -92
  340. package/lib/module/features/remoteControls.js.map +1 -1
  341. package/lib/module/features/sleepTimer.js +13 -1
  342. package/lib/module/features/sleepTimer.js.map +1 -1
  343. package/lib/module/test-utils/reactNativeStub.js +58 -0
  344. package/lib/module/test-utils/reactNativeStub.js.map +1 -0
  345. package/lib/module/utils/LazyNativeEmitter.js +28 -14
  346. package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
  347. package/lib/module/utils/NativeUpdatedValue.js +31 -13
  348. package/lib/module/utils/NativeUpdatedValue.js.map +1 -1
  349. package/lib/module/utils/getTrackIdentity.js +20 -0
  350. package/lib/module/utils/getTrackIdentity.js.map +1 -0
  351. package/lib/module/utils/useNativeValueRefreshedBy.js +29 -0
  352. package/lib/module/utils/useNativeValueRefreshedBy.js.map +1 -0
  353. package/lib/module/web/NativeAudioBrowser.js +201 -76
  354. package/lib/module/web/NativeAudioBrowser.js.map +1 -1
  355. package/lib/module/web/TrackPlayer/LoadAttempt.js +32 -0
  356. package/lib/module/web/TrackPlayer/LoadAttempt.js.map +1 -0
  357. package/lib/module/web/TrackPlayer/PlaybackStateMachine.js +18 -2
  358. package/lib/module/web/TrackPlayer/PlaybackStateMachine.js.map +1 -1
  359. package/lib/module/web/TrackPlayer/Player.js +254 -39
  360. package/lib/module/web/TrackPlayer/Player.js.map +1 -1
  361. package/lib/module/web/TrackPlayer/PlayingStateFactory.js +1 -1
  362. package/lib/module/web/TrackPlayer/QueueManager.js +14 -0
  363. package/lib/module/web/TrackPlayer/QueueManager.js.map +1 -1
  364. package/lib/module/web/TrackPlayer/QueuePlayer.js +33 -12
  365. package/lib/module/web/TrackPlayer/QueuePlayer.js.map +1 -1
  366. package/lib/module/web/browser/BrowserManager.js +175 -100
  367. package/lib/module/web/browser/BrowserManager.js.map +1 -1
  368. package/lib/module/web/browser/FavoriteManager.js +31 -21
  369. package/lib/module/web/browser/FavoriteManager.js.map +1 -1
  370. package/lib/module/web/browser/NavigationErrorManager.js +3 -1
  371. package/lib/module/web/browser/NavigationErrorManager.js.map +1 -1
  372. package/lib/module/web/browser/SearchManager.js +18 -23
  373. package/lib/module/web/browser/SearchManager.js.map +1 -1
  374. package/lib/module/web/browser/assertBrowsePageShape.js +2 -2
  375. package/lib/module/web/browser/assertBrowsePageShape.js.map +1 -1
  376. package/lib/module/web/browser/classifyTrackNavigation.js +9 -9
  377. package/lib/module/web/browser/classifyTrackNavigation.js.map +1 -1
  378. package/lib/module/web/browser/sections.js +83 -0
  379. package/lib/module/web/browser/sections.js.map +1 -0
  380. package/lib/module/web/http/RequestConfigBuilder.js +110 -27
  381. package/lib/module/web/http/RequestConfigBuilder.js.map +1 -1
  382. package/lib/module/web/http/followMediaRedirect.js +215 -0
  383. package/lib/module/web/http/followMediaRedirect.js.map +1 -0
  384. package/lib/module/web/player/NowPlayingManager.js +4 -3
  385. package/lib/module/web/player/NowPlayingManager.js.map +1 -1
  386. package/lib/module/web/util/BrowserPathHelper.js +62 -11
  387. package/lib/module/web/util/BrowserPathHelper.js.map +1 -1
  388. package/lib/typescript/package.json +1 -0
  389. package/lib/typescript/src/AudioBrowser.d.ts +1 -1
  390. package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
  391. package/lib/typescript/src/features/browser-config.d.ts.map +1 -1
  392. package/lib/typescript/src/features/browser.d.ts +10 -2
  393. package/lib/typescript/src/features/browser.d.ts.map +1 -1
  394. package/lib/typescript/src/features/carConnection.d.ts +1 -1
  395. package/lib/typescript/src/features/equalizer.d.ts +1 -1
  396. package/lib/typescript/src/features/errors.d.ts +3 -3
  397. package/lib/typescript/src/features/favorites.d.ts +14 -11
  398. package/lib/typescript/src/features/favorites.d.ts.map +1 -1
  399. package/lib/typescript/src/features/gate.d.ts +18 -2
  400. package/lib/typescript/src/features/gate.d.ts.map +1 -1
  401. package/lib/typescript/src/features/metadata.d.ts +20 -3
  402. package/lib/typescript/src/features/metadata.d.ts.map +1 -1
  403. package/lib/typescript/src/features/network.d.ts +1 -1
  404. package/lib/typescript/src/features/nowPlaying.d.ts +1 -1
  405. package/lib/typescript/src/features/output.d.ts +1 -1
  406. package/lib/typescript/src/features/playback/controls.d.ts +4 -2
  407. package/lib/typescript/src/features/playback/controls.d.ts.map +1 -1
  408. package/lib/typescript/src/features/playback/playWhenReady.d.ts +1 -1
  409. package/lib/typescript/src/features/playback/playing.d.ts +1 -1
  410. package/lib/typescript/src/features/playback/progress.d.ts +1 -1
  411. package/lib/typescript/src/features/playback/progress.d.ts.map +1 -1
  412. package/lib/typescript/src/features/playback/rate.d.ts +8 -1
  413. package/lib/typescript/src/features/playback/rate.d.ts.map +1 -1
  414. package/lib/typescript/src/features/playback/state.d.ts +3 -4
  415. package/lib/typescript/src/features/playback/state.d.ts.map +1 -1
  416. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts +1 -1
  417. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts.map +1 -1
  418. package/lib/typescript/src/features/playback/volume.d.ts +1 -1
  419. package/lib/typescript/src/features/player/options.d.ts +108 -56
  420. package/lib/typescript/src/features/player/options.d.ts.map +1 -1
  421. package/lib/typescript/src/features/player/setup.d.ts +14 -4
  422. package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
  423. package/lib/typescript/src/features/player/validateOptions.d.ts +2 -0
  424. package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -0
  425. package/lib/typescript/src/features/queue/activeTrack.d.ts +9 -2
  426. package/lib/typescript/src/features/queue/activeTrack.d.ts.map +1 -1
  427. package/lib/typescript/src/features/queue/queue.d.ts +7 -4
  428. package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
  429. package/lib/typescript/src/features/queue/repeatMode.d.ts +1 -1
  430. package/lib/typescript/src/features/queue/shuffle.d.ts +1 -1
  431. package/lib/typescript/src/features/remoteControls.d.ts +23 -22
  432. package/lib/typescript/src/features/remoteControls.d.ts.map +1 -1
  433. package/lib/typescript/src/features/sleepTimer.d.ts +1 -1
  434. package/lib/typescript/src/features/sleepTimer.d.ts.map +1 -1
  435. package/lib/typescript/src/test-utils/reactNativeStub.d.ts +36 -0
  436. package/lib/typescript/src/test-utils/reactNativeStub.d.ts.map +1 -0
  437. package/lib/typescript/src/types/browser-native.d.ts +3 -2
  438. package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
  439. package/lib/typescript/src/types/browser-nodes.d.ts +387 -126
  440. package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
  441. package/lib/typescript/src/types/browser.d.ts +93 -61
  442. package/lib/typescript/src/types/browser.d.ts.map +1 -1
  443. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +19 -16
  444. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
  445. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts +24 -19
  446. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts.map +1 -1
  447. package/lib/typescript/src/utils/getTrackIdentity.d.ts +18 -0
  448. package/lib/typescript/src/utils/getTrackIdentity.d.ts.map +1 -0
  449. package/lib/typescript/src/utils/resolveAssetSource.d.ts +1 -2
  450. package/lib/typescript/src/utils/resolveAssetSource.d.ts.map +1 -1
  451. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts +21 -0
  452. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts.map +1 -0
  453. package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +1 -2
  454. package/nitrogen/generated/android/AudioBrowserOnLoad.cpp +4 -2
  455. package/nitrogen/generated/android/c++/JAndroidAudioContentType.hpp +2 -2
  456. package/nitrogen/generated/android/c++/JAndroidAudioOffloadSettings.hpp +2 -2
  457. package/nitrogen/generated/android/c++/JAndroidOptions.hpp +12 -12
  458. package/nitrogen/generated/android/c++/JAndroidPlayerWakeMode.hpp +2 -2
  459. package/nitrogen/generated/android/c++/JAppKilledPlaybackBehavior.hpp +2 -2
  460. package/nitrogen/generated/android/c++/JArtworkRendering.hpp +58 -0
  461. package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +10 -5
  462. package/nitrogen/generated/android/c++/JArtworkVariants.hpp +2 -2
  463. package/nitrogen/generated/android/c++/JBatteryOptimizationStatus.hpp +2 -2
  464. package/nitrogen/generated/android/c++/JBatteryOptimizationStatusChangedEvent.hpp +2 -2
  465. package/nitrogen/generated/android/c++/JBatteryWarningPendingChangedEvent.hpp +2 -2
  466. package/nitrogen/generated/android/c++/JBrowseError.hpp +2 -2
  467. package/nitrogen/generated/android/c++/JBrowseResult.hpp +19 -7
  468. package/nitrogen/generated/android/c++/JBrowserSourceCallbackParam.hpp +2 -2
  469. package/nitrogen/generated/android/c++/JCarPlayNowPlayingButton.hpp +2 -2
  470. package/nitrogen/generated/android/c++/JCarPlayPlayingIndicatorLocation.hpp +58 -0
  471. package/nitrogen/generated/android/c++/JCarPlaySiriListButtonPosition.hpp +2 -2
  472. package/nitrogen/generated/android/c++/JCardImage.hpp +58 -0
  473. package/nitrogen/generated/android/c++/JChapterMetadata.hpp +2 -2
  474. package/nitrogen/generated/android/c++/JEqualizerSettings.hpp +10 -10
  475. package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +10 -5
  476. package/nitrogen/generated/android/c++/JFormatNavigationErrorParams.hpp +11 -5
  477. package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +10 -5
  478. package/nitrogen/generated/android/c++/JFormattedNavigationError.hpp +2 -2
  479. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___MediaTransformParams.hpp +2 -2
  480. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___RequestConfig_std__optional_std__unordered_map_std__string__std__string__.hpp +9 -9
  481. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +10 -5
  482. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_FormattedNavigationError____FormatNavigationErrorParams.hpp +4 -2
  483. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +10 -5
  484. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +10 -5
  485. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_GateDecision_____NativeGateRequest.hpp +2 -2
  486. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____MediaTransformParams.hpp +2 -2
  487. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____RequestConfig_std__optional_std__unordered_map_std__string__std__string__.hpp +9 -9
  488. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +10 -5
  489. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_TransformableRequestConfig____.hpp +2 -2
  490. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +18 -12
  491. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +10 -4
  492. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__string__.hpp +102 -0
  493. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.hpp +18 -6
  494. package/nitrogen/generated/android/c++/JFunc_void.hpp +2 -2
  495. package/nitrogen/generated/android/c++/JFunc_void_BatteryOptimizationStatusChangedEvent.hpp +2 -2
  496. package/nitrogen/generated/android/c++/JFunc_void_BatteryWarningPendingChangedEvent.hpp +2 -2
  497. package/nitrogen/generated/android/c++/JFunc_void_EqualizerSettings.hpp +2 -2
  498. package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +10 -5
  499. package/nitrogen/generated/android/c++/JFunc_void_GateEvent.hpp +2 -2
  500. package/nitrogen/generated/android/c++/JFunc_void_NavigationErrorEvent.hpp +2 -2
  501. package/nitrogen/generated/android/c++/JFunc_void_NowPlayingMetadata.hpp +2 -2
  502. package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +9 -12
  503. package/nitrogen/generated/android/c++/JFunc_void_Output.hpp +2 -2
  504. package/nitrogen/generated/android/c++/JFunc_void_Playback.hpp +2 -2
  505. package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +10 -5
  506. package/nitrogen/generated/android/c++/JFunc_void_PlaybackErrorEvent.hpp +2 -2
  507. package/nitrogen/generated/android/c++/JFunc_void_PlaybackPlayWhenReadyChangedEvent.hpp +2 -2
  508. package/nitrogen/generated/android/c++/JFunc_void_PlaybackProgressUpdatedEvent.hpp +2 -2
  509. package/nitrogen/generated/android/c++/JFunc_void_PlaybackQueueEndedEvent.hpp +2 -2
  510. package/nitrogen/generated/android/c++/JFunc_void_PlayingState.hpp +2 -2
  511. package/nitrogen/generated/android/c++/JFunc_void_RemoteJumpBackwardEvent.hpp +2 -2
  512. package/nitrogen/generated/android/c++/JFunc_void_RemoteJumpForwardEvent.hpp +2 -2
  513. package/nitrogen/generated/android/c++/JFunc_void_RemotePlayIdEvent.hpp +2 -2
  514. package/nitrogen/generated/android/c++/JFunc_void_RemotePlaySearchEvent.hpp +2 -2
  515. package/nitrogen/generated/android/c++/JFunc_void_RemoteSeekEvent.hpp +2 -2
  516. package/nitrogen/generated/android/c++/JFunc_void_RemoteSkipEvent.hpp +2 -2
  517. package/nitrogen/generated/android/c++/JFunc_void_RepeatModeChangedEvent.hpp +2 -2
  518. package/nitrogen/generated/android/c++/JFunc_void_TimedMetadata.hpp +2 -2
  519. package/nitrogen/generated/android/c++/JFunc_void_TrackMetadata.hpp +2 -2
  520. package/nitrogen/generated/android/c++/JFunc_void_bool.hpp +2 -2
  521. package/nitrogen/generated/android/c++/JFunc_void_double.hpp +2 -2
  522. package/nitrogen/generated/android/c++/JFunc_void_std__optional_FormattedNavigationError_.hpp +2 -2
  523. package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +17 -5
  524. package/nitrogen/generated/android/c++/JFunc_void_std__optional_std__variant_nitro__NullType__SleepTimerTime__SleepTimerEndOfTrack__.hpp +2 -2
  525. package/nitrogen/generated/android/c++/JFunc_void_std__string.hpp +2 -2
  526. package/nitrogen/generated/android/c++/JFunc_void_std__vector_ChapterMetadata_.hpp +16 -16
  527. package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +24 -18
  528. package/nitrogen/generated/android/c++/JGate.hpp +2 -2
  529. package/nitrogen/generated/android/c++/JGateDecision.hpp +2 -2
  530. package/nitrogen/generated/android/c++/JGateEvent.hpp +2 -2
  531. package/nitrogen/generated/android/c++/JGateReason.hpp +2 -2
  532. package/nitrogen/generated/android/c++/JGridTile.hpp +64 -0
  533. package/nitrogen/generated/android/c++/JHttpMethod.hpp +2 -2
  534. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +77 -53
  535. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.hpp +3 -3
  536. package/nitrogen/generated/android/c++/JIOSCategory.hpp +2 -2
  537. package/nitrogen/generated/android/c++/JIOSCategoryMode.hpp +2 -2
  538. package/nitrogen/generated/android/c++/JIOSCategoryOptions.hpp +2 -2
  539. package/nitrogen/generated/android/c++/JIOSCategoryPolicy.hpp +2 -2
  540. package/nitrogen/generated/android/c++/JIOSOptions.hpp +17 -11
  541. package/nitrogen/generated/android/c++/JImageContext.hpp +2 -2
  542. package/nitrogen/generated/android/c++/JImageQueryParams.hpp +2 -2
  543. package/nitrogen/generated/android/c++/JImageShape.hpp +58 -0
  544. package/nitrogen/generated/android/c++/JImageSource.hpp +2 -2
  545. package/nitrogen/generated/android/c++/JMediaReference.hpp +2 -2
  546. package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +10 -5
  547. package/nitrogen/generated/android/c++/JMediaTransformParams.hpp +2 -2
  548. package/nitrogen/generated/android/c++/JNativeAndroidSetupOptions.hpp +2 -2
  549. package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +48 -21
  550. package/nitrogen/generated/android/c++/JNativeGateRequest.hpp +2 -2
  551. package/nitrogen/generated/android/c++/JNativeIOSSetupOptions.hpp +10 -10
  552. package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +16 -4
  553. package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +17 -14
  554. package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +9 -12
  555. package/nitrogen/generated/android/c++/JNavigationError.hpp +2 -2
  556. package/nitrogen/generated/android/c++/JNavigationErrorEvent.hpp +2 -2
  557. package/nitrogen/generated/android/c++/JNavigationErrorKind.hpp +58 -0
  558. package/nitrogen/generated/android/c++/JNavigationErrorType.hpp +2 -2
  559. package/nitrogen/generated/android/c++/JNitroAndroidUpdateOptions.hpp +12 -12
  560. package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +17 -11
  561. package/nitrogen/generated/android/c++/JNowPlayingMetadata.hpp +2 -2
  562. package/nitrogen/generated/android/c++/JNowPlayingUpdate.hpp +2 -2
  563. package/nitrogen/generated/android/c++/JOptions.hpp +9 -12
  564. package/nitrogen/generated/android/c++/JOutput.hpp +2 -2
  565. package/nitrogen/generated/android/c++/JOutputType.hpp +2 -2
  566. package/nitrogen/generated/android/c++/JPlayback.hpp +2 -2
  567. package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +10 -5
  568. package/nitrogen/generated/android/c++/JPlaybackError.hpp +2 -2
  569. package/nitrogen/generated/android/c++/JPlaybackErrorEvent.hpp +2 -2
  570. package/nitrogen/generated/android/c++/JPlaybackErrorKind.hpp +2 -2
  571. package/nitrogen/generated/android/c++/JPlaybackPlayWhenReadyChangedEvent.hpp +2 -2
  572. package/nitrogen/generated/android/c++/JPlaybackProgressUpdatedEvent.hpp +2 -2
  573. package/nitrogen/generated/android/c++/JPlaybackQueueEndedEvent.hpp +2 -2
  574. package/nitrogen/generated/android/c++/JPlaybackState.hpp +2 -2
  575. package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +7 -13
  576. package/nitrogen/generated/android/c++/JPlayingState.hpp +2 -2
  577. package/nitrogen/generated/android/c++/JProgress.hpp +2 -2
  578. package/nitrogen/generated/android/c++/{JNotificationButton.hpp → JRemoteButton.hpp} +19 -19
  579. package/nitrogen/generated/android/c++/JRemoteButtonLayout.hpp +86 -0
  580. package/nitrogen/generated/android/c++/JRemoteJumpBackwardEvent.hpp +2 -2
  581. package/nitrogen/generated/android/c++/JRemoteJumpForwardEvent.hpp +2 -2
  582. package/nitrogen/generated/android/c++/JRemotePlayIdEvent.hpp +2 -2
  583. package/nitrogen/generated/android/c++/JRemotePlaySearchEvent.hpp +2 -2
  584. package/nitrogen/generated/android/c++/JRemoteSeekEvent.hpp +2 -2
  585. package/nitrogen/generated/android/c++/JRemoteSkipEvent.hpp +2 -2
  586. package/nitrogen/generated/android/c++/JRepeatMode.hpp +2 -2
  587. package/nitrogen/generated/android/c++/JRepeatModeChangedEvent.hpp +2 -2
  588. package/nitrogen/generated/android/c++/JRequestConfig.hpp +2 -2
  589. package/nitrogen/generated/android/c++/JResolvedTrack.hpp +67 -59
  590. package/nitrogen/generated/android/c++/JRetryConfig.hpp +2 -2
  591. package/nitrogen/generated/android/c++/JSearchMode.hpp +2 -2
  592. package/nitrogen/generated/android/c++/JSearchParams.hpp +2 -2
  593. package/nitrogen/generated/android/c++/JSection.hpp +116 -0
  594. package/nitrogen/generated/android/c++/JSectionStyle.hpp +96 -0
  595. package/nitrogen/generated/android/c++/JSleepTimer.hpp +4 -4
  596. package/nitrogen/generated/android/c++/JSleepTimerEndOfTrack.hpp +2 -2
  597. package/nitrogen/generated/android/c++/JSleepTimerTime.hpp +2 -2
  598. package/nitrogen/generated/android/c++/JStallReason.hpp +2 -2
  599. package/nitrogen/generated/android/c++/JStyleDisplay.hpp +58 -0
  600. package/nitrogen/generated/android/c++/JTimedMetadata.hpp +2 -2
  601. package/nitrogen/generated/android/c++/JTrack.hpp +27 -48
  602. package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +18 -12
  603. package/nitrogen/generated/android/c++/JTrackMetadata.hpp +2 -2
  604. package/nitrogen/generated/android/c++/JTrackRequest.hpp +2 -2
  605. package/nitrogen/generated/android/c++/JTrackStyle.hpp +47 -19
  606. package/nitrogen/generated/android/c++/JTransformableRequestConfig.hpp +2 -2
  607. package/nitrogen/generated/android/c++/JVariant_Boolean_AndroidAudioOffloadSettings.hpp +3 -3
  608. package/nitrogen/generated/android/c++/JVariant_Boolean_RetryConfig.hpp +3 -3
  609. package/nitrogen/generated/android/c++/JVariant_NullType_Double.hpp +3 -3
  610. package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.cpp +26 -0
  611. package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.hpp +75 -0
  612. package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +20 -8
  613. package/nitrogen/generated/android/c++/JVariant_String_ArtworkVariants.hpp +3 -3
  614. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidAudioOffloadSettings.kt +15 -0
  615. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidOptions.kt +20 -3
  616. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoritesMatchMode.kt → ArtworkRendering.kt} +5 -5
  617. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ArtworkRequestConfig.kt +37 -0
  618. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ArtworkVariants.kt +15 -0
  619. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/BatteryOptimizationStatusChangedEvent.kt +13 -0
  620. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/BatteryWarningPendingChangedEvent.kt +13 -0
  621. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/BrowseError.kt +13 -0
  622. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/BrowseResult.kt +16 -7
  623. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/BrowserSourceCallbackParam.kt +15 -0
  624. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CarPlayPlayingIndicatorLocation.kt +23 -0
  625. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CardImage.kt +23 -0
  626. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ChapterMetadata.kt +19 -0
  627. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/EqualizerSettings.kt +27 -0
  628. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FavoriteChangedEvent.kt +15 -0
  629. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FormatNavigationErrorParams.kt +25 -3
  630. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FormatNowPlayingParams.kt +21 -0
  631. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FormattedNavigationError.kt +15 -0
  632. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_RequestConfig___MediaTransformParams.kt +0 -2
  633. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_RequestConfig___RequestConfig_std__optional_std__unordered_map_std__string__std__string__.kt +0 -2
  634. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_RequestConfig___Track.kt +0 -2
  635. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__optional_FormattedNavigationError____FormatNavigationErrorParams.kt +0 -2
  636. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.kt +0 -2
  637. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__optional_std__string____Track.kt +0 -2
  638. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_GateDecision_____NativeGateRequest.kt +0 -2
  639. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____MediaTransformParams.kt +0 -2
  640. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____RequestConfig_std__optional_std__unordered_map_std__string__std__string__.kt +0 -2
  641. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.kt +0 -2
  642. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_TransformableRequestConfig____.kt +0 -2
  643. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.kt +0 -2
  644. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.kt +0 -2
  645. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__string__.kt +78 -0
  646. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.kt +0 -2
  647. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void.kt +0 -2
  648. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_BatteryOptimizationStatusChangedEvent.kt +0 -2
  649. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_BatteryWarningPendingChangedEvent.kt +0 -2
  650. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_EqualizerSettings.kt +0 -2
  651. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_FavoriteChangedEvent.kt +0 -2
  652. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_GateEvent.kt +0 -2
  653. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_NavigationErrorEvent.kt +0 -2
  654. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_NowPlayingMetadata.kt +0 -2
  655. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_Options.kt +0 -2
  656. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_Output.kt +0 -2
  657. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_Playback.kt +0 -2
  658. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlaybackActiveTrackChangedEvent.kt +0 -2
  659. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlaybackErrorEvent.kt +0 -2
  660. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlaybackPlayWhenReadyChangedEvent.kt +0 -2
  661. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlaybackProgressUpdatedEvent.kt +0 -2
  662. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlaybackQueueEndedEvent.kt +0 -2
  663. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlayingState.kt +0 -2
  664. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemoteJumpBackwardEvent.kt +0 -2
  665. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemoteJumpForwardEvent.kt +0 -2
  666. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemotePlayIdEvent.kt +0 -2
  667. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemotePlaySearchEvent.kt +0 -2
  668. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemoteSeekEvent.kt +0 -2
  669. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemoteSkipEvent.kt +0 -2
  670. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RepeatModeChangedEvent.kt +0 -2
  671. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_TimedMetadata.kt +0 -2
  672. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_TrackMetadata.kt +0 -2
  673. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_bool.kt +0 -2
  674. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_double.kt +0 -2
  675. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__optional_FormattedNavigationError_.kt +0 -2
  676. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__optional_ResolvedTrack_.kt +0 -2
  677. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__optional_std__variant_nitro__NullType__SleepTimerTime__SleepTimerEndOfTrack__.kt +0 -2
  678. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__string.kt +0 -2
  679. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__vector_ChapterMetadata_.kt +0 -2
  680. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__vector_Track_.kt +0 -2
  681. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Gate.kt +15 -0
  682. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GateDecision.kt +15 -0
  683. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GateEvent.kt +13 -0
  684. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GridTile.kt +25 -0
  685. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/HybridAudioBrowserSpec.kt +3 -1
  686. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IOSOptions.kt +25 -3
  687. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageContext.kt +15 -0
  688. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageQueryParams.kt +15 -0
  689. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageShape.kt +23 -0
  690. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageSource.kt +19 -0
  691. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/MediaRequestConfig.kt +35 -0
  692. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/MediaTransformParams.kt +15 -0
  693. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeAndroidSetupOptions.kt +31 -0
  694. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +51 -5
  695. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeGateRequest.kt +17 -0
  696. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeIOSSetupOptions.kt +21 -0
  697. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeRouteEntry.kt +27 -0
  698. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeSetupPlayerOptions.kt +29 -0
  699. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeUpdateOptions.kt +23 -0
  700. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NavigationError.kt +19 -0
  701. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NavigationErrorEvent.kt +13 -0
  702. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NavigationErrorKind.kt +23 -0
  703. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroAndroidUpdateOptions.kt +20 -3
  704. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroIOSUpdateOptions.kt +25 -3
  705. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NowPlayingMetadata.kt +29 -0
  706. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NowPlayingUpdate.kt +17 -0
  707. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Options.kt +23 -0
  708. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Output.kt +17 -0
  709. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Playback.kt +15 -0
  710. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackActiveTrackChangedEvent.kt +21 -0
  711. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackError.kt +21 -0
  712. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackErrorEvent.kt +13 -0
  713. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackPlayWhenReadyChangedEvent.kt +13 -0
  714. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackProgressUpdatedEvent.kt +19 -0
  715. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackQueueEndedEvent.kt +15 -0
  716. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayerCapabilities.kt +37 -2
  717. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayingState.kt +15 -0
  718. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Progress.kt +17 -0
  719. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{NotificationButton.kt → RemoteButton.kt} +3 -3
  720. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteButtonLayout.kt +61 -0
  721. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteJumpBackwardEvent.kt +13 -0
  722. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteJumpForwardEvent.kt +13 -0
  723. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemotePlayIdEvent.kt +15 -0
  724. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemotePlaySearchEvent.kt +13 -0
  725. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteSeekEvent.kt +13 -0
  726. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteSkipEvent.kt +13 -0
  727. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RepeatModeChangedEvent.kt +13 -0
  728. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RequestConfig.kt +27 -0
  729. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ResolvedTrack.kt +67 -15
  730. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RetryConfig.kt +17 -0
  731. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SearchParams.kt +27 -0
  732. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Section.kt +66 -0
  733. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SectionStyle.kt +86 -0
  734. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SleepTimer.kt +18 -8
  735. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SleepTimerEndOfTrack.kt +13 -0
  736. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SleepTimerTime.kt +13 -0
  737. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/StyleDisplay.kt +23 -0
  738. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TimedMetadata.kt +21 -0
  739. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +55 -12
  740. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackLoadEvent.kt +17 -0
  741. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackMetadata.kt +43 -0
  742. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackRequest.kt +17 -0
  743. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +58 -5
  744. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TransformableRequestConfig.kt +31 -0
  745. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_AndroidAudioOffloadSettings.kt +16 -7
  746. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_RetryConfig.kt +16 -7
  747. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_NullType_Double.kt +16 -7
  748. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Variant_NullType_NotificationButtonLayout.kt → Variant_NullType_RemoteButtonLayout.kt} +24 -15
  749. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_ResolvedTrack_BrowseError_Promise_BrowseResult_.kt +18 -8
  750. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_String_ArtworkVariants.kt +16 -7
  751. package/nitrogen/generated/ios/AudioBrowser+autolinking.rb +2 -0
  752. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.cpp +38 -29
  753. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +779 -665
  754. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +34 -16
  755. package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +36 -18
  756. package/nitrogen/generated/ios/swift/AndroidOptions.swift +12 -12
  757. package/nitrogen/generated/ios/swift/ArtworkRendering.swift +40 -0
  758. package/nitrogen/generated/ios/swift/BrowseResult.swift +12 -0
  759. package/nitrogen/generated/ios/swift/CarPlayPlayingIndicatorLocation.swift +40 -0
  760. package/nitrogen/generated/ios/swift/CardImage.swift +40 -0
  761. package/nitrogen/generated/ios/swift/FormatNavigationErrorParams.swift +7 -2
  762. package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__string__.swift +54 -0
  763. package/nitrogen/generated/ios/swift/GridTile.swift +48 -0
  764. package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec.swift +1 -1
  765. package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec_cxx.swift +2 -2
  766. package/nitrogen/generated/ios/swift/IOSOptions.swift +7 -2
  767. package/nitrogen/generated/ios/swift/ImageShape.swift +40 -0
  768. package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +52 -5
  769. package/nitrogen/generated/ios/swift/NavigationErrorKind.swift +40 -0
  770. package/nitrogen/generated/ios/swift/NitroAndroidUpdateOptions.swift +12 -12
  771. package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +12 -1
  772. package/nitrogen/generated/ios/swift/PlayerCapabilities.swift +8 -27
  773. package/nitrogen/generated/ios/swift/{NotificationButton.swift → RemoteButton.swift} +6 -6
  774. package/nitrogen/generated/ios/swift/RemoteButtonLayout.swift +57 -0
  775. package/nitrogen/generated/ios/swift/ResolvedTrack.swift +62 -73
  776. package/nitrogen/generated/ios/swift/Section.swift +82 -0
  777. package/nitrogen/generated/ios/swift/SectionStyle.swift +133 -0
  778. package/nitrogen/generated/ios/swift/SleepTimer.swift +13 -0
  779. package/nitrogen/generated/ios/swift/StyleDisplay.swift +40 -0
  780. package/nitrogen/generated/ios/swift/Track.swift +28 -63
  781. package/nitrogen/generated/ios/swift/TrackStyle.swift +86 -22
  782. package/nitrogen/generated/ios/swift/Variant_Bool_AndroidAudioOffloadSettings.swift +12 -0
  783. package/nitrogen/generated/ios/swift/Variant_Bool_RetryConfig.swift +12 -0
  784. package/nitrogen/generated/ios/swift/Variant_NullType_Double.swift +12 -0
  785. package/nitrogen/generated/ios/swift/Variant_NullType_RemoteButtonLayout.swift +30 -0
  786. package/nitrogen/generated/ios/swift/Variant_ResolvedTrack_BrowseError_Promise_BrowseResult_.swift +13 -0
  787. package/nitrogen/generated/ios/swift/Variant_String_ArtworkVariants.swift +12 -0
  788. package/nitrogen/generated/shared/c++/AndroidOptions.hpp +8 -8
  789. package/nitrogen/generated/shared/c++/{FavoritesMatchMode.hpp → ArtworkRendering.hpp} +17 -17
  790. package/nitrogen/generated/shared/c++/CarPlayPlayingIndicatorLocation.hpp +76 -0
  791. package/nitrogen/generated/shared/c++/CardImage.hpp +76 -0
  792. package/nitrogen/generated/shared/c++/FormatNavigationErrorParams.hpp +9 -2
  793. package/nitrogen/generated/shared/c++/GridTile.hpp +84 -0
  794. package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.cpp +1 -1
  795. package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.hpp +1 -1
  796. package/nitrogen/generated/shared/c++/IOSOptions.hpp +9 -2
  797. package/nitrogen/generated/shared/c++/ImageShape.hpp +76 -0
  798. package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +10 -6
  799. package/nitrogen/generated/shared/c++/NavigationErrorKind.hpp +76 -0
  800. package/nitrogen/generated/shared/c++/NitroAndroidUpdateOptions.hpp +8 -8
  801. package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +9 -2
  802. package/nitrogen/generated/shared/c++/PlayerCapabilities.hpp +6 -9
  803. package/nitrogen/generated/shared/c++/{NotificationButton.hpp → RemoteButton.hpp} +19 -19
  804. package/nitrogen/generated/shared/c++/RemoteButtonLayout.hpp +94 -0
  805. package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +33 -37
  806. package/nitrogen/generated/shared/c++/Section.hpp +102 -0
  807. package/nitrogen/generated/shared/c++/SectionStyle.hpp +126 -0
  808. package/nitrogen/generated/shared/c++/StyleDisplay.hpp +76 -0
  809. package/nitrogen/generated/shared/c++/Track.hpp +18 -30
  810. package/nitrogen/generated/shared/c++/TrackStyle.hpp +73 -34
  811. package/package.json +24 -11
  812. package/src/AudioBrowser.ts +2 -1
  813. package/src/CLAUDE.md +52 -0
  814. package/src/features/battery.test.ts +79 -0
  815. package/src/features/browser-config.test.ts +21 -6
  816. package/src/features/browser-config.ts +12 -8
  817. package/src/features/browser.test.ts +1 -1
  818. package/src/features/browser.ts +11 -3
  819. package/src/features/carConnection.ts +1 -1
  820. package/src/features/equalizer.ts +1 -1
  821. package/src/features/errors.ts +3 -3
  822. package/src/features/favorites.ts +14 -11
  823. package/src/features/gate.ts +16 -1
  824. package/src/features/hook-wiring.test.ts +302 -0
  825. package/src/features/metadata.ts +20 -3
  826. package/src/features/network.ts +1 -1
  827. package/src/features/nowPlaying.ts +1 -1
  828. package/src/features/output.ts +1 -1
  829. package/src/features/playback/controls.ts +4 -2
  830. package/src/features/playback/playWhenReady.ts +1 -1
  831. package/src/features/playback/playing.ts +1 -1
  832. package/src/features/playback/progress.test.ts +169 -0
  833. package/src/features/playback/progress.ts +3 -2
  834. package/src/features/playback/rate.ts +8 -1
  835. package/src/features/playback/state.ts +3 -4
  836. package/src/features/playback/trackPlaybackTime.test.ts +16 -6
  837. package/src/features/playback/trackPlaybackTime.ts +1 -6
  838. package/src/features/playback/volume.ts +1 -1
  839. package/src/features/player/options.ts +126 -76
  840. package/src/features/player/setup.test.ts +24 -7
  841. package/src/features/player/setup.ts +22 -10
  842. package/src/features/player/validateOptions.ts +21 -0
  843. package/src/features/queue/activeTrack.ts +15 -4
  844. package/src/features/queue/queue.ts +13 -6
  845. package/src/features/queue/repeatMode.ts +1 -1
  846. package/src/features/queue/shuffle.ts +1 -1
  847. package/src/features/remoteControls.ts +13 -92
  848. package/src/features/sleepTimer.test.ts +172 -0
  849. package/src/features/sleepTimer.ts +9 -1
  850. package/src/nitro-stub-drift.test.ts +277 -0
  851. package/src/public-surface.test.ts +147 -0
  852. package/src/specs/audio-browser.nitro.ts +5 -4
  853. package/src/style-resolution.test.ts +210 -0
  854. package/src/test-utils/reactNativeStub.ts +60 -0
  855. package/src/types/browser-native.ts +7 -3
  856. package/src/types/browser-nodes.ts +403 -134
  857. package/src/types/browser.ts +97 -63
  858. package/src/utils/LazyNativeEmitter.ts +28 -15
  859. package/src/utils/NativeUpdatedValue.ts +29 -13
  860. package/src/utils/getTrackIdentity.ts +20 -0
  861. package/src/utils/useDebug.test.ts +182 -0
  862. package/src/utils/useNativeUpdatedValue.test.ts +103 -0
  863. package/src/utils/useNativeValueRefreshedBy.ts +38 -0
  864. package/src/web/NativeAudioBrowser.playWhenReady.test.ts +232 -2
  865. package/src/web/NativeAudioBrowser.ts +240 -83
  866. package/src/web/TrackPlayer/LoadAttempt.ts +29 -0
  867. package/src/web/TrackPlayer/PlaybackStateMachine.test.ts +60 -13
  868. package/src/web/TrackPlayer/PlaybackStateMachine.ts +22 -6
  869. package/src/web/TrackPlayer/Player.dispatch.test.ts +5 -3
  870. package/src/web/TrackPlayer/Player.load.test.ts +137 -1
  871. package/src/web/TrackPlayer/Player.ts +284 -37
  872. package/src/web/TrackPlayer/PlayingStateFactory.test.ts +1 -1
  873. package/src/web/TrackPlayer/PlayingStateFactory.ts +1 -1
  874. package/src/web/TrackPlayer/QueueManager.test.ts +29 -0
  875. package/src/web/TrackPlayer/QueueManager.ts +14 -0
  876. package/src/web/TrackPlayer/QueuePlayer.ts +33 -14
  877. package/src/web/__tests__/lifecycle.test.ts +345 -0
  878. package/src/web/__tests__/lifecycleHarness.ts +351 -0
  879. package/src/web/__tests__/loadEdgeCases.test.ts +246 -0
  880. package/src/web/__tests__/loadSupersession.test.ts +175 -0
  881. package/src/web/__tests__/retry.test.ts +169 -0
  882. package/src/web/browser/BrowserManager.ts +209 -111
  883. package/src/web/browser/FavoriteManager.test.ts +84 -0
  884. package/src/web/browser/FavoriteManager.ts +28 -23
  885. package/src/web/browser/NavigationErrorManager.ts +3 -1
  886. package/src/web/browser/SearchManager.test.ts +75 -0
  887. package/src/web/browser/SearchManager.ts +25 -28
  888. package/src/web/browser/__tests__/artwork-resolution.test.ts +117 -0
  889. package/src/web/browser/__tests__/layer-resolver.test.ts +35 -0
  890. package/src/web/browser/__tests__/search-artwork.test.ts +46 -0
  891. package/src/web/browser/__tests__/search-navigation.test.ts +76 -0
  892. package/src/web/browser/assertBrowsePageShape.test.ts +1 -1
  893. package/src/web/browser/assertBrowsePageShape.ts +5 -3
  894. package/src/web/browser/classifyTrackNavigation.test.ts +10 -8
  895. package/src/web/browser/classifyTrackNavigation.ts +9 -9
  896. package/src/web/browser/parseSearchResponse.test.ts +8 -2
  897. package/src/web/browser/sections.test.ts +91 -0
  898. package/src/web/browser/sections.ts +72 -0
  899. package/src/web/http/RequestConfigBuilder.test.ts +241 -1
  900. package/src/web/http/RequestConfigBuilder.ts +140 -33
  901. package/src/web/http/followMediaRedirect.test.ts +289 -0
  902. package/src/web/http/followMediaRedirect.ts +253 -0
  903. package/src/web/player/NowPlayingManager.ts +4 -3
  904. package/src/web/util/BrowserPathHelper.test.ts +68 -0
  905. package/src/web/util/BrowserPathHelper.ts +70 -13
  906. package/website/guide/android-auto.md +114 -0
  907. package/website/guide/android-certificates.md +409 -0
  908. package/website/guide/artwork.md +255 -0
  909. package/website/guide/audio-output.md +135 -0
  910. package/website/guide/automotive.md +86 -0
  911. package/website/guide/basic-usage.md +151 -0
  912. package/website/guide/battery.md +115 -0
  913. package/website/guide/browser.md +587 -0
  914. package/website/guide/carplay.md +650 -0
  915. package/website/guide/configuration.md +173 -0
  916. package/website/guide/equalizer.md +231 -0
  917. package/website/guide/errors.md +310 -0
  918. package/website/guide/favorites.md +154 -0
  919. package/website/guide/gate.md +143 -0
  920. package/website/guide/getting-started.md +46 -0
  921. package/website/guide/hooks.md +238 -0
  922. package/website/guide/live-streams.md +125 -0
  923. package/website/guide/metadata.md +239 -0
  924. package/website/guide/migrating-from-track-player.md +244 -0
  925. package/website/guide/native-callback-fetch.md +127 -0
  926. package/website/guide/network.md +86 -0
  927. package/website/guide/now-playing.md +175 -0
  928. package/website/guide/playback.md +328 -0
  929. package/website/guide/queue.md +300 -0
  930. package/website/guide/remote-controls.md +256 -0
  931. package/website/guide/search.md +255 -0
  932. package/website/guide/sleep-timer.md +268 -0
  933. package/website/guide/track.md +247 -0
  934. package/ios/Bridge.h +0 -8
  935. package/ios/Browser/MediaResolveComposer.swift +0 -114
  936. package/ios/CarPlay/ImageRowItem+CarPlay.swift +0 -19
  937. package/ios/Model/MediaURL.swift +0 -67
  938. package/ios/NowPlayingInfo/MediaItemProperty.swift +0 -78
  939. package/ios/NowPlayingInfo/NowPlayingInfoKeyValue.swift +0 -6
  940. package/ios/NowPlayingInfo/NowPlayingInfoProperty.swift +0 -220
  941. package/ios/Observer/PlayerItemNotificationObserver.swift +0 -119
  942. package/ios/Observer/PlayerItemPropertyObserver.swift +0 -133
  943. package/ios/Observer/PlayerStateObserver.swift +0 -50
  944. package/ios/Observer/PlayerTimeObserver.swift +0 -55
  945. package/ios/Option/PitchAlgorithms.swift +0 -17
  946. package/ios/Option/TimeEventFrequency.swift +0 -18
  947. package/ios/Player/LoadSeekCoordinator.swift +0 -99
  948. package/ios/Player/MediaLoaderDelegate.swift +0 -11
  949. package/ios/Player/PlaybackCoordinator.swift +0 -754
  950. package/ios/Player/PlaybackEffectHandler.swift +0 -54
  951. package/ios/Player/PlaybackErrorHandler.swift +0 -175
  952. package/ios/Player/PlaybackStateStore.swift +0 -27
  953. package/ios/Player/PlayingStateManager.swift +0 -33
  954. package/ios/Player/RetryManager.swift +0 -322
  955. package/ios/Player/SeekCompletionHandler.swift +0 -3
  956. package/ios/Player/SleepTimerHandling.swift +0 -15
  957. package/ios/Support/RNTrackPlayer-Bridging-Header.h +0 -7
  958. package/ios/Tests/LoadSeekCoordinatorTests.swift +0 -325
  959. package/ios/Tests/MediaResolveTests.swift +0 -126
  960. package/ios/Tests/OnceValueTests.swift +0 -79
  961. package/ios/Tests/PlaybackErrorHandlerTests.swift +0 -547
  962. package/ios/Tests/PlaybackStateStoreTests.swift +0 -48
  963. package/ios/Tests/PlayingStateManagerTests.swift +0 -57
  964. package/ios/Tests/RetryManagerTests.swift +0 -89
  965. package/ios/TrackPlayer.swift +0 -1014
  966. package/ios/TrackPlayerCallbacks.swift +0 -86
  967. package/ios/Util/MetadataAdapter.swift +0 -131
  968. package/ios/Util/OnceValue.swift +0 -104
  969. package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +0 -58
  970. package/nitrogen/generated/android/c++/JFavoritesMatchMode.hpp +0 -58
  971. package/nitrogen/generated/android/c++/JImageRowItem.hpp +0 -105
  972. package/nitrogen/generated/android/c++/JNotificationButtonLayout.hpp +0 -94
  973. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.cpp +0 -26
  974. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.hpp +0 -71
  975. package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.cpp +0 -26
  976. package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.hpp +0 -75
  977. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FavoriteConfig.kt +0 -38
  978. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageRowItem.kt +0 -68
  979. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NotificationButtonLayout.kt +0 -50
  980. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_FavoriteConfig.kt +0 -53
  981. package/nitrogen/generated/ios/swift/FavoriteConfig.swift +0 -29
  982. package/nitrogen/generated/ios/swift/FavoritesMatchMode.swift +0 -40
  983. package/nitrogen/generated/ios/swift/ImageRowItem.swift +0 -195
  984. package/nitrogen/generated/ios/swift/NotificationButtonLayout.swift +0 -92
  985. package/nitrogen/generated/ios/swift/Variant_Bool_FavoriteConfig.swift +0 -18
  986. package/nitrogen/generated/ios/swift/Variant_NullType_NotificationButtonLayout.swift +0 -18
  987. package/nitrogen/generated/shared/c++/FavoriteConfig.hpp +0 -84
  988. package/nitrogen/generated/shared/c++/ImageRowItem.hpp +0 -129
  989. package/nitrogen/generated/shared/c++/NotificationButtonLayout.hpp +0 -102
@@ -1,134 +1,36 @@
1
1
  import CarPlay
2
2
  import Foundation
3
- import NitroModules
4
3
  import os.log
5
4
 
6
- /// Controller managing CarPlay templates and navigation.
7
- ///
8
- /// Responsibilities:
9
- /// - Creates and manages CPTabBarTemplate from browser tabs
10
- /// - Converts browser content to CPListTemplate for navigation
11
- /// - Handles item selection for playback and navigation
12
- /// - Delegates image loading to CarPlayImageLoader
13
- /// - Delegates Now Playing management to CarPlayNowPlayingManager
14
- ///
15
- /// This class is exposed to Objective-C for use by RNABCarPlaySceneDelegate.
5
+ /// The CarPlay scene's controller, exposed to Objective-C for `RNABCarPlaySceneDelegate`. It
6
+ /// shows a loading root until the player and a configured browser exist, then runs a
7
+ /// `CarPlaySession`; `stop()` ends it.
16
8
  @MainActor
17
9
  @objc(RNABCarPlayController)
18
10
  public final class RNABCarPlayController: NSObject {
19
- // Force the linker to include RNABMediaIntentHandler so NSClassFromString can find it
20
- @objc public static let mediaIntentHandlerClass: AnyClass = RNABMediaIntentHandler.self
11
+ /// The Siri handlers `RNABAudioBrowser.m` looks up by name, referenced so a static link
12
+ /// keeps them.
13
+ @objc public static let siriIntentHandlerClasses: [AnyClass] = [
14
+ RNABMediaIntentHandler.self, RNABMediaAffinityHandler.self, RNABMediaAddHandler.self,
15
+ ]
21
16
 
22
17
  private let logger = Logger(subsystem: "com.audiobrowser", category: "CarPlayController")
23
-
24
18
  private let interfaceController: CPInterfaceController
25
- private weak var audioBrowser: HybridAudioBrowser?
26
- private var trackSelector: TrackSelector?
27
-
28
- /// Track content subscriptions
29
- private var isStarted = false
30
- private var listenerRemovals: [() -> Void] = []
31
-
32
- /// Paths whose loading template has been pushed but not yet appeared. Guards a
33
- /// rapid double-tap from pushing the same destination twice (the first push is
34
- /// async, so `topTemplate` may not reflect it yet). Cleared when it appears.
35
- private var navigatingPaths: Set<String> = []
36
-
37
- /// Templates with a content load in flight. Guards a re-entrant load when a
38
- /// still-loading template re-appears (back-and-forth navigation) from spawning
39
- /// a second resolve + watchdog.
40
- private var loadingTemplates: Set<ObjectIdentifier> = []
41
-
42
- /// Tabs received while the user had templates pushed. Rebuilding the tab bar
43
- /// replaces the root template, which tears down the pushed navigation stack —
44
- /// so the rebuild is deferred until the user is back at the tab bar.
45
- private var pendingTabs: [Track]?
46
-
47
- /// Whether a gate is active. While set, tabs keep their tab-bar entries but
48
- /// each path is resolved per-request via `gateDecision(for:)`: a gated path
49
- /// renders the gate page instead of content; an allowed path shows real
50
- /// content. Mirrors `audioBrowser.isGateActive`; seeded in start() so a gate
51
- /// set before the scene connects renders at connect.
52
- private var isGated = false
53
-
54
- /// Drops a stale tab-bar build when a newer build supersedes it
55
- /// (latest-build-wins). Shared across BOTH the gated and non-gated `showTabBar`
56
- /// paths: each bumps and captures this at entry and bails before any
57
- /// `setRootTemplate` / `updateTemplates` once it's been bumped past their
58
- /// captured value. A build suspends the main actor at its `await` points
59
- /// (per-tab `gateDecision` when gated, first-tab `loadContent` when not), so a
60
- /// gate change can interleave a build of the opposite kind — most importantly a
61
- /// `clearGate` kicking the non-gated build while a gated build is mid-await.
62
- /// Covering both paths with one token means that clear-vs-gated interleave is
63
- /// caught too (not just gated-vs-gated), so a stale gated build can't paint a
64
- /// gate page over just-cleared content. Mirrors `albumArtistGeneration` in
65
- /// `CarPlayNowPlayingManager`.
66
- private var gateBuildGeneration: UInt = 0
67
-
68
- /// How long a browse resolve may run before the destination's loading spinner
69
- /// is replaced with an error state. The selection completion is fired
70
- /// immediately (so CarPlay never blocks the list — per Apple's async handler
71
- /// guidance we push the destination and fill it in), so this only bounds how
72
- /// long the *destination screen* spins. Backing out and re-tapping retries.
73
- private let resolveTimeout: Duration = .seconds(15)
74
-
75
- /// Helper object for CPInterfaceControllerDelegate conformance
76
19
  private var interfaceDelegate: InterfaceControllerDelegate?
20
+ private var instanceSubscription: Subscription?
77
21
 
78
- /// Image loading service for CarPlay artwork
79
- private var imageLoader: CarPlayImageLoader?
80
-
81
- /// List item and section factory
82
- private var listItemFactory: CarPlayListItemFactory?
83
-
84
- /// Now Playing template and button management
85
- private let nowPlayingManager: CarPlayNowPlayingManager
86
-
87
- /// Convenience accessor for browser config
88
- private var config: BrowserConfig {
89
- audioBrowser?.browserManager.config ?? BrowserConfig()
90
- }
91
-
92
- /// Whether a list item identifies the currently active (loaded) track.
93
- ///
94
- /// Identity is the stable `Track.id` when both sides carry one — the active
95
- /// track may have been loaded outside the browse tree (the consumer's own
96
- /// `load`/`setQueue`) with a `src` that differs textually from the browse
97
- /// row's (absolute vs relative, extra query params) while being the same
98
- /// item. Exact `src` equality remains the fallback for consumers that don't
99
- /// assign ids.
100
- private func isActiveTrack(id: String?, src: String?) -> Bool {
101
- guard let currentTrack = audioBrowser?.getPlayer()?.currentTrack else { return false }
102
- if let id, !id.isEmpty, let currentId = currentTrack.id, !currentId.isEmpty {
103
- return id == currentId
104
- }
105
- guard let src else { return false }
106
- return currentTrack.src == src
107
- }
108
-
109
- // MARK: - Initialization
22
+ /// The pending or running start; nil while stopped.
23
+ private var run: Task<Void, Never>?
24
+ private(set) var session: CarPlaySession?
110
25
 
111
26
  @objc
112
27
  public init(interfaceController: CPInterfaceController) {
113
28
  self.interfaceController = interfaceController
114
- audioBrowser = HybridAudioBrowser.shared
115
- nowPlayingManager = CarPlayNowPlayingManager(interfaceController: interfaceController)
116
29
  super.init()
117
- nowPlayingManager.listItemFactory = { [weak self] track, handler in
118
- self?.listItemFactory?.createListItem(for: track, handler: handler)
119
- ?? CPListItem(text: track.title, detailText: nil)
120
- }
121
- nowPlayingManager.navigateToUrl = { [weak self] url, title in
122
- Task { @MainActor in await self?.navigateToUrl(url, title: title) }
123
- }
124
30
  }
125
31
 
126
- // MARK: - Lifecycle
127
-
128
- /// Reports CarPlay scene connection to the library (drives the JS-side
129
- /// `isCarPlayConnected` / `onCarPlayConnectedChanged`). Called by
130
- /// RNABCarPlaySceneDelegate on scene connect/disconnect — deliberately not
131
- /// from start()/stop(), which also cycle on a JS runtime reload.
32
+ /// Reports the scene's connection (JS `isCarPlayConnected`). Called on scene connect and
33
+ /// disconnect, not from `start()`/`stop()`, which also cycle on a JS reload.
132
34
  @objc
133
35
  public static func setConnected(_ connected: Bool) {
134
36
  HybridAudioBrowser.setCarPlayConnected(connected)
@@ -136,1147 +38,57 @@ public final class RNABCarPlayController: NSObject {
136
38
 
137
39
  @objc
138
40
  public func start() {
139
- guard !isStarted else { return }
140
- isStarted = true
141
-
41
+ guard run == nil else { return }
142
42
  logger.info("Starting CarPlay controller")
143
43
 
144
- // Set up interface controller delegate for template lifecycle events
145
44
  let delegate = InterfaceControllerDelegate(controller: self)
146
45
  interfaceDelegate = delegate
147
46
  interfaceController.delegate = delegate
148
47
 
149
- // Restart when a new HybridAudioBrowser replaces the shared instance
150
- // (JS runtime reload) — our subscriptions point at the old instance's
151
- // emitters and would otherwise go silent.
152
- let instanceToken = HybridAudioBrowser.instanceChangedEmitter.addListener { [weak self] _ in
153
- Task { @MainActor in
154
- self?.restart()
155
- }
48
+ // A JS reload replaces the bridge, and with it every emitter the session listens to.
49
+ instanceSubscription = HybridAudioBrowser.instanceChangedEmitter.subscribeOnMain { [weak self] in
50
+ self?.restart()
156
51
  }
157
- listenerRemovals.append {
158
- HybridAudioBrowser.instanceChangedEmitter.removeListener(instanceToken)
159
- }
160
-
161
- // Show loading template while waiting
162
- showLoadingTemplate()
163
52
 
164
- // Wait for both browser and player to be ready
165
- Task { @MainActor in
166
- let (browser, _) = await playerAndConfiguredBrowser.wait()
167
- guard self.isStarted else { return }
168
- self.logger.debug("AudioBrowser and player ready, setting up CarPlay")
169
- self.audioBrowser = browser
170
- self.isGated = browser.isGateActive
171
- self.trackSelector = TrackSelector(browserManager: browser.browserManager)
172
-
173
- // Create image loader with CarPlay display traits
174
- self.imageLoader = CarPlayImageLoader(
175
- carTraitCollection: self.interfaceController.carTraitCollection,
176
- browserManager: browser.browserManager,
177
- )
53
+ // Before the config loads, the app's loading title isn't known: the spinner only.
54
+ let browserManager = HybridAudioBrowser.shared?.browserManager
55
+ let loadingTitle = browserManager?.isConfigured == true ? browserManager?.config.carPlayLoadingTitle : nil
56
+ interfaceController.safeSetRoot(
57
+ CPListTemplate.loading(title: nil, path: nil, loadingTitle: loadingTitle), animated: false,
58
+ )
178
59
 
179
- // Create list item factory
180
- let factory = CarPlayListItemFactory(
181
- isActiveTrack: { [weak self] id, src in self?.isActiveTrack(id: id, src: src) ?? false },
182
- onItemSelected: { [weak self] track, completion in
183
- self?.handleItemSelection(track: track, completion: completion)
184
- },
60
+ run = Task { [weak self] in
61
+ guard let (browser, _) = await playerAndConfiguredBrowser.wait(), !Task.isCancelled,
62
+ let self
63
+ else { return }
64
+ let session = CarPlaySession(
65
+ interfaceController: interfaceController, audioBrowser: browser,
185
66
  )
186
- factory.imageLoader = self.imageLoader
187
- self.listItemFactory = factory
188
-
189
- self.setupContentSubscriptions()
190
-
191
- // Wire up now playing manager
192
- self.nowPlayingManager.setup(audioBrowser: browser)
193
-
194
- await self.buildInitialInterface()
67
+ self.session = session
68
+ session.start()
195
69
  }
196
70
  }
197
71
 
198
72
  @objc
199
73
  public func stop() {
200
- guard isStarted else { return }
201
- isStarted = false
202
-
74
+ guard run != nil else { return }
203
75
  logger.info("Stopping CarPlay controller")
204
-
205
- // Remove all emitter listeners
206
- for removal in listenerRemovals {
207
- removal()
208
- }
209
- listenerRemovals.removeAll()
210
-
211
- // Clear config callback
212
- audioBrowser?.browserManager.onConfigChanged = nil
213
- audioBrowser?.browserManager.onFavoritesChanged = nil
214
-
215
- nowPlayingManager.teardown()
216
- listItemFactory = nil
217
-
218
- navigatingPaths.removeAll()
219
- loadingTemplates.removeAll()
220
- pendingTabs = nil
76
+ run?.cancel()
77
+ run = nil
78
+ session?.end()
79
+ session = nil
80
+ instanceSubscription = nil
221
81
  }
222
82
 
223
- /// Full stop/start cycle. Used when the JS runtime reloads: the new
224
- /// HybridAudioBrowser instance replaces the emitters this controller is
225
- /// subscribed to and resets the readiness gate, so everything must be
226
- /// re-subscribed against the live instance (start() waits for it).
227
- @MainActor
228
83
  private func restart() {
229
- guard isStarted else { return }
84
+ guard run != nil else { return }
230
85
  logger.info("AudioBrowser instance changed — restarting CarPlay controller")
231
86
  stop()
232
87
  start()
233
88
  }
234
-
235
- // MARK: - Content Subscriptions
236
-
237
- private func setupContentSubscriptions() {
238
- guard let audioBrowser else {
239
- logger.warning("AudioBrowser not available for CarPlay")
240
- return
241
- }
242
-
243
- // Subscribe to tab changes
244
- let tabsToken = audioBrowser.tabsChangedEmitter.addListener { [weak self] tabs in
245
- Task { @MainActor in
246
- self?.handleTabsChanged(tabs)
247
- }
248
- }
249
- listenerRemovals.append { [weak audioBrowser] in
250
- audioBrowser?.tabsChangedEmitter.removeListener(tabsToken)
251
- }
252
-
253
- // Subscribe to content changes
254
- let contentToken = audioBrowser.contentChangedEmitter.addListener { [weak self] content in
255
- Task { @MainActor in
256
- self?.handleContentChanged(content)
257
- }
258
- }
259
- listenerRemovals.append { [weak audioBrowser] in
260
- audioBrowser?.contentChangedEmitter.removeListener(contentToken)
261
- }
262
-
263
- // Subscribe to config changes (for Now Playing buttons and per-track
264
- // button state — e.g. resolveAlbumUrl appearing/disappearing)
265
- audioBrowser.browserManager.onConfigChanged = { [weak self] _ in
266
- Task { @MainActor in
267
- self?.nowPlayingManager.setupNowPlayingButtons()
268
- self?.nowPlayingManager.updateNowPlayingButtonStates()
269
- }
270
- }
271
-
272
- // Now-playing buttons + Up Next are runtime-updatable via updateOptions();
273
- // refresh them when player options change (configureBrowser is no longer
274
- // the only path that can change them).
275
- let optionsToken = audioBrowser.playerOptionsChangedEmitter.addListener { [weak self] in
276
- Task { @MainActor in
277
- self?.nowPlayingManager.setupNowPlayingButtons()
278
- self?.nowPlayingManager.updateNowPlayingButtonStates()
279
- }
280
- }
281
- listenerRemovals.append { [weak audioBrowser] in
282
- audioBrowser?.playerOptionsChangedEmitter.removeListener(optionsToken)
283
- }
284
-
285
- // Refresh the Now Playing heart when favorites change externally (app /
286
- // webview). The favorite/active-track emitters only fire for the player's
287
- // own toggles, so without this an in-app favorite leaves the CarPlay heart
288
- // stale.
289
- audioBrowser.browserManager.onFavoritesChanged = { [weak self] in
290
- Task { @MainActor in
291
- self?.nowPlayingManager.updateFavoriteButtonState()
292
- }
293
- }
294
-
295
- // Subscribe to favorite changes (for Now Playing button)
296
- let favoriteToken = audioBrowser.favoriteChangedEmitter.addListener { [weak self] _ in
297
- Task { @MainActor in
298
- self?.nowPlayingManager.updateFavoriteButtonState()
299
- }
300
- }
301
- listenerRemovals.append { [weak audioBrowser] in
302
- audioBrowser?.favoriteChangedEmitter.removeListener(favoriteToken)
303
- }
304
-
305
- // Repeat/shuffle mode changes re-stamp the now-playing buttons' serialized
306
- // `isSelected` (a rebuild, skipped when unchanged) so CarPlay re-renders
307
- // draw the correct selected background on the first frame — see
308
- // CarPlayNowPlayingManager.BuiltButtonRow.
309
- let repeatToken = audioBrowser.repeatModeChangedEmitter.addListener { [weak self] _ in
310
- Task { @MainActor in
311
- self?.nowPlayingManager.setupNowPlayingButtons()
312
- }
313
- }
314
- listenerRemovals.append { [weak audioBrowser] in
315
- audioBrowser?.repeatModeChangedEmitter.removeListener(repeatToken)
316
- }
317
- let shuffleToken = audioBrowser.shuffleChangedEmitter.addListener { [weak self] _ in
318
- Task { @MainActor in
319
- self?.nowPlayingManager.setupNowPlayingButtons()
320
- }
321
- }
322
- listenerRemovals.append { [weak audioBrowser] in
323
- audioBrowser?.shuffleChangedEmitter.removeListener(shuffleToken)
324
- }
325
-
326
- // Subscribe to external content changes (notifyContentChanged /
327
- // invalidateAllContent).
328
- let externalContentToken = audioBrowser.externalContentChangedEmitter.addListener { [weak self] path in
329
- // Hop to the main actor like the sibling listeners — the emitter runs
330
- // listeners synchronously on the emitting (JS) thread.
331
- Task { @MainActor in
332
- if path == HybridAudioBrowser.invalidateAllSentinel {
333
- self?.invalidateAllContent()
334
- } else {
335
- self?.notifyContentChanged(path: path)
336
- }
337
- }
338
- }
339
- listenerRemovals.append { [weak audioBrowser] in
340
- audioBrowser?.externalContentChangedEmitter.removeListener(externalContentToken)
341
- }
342
-
343
- // Subscribe to gate changes (set / in-place update / clear)
344
- let gateToken = audioBrowser.gateChangedEmitter.addListener { [weak self] active in
345
- Task { @MainActor in
346
- self?.handleGateChanged(active)
347
- }
348
- }
349
- listenerRemovals.append { [weak audioBrowser] in
350
- audioBrowser?.gateChangedEmitter.removeListener(gateToken)
351
- }
352
-
353
- // Subscribe to active track changes (for playing indicator in lists)
354
- let activeTrackToken = audioBrowser.activeTrackChangedEmitter.addListener { [weak self] event in
355
- Task { @MainActor in
356
- self?.handleActiveTrackChanged(event)
357
- }
358
- }
359
- listenerRemovals.append { [weak audioBrowser] in
360
- audioBrowser?.activeTrackChangedEmitter.removeListener(activeTrackToken)
361
- }
362
-
363
- // Subscribe to queue changes (for Up Next list updates)
364
- let queueToken = audioBrowser.queueChangedEmitter.addListener { [weak self] tracks in
365
- Task { @MainActor in
366
- self?.nowPlayingManager.handleQueueChanged(tracks)
367
- }
368
- }
369
- listenerRemovals.append { [weak audioBrowser] in
370
- audioBrowser?.queueChangedEmitter.removeListener(queueToken)
371
- }
372
-
373
- // A voice media intent started playback — surface Now Playing so the user
374
- // lands on the playing station (Up Next holds the rest of the results).
375
- let showNowPlayingToken = audioBrowser.showNowPlayingRequestedEmitter.addListener { [weak self] _ in
376
- Task { @MainActor in
377
- self?.nowPlayingManager.showNowPlaying()
378
- }
379
- }
380
- listenerRemovals.append { [weak audioBrowser] in
381
- audioBrowser?.showNowPlayingRequestedEmitter.removeListener(showNowPlayingToken)
382
- }
383
-
384
- // Subscribe to navigation errors (from browser layer)
385
- let navErrorToken = audioBrowser.navigationErrorEmitter.addListener { [weak self] event in
386
- Task { @MainActor in
387
- self?.handleNavigationError(event)
388
- }
389
- }
390
- listenerRemovals.append { [weak audioBrowser] in
391
- audioBrowser?.navigationErrorEmitter.removeListener(navErrorToken)
392
- }
393
- }
394
-
395
- /// Handles navigation errors from the browser layer, displaying them in CarPlay
396
- @MainActor
397
- private func handleNavigationError(_ event: NavigationErrorEvent) {
398
- guard let error = event.error else { return }
399
- logger.warning("Navigation error: \(error.code.stringValue) - \(error.message)")
400
- // Derive the current path from the live top template (authoritative) rather
401
- // than a hand-maintained stack that goes stale on back navigation.
402
- let path = interfaceController.topTemplate.flatMap { getPath(from: $0) } ?? "/"
403
- showNavigationError(error, path: path)
404
- }
405
-
406
- // MARK: - Initial Interface
407
-
408
- @MainActor
409
- private func buildInitialInterface() async {
410
- guard let audioBrowser else {
411
- logger.error("AudioBrowser not available")
412
- await showRootNavigationError(
413
- NavigationError(code: .unknownError, message: "", statusCode: nil, statusCodeSuccess: nil),
414
- )
415
- return
416
- }
417
-
418
- // Get tabs from browser manager
419
- let tabs = audioBrowser.browserManager.getTabs()
420
-
421
- if let tabs, !tabs.isEmpty {
422
- await showTabBar(tabs: tabs)
423
- } else {
424
- // No tabs yet - query them
425
- logger.info("No tabs available, querying...")
426
- // Config exists by now (start() waited for it), so re-show the loading
427
- // root to pick up the app's localized loading title — the first loading
428
- // template was created before config was available.
429
- if config.carPlayLoadingTitle != nil {
430
- showLoadingTemplate()
431
- }
432
- do {
433
- let queriedTabs = try await audioBrowser.browserManager.queryTabs()
434
- if !queriedTabs.isEmpty {
435
- await showTabBar(tabs: queriedTabs)
436
- } else {
437
- // Empty is a navigation error (.emptyContent) so it formats through
438
- // the app's formatNavigationError like every other failure (ADR 0001).
439
- await showRootNavigationError(
440
- NavigationError(code: .emptyContent, message: "", statusCode: nil, statusCodeSuccess: nil),
441
- )
442
- }
443
- } catch {
444
- logger.error("Failed to query tabs: \(error.localizedDescription)")
445
- await showRootNavigationError(NavigationError.from(error))
446
- }
447
- }
448
- }
449
-
450
- /// Shows a loading template as root while waiting for initialization. Before
451
- /// the browser is configured this can only show the spinner (iOS 18.4+) — the
452
- /// app's `carPlayLoadingTitle` isn't known yet; `buildInitialInterface`
453
- /// re-shows it once config is available.
454
- private func showLoadingTemplate() {
455
- let template = makeLoadingTemplate(title: nil, path: nil)
456
- interfaceController.safeSetRoot(template, animated: false)
457
- }
458
-
459
- // MARK: - Tab Bar
460
-
461
- @MainActor
462
- private func showTabBar(tabs: [Track]) async {
463
- // Serialize concurrent tab-bar builds across BOTH the gated and non-gated
464
- // paths with one shared generation token: bump at entry, capture, and bail
465
- // before any template mutation once a newer build supersedes us. Both paths
466
- // suspend the main actor at `await` points (per-tab `gateDecision` when
467
- // gated, first-tab `loadContent` when not), so a gate change can interleave
468
- // a build of the opposite kind — a `clearGate` kicking the non-gated build
469
- // while a gated build is mid-await, or vice versa. A single token covers the
470
- // clear-vs-gated interleave too, not just gated-vs-gated: the superseding
471
- // build bumps it, and the in-flight build bails before painting (preventing
472
- // a stale gate page over just-cleared content, or a clobbered root template).
473
- gateBuildGeneration &+= 1
474
- let generation = gateBuildGeneration
475
-
476
- // While gated, each tab path is resolved per-request: a gated tab renders
477
- // the gate page with that request's chrome; an allowed tab shows real
478
- // content. This generalizes the old "all tabs show the gate" — a path the
479
- // resolver allows now shows real content.
480
- if isGated {
481
- await showGatedTabBar(tabs: tabs, generation: generation)
482
- return
483
- }
484
-
485
- logger.info("Building tab bar with \(tabs.count) tabs")
486
-
487
- let maxTabs = CPTabBarTemplate.maximumTabCount
488
-
489
- // Create tab templates synchronously (empty shells) - don't block on content loading
490
- let tabTemplates: [CPListTemplate] = tabs.prefix(maxTabs).map { tab in
491
- createTabTemplate(for: tab)
492
- }
493
-
494
- // A newer build (e.g. a gate raised mid-clear) superseded us — don't clobber
495
- // its root template.
496
- guard gateBuildGeneration == generation else { return }
497
-
498
- // Set the tab bar immediately so UI appears fast
499
- logger.info("Setting tab bar root template with \(tabTemplates.count) templates")
500
- let tabBar = CPTabBarTemplate(templates: tabTemplates)
501
- interfaceController.safeSetRoot(tabBar, animated: true)
502
-
503
- // Load content for the first tab only - others load lazily when selected
504
- if let firstTemplate = tabTemplates.first, let firstTab = tabs.first, let url = firstTab.url {
505
- await loadContent(for: url, into: firstTemplate)
506
- }
507
- }
508
-
509
- /// Builds the tab bar while a gate is active. Each tab's path is run through
510
- /// the single gate choke point (`gateDecision(for:)`): a gated tab becomes a
511
- /// gate page carrying that decision's chrome (and fires `onGate`); an allowed
512
- /// tab becomes a normal content shell that lazy-loads. With a static gate (no
513
- /// resolver) every tab is gated with the default chrome — today's behaviour.
514
- ///
515
- /// `generation` is the shared `gateBuildGeneration` captured by the calling
516
- /// `showTabBar`; we bail before any template mutation once a newer build (of
517
- /// either kind) bumps it past `generation`.
518
- @MainActor
519
- private func showGatedTabBar(tabs: [Track], generation: UInt) async {
520
- guard let audioBrowser else { return }
521
-
522
- guard !tabs.isEmpty else {
523
- // Tabs unknown (config not loaded yet, or none) — resolve the root path
524
- // and show a single gate page (or, if allowed, fall back to a normal
525
- // build once tabs arrive).
526
- let outcome = await audioBrowser.gateDecision(
527
- for: NativeGateRequest(reason: .browse, path: nil, search: nil),
528
- )
529
- guard gateBuildGeneration == generation else { return } // superseded by a newer build
530
- if outcome.gated {
531
- audioBrowser.onGate(GateEvent(reason: .browse))
532
- interfaceController.safeSetRoot(
533
- makeGateTemplate(gate: outcome.chrome, tab: nil), animated: true,
534
- )
535
- }
536
- return
537
- }
538
-
539
- let limitedTabs = Array(tabs.prefix(CPTabBarTemplate.maximumTabCount))
540
- var templates: [CPListTemplate] = []
541
- for tab in limitedTabs {
542
- let outcome = await audioBrowser.gateDecision(
543
- for: NativeGateRequest(reason: .browse, path: tab.url, search: nil),
544
- )
545
- guard gateBuildGeneration == generation else { return } // superseded by a newer build
546
- if outcome.gated {
547
- audioBrowser.onGate(GateEvent(reason: .browse))
548
- templates.append(makeGateTemplate(gate: outcome.chrome, tab: tab))
549
- } else {
550
- // This path is allowed even though a gate is active — show real content.
551
- templates.append(createTabTemplate(for: tab))
552
- }
553
- }
554
-
555
- if let tabBar = interfaceController.rootTemplate as? CPTabBarTemplate,
556
- tabBar.templates.count == templates.count
557
- {
558
- // Equal-count in-place swap keeps the selected tab index.
559
- tabBar.updateTemplates(templates)
560
- } else {
561
- interfaceController.safeSetRoot(
562
- CPTabBarTemplate(templates: templates), animated: true,
563
- )
564
- }
565
-
566
- // Eagerly fill any allowed (content) tabs: templateDidAppear isn't
567
- // guaranteed to re-fire for templates swapped in via updateTemplates.
568
- for (tab, template) in zip(limitedTabs, templates) {
569
- guard getPath(from: template) != nil, let url = tab.url else { continue }
570
- await loadContent(for: url, into: template)
571
- }
572
- }
573
-
574
- /// Creates a tab template shell without loading content (synchronous).
575
- /// The shell carries the loading state (spinner / `carPlayLoadingTitle`)
576
- /// until its content lazy-loads on first appearance.
577
- private func createTabTemplate(for track: Track) -> CPListTemplate {
578
- // The path stored on the template drives lazy loading and refresh.
579
- let template = makeLoadingTemplate(title: track.title, path: track.url)
580
- applyTabBarEntry(to: template, for: track)
581
- return template
582
- }
583
-
584
- /// Stamps a template's tab-bar entry: title and image (default, SF Symbol,
585
- /// or loaded artwork). Shared by content tabs and gate-page tabs so a gate
586
- /// keeps the familiar tab bar.
587
- private func applyTabBarEntry(to template: CPTemplate, for track: Track) {
588
- // Set tab title explicitly (required for tab bar display)
589
- template.tabTitle = track.title
590
-
591
- // Set tab image only when the tab supplies one (SF Symbol / artwork /
592
- // artworkSource). A tab with no icon stays title-only — CPTemplate renders
593
- // fine with just `tabTitle`. Tab bar icons are 24pt x 24pt per the CarPlay
594
- // Developer Guide: https://developer.apple.com/download/files/CarPlay-Developer-Guide.pdf
595
- if let artwork = track.artwork?.url, SFSymbolRenderer.isSFSymbol(artwork) {
596
- let (symbolName, _, _) = SFSymbolRenderer.parseArtwork(artwork)
597
- if let image = imageLoader?.sfSymbolImage(symbolName) {
598
- template.tabImage = image
599
- }
600
- } else if track.artwork != nil || track.artworkSource != nil {
601
- // loadArtwork handles both artwork and artworkSource
602
- let tabImageSize = CGSize(width: 24, height: 24)
603
- imageLoader?.loadArtwork(for: track, size: tabImageSize) { [weak template] image in
604
- Task { @MainActor in
605
- if let image {
606
- template?.tabImage = image
607
- }
608
- }
609
- }
610
- } else {
611
- // No icon supplied: clear any prior image so a restamp (handleTabsChanged)
612
- // of an existing template doesn't leave a stale icon on a now title-only tab.
613
- template.tabImage = nil
614
- }
615
- }
616
-
617
- // MARK: - List Templates
618
-
619
- /// Finds the path associated with a template, if any
620
- private func getPath(from template: CPTemplate) -> String? {
621
- (template.userInfo as? [String: Any])?["path"] as? String
622
- }
623
-
624
- /// Updates a template's sections and assistant cell from resolved content.
625
- private func updateTemplate(_ template: CPListTemplate, with resolvedTrack: ResolvedTrack) {
626
- let sections = listItemFactory?.createSections(from: resolvedTrack) ?? []
627
- template.updateSections(sections)
628
- configureAssistantCell(on: template, from: resolvedTrack)
629
- }
630
-
631
- /// Configures the assistant cell ("Ask Siri to Play Audio") on a template
632
- /// based on the `carPlaySiriListButton` property of the resolved content.
633
- private func configureAssistantCell(on template: CPListTemplate, from resolvedTrack: ResolvedTrack) {
634
- guard let position = resolvedTrack.carPlaySiriListButton else {
635
- template.assistantCellConfiguration = nil
636
- return
637
- }
638
- let cellPosition: CPListItem.AssistantCellPosition = position == .top ? .top : .bottom
639
- template.assistantCellConfiguration = CPAssistantCellConfiguration(
640
- position: cellPosition,
641
- visibility: .always,
642
- assistantAction: .playMedia,
643
- )
644
- }
645
-
646
- // MARK: - Content Loading
647
-
648
- /// Resolves `path` and fills an empty (loading) template. Driven by
649
- /// `templateDidAppear` → `loadContentIfNeeded`, so it runs once the template is
650
- /// on screen — the timing where CarPlay actually applies updates (updates made
651
- /// earlier, e.g. straight after push, are dropped). Shows a centered empty
652
- /// state on empty/error/timeout.
653
- @MainActor
654
- private func loadContent(for path: String, into template: CPListTemplate) async {
655
- guard let audioBrowser else { return }
656
-
657
- // Single-flight: don't start a second load if one is already in flight for
658
- // this template (e.g. the user backed out and into a still-loading screen).
659
- let templateId = ObjectIdentifier(template)
660
- guard !loadingTemplates.contains(templateId) else { return }
661
- loadingTemplates.insert(templateId)
662
- defer { loadingTemplates.remove(templateId) }
663
-
664
- // Watchdog: if still loading (no rows) after the timeout, show an error.
665
- let timeout = resolveTimeout
666
- let watchdog = Task { @MainActor [weak self] in
667
- do { try await Task.sleep(for: timeout) } catch { return } // cancelled → abort
668
- guard let self, template.sections.isEmpty else { return }
669
- self.logger.error("loadContent: resolve timed out for \(path)")
670
- // A timeout is a navigation error (code .timeout) too, so it formats through
671
- // the app's formatNavigationError like every other browse failure.
672
- let timedOut = NavigationError(
673
- code: .timeout, message: "", statusCode: nil, statusCodeSuccess: nil,
674
- )
675
- await self.showNavigationErrorView(timedOut, path: path, on: template)
676
- }
677
-
678
- do {
679
- let resolved = try await audioBrowser.browserManager.resolve(path, useCache: true)
680
- watchdog.cancel()
681
- // A page carrying an assistant cell (carPlaySiriListButton) is meaningful
682
- // even with no rows — the "Ask Siri to Play" cell IS its content — so it
683
- // renders rather than falling through to the empty-content state.
684
- let hasAssistantCell = resolved.carPlaySiriListButton != nil
685
- if resolved.children?.isEmpty ?? true, !hasAssistantCell {
686
- // Empty is modeled as a navigation error (code .emptyContent) so it goes
687
- // through the same path-aware formatter as failures — letting an app give
688
- // an empty Favorites tab different copy than an empty search. ADR 0001.
689
- let empty = NavigationError(
690
- code: .emptyContent, message: "", statusCode: nil, statusCodeSuccess: nil,
691
- )
692
- await showNavigationErrorView(empty, path: path, on: template)
693
- } else {
694
- updateTemplate(template, with: resolved)
695
- }
696
- } catch {
697
- watchdog.cancel()
698
- logger.error("Failed to load content for \(path): \(error.localizedDescription)")
699
- await showNavigationErrorView(NavigationError.from(error), path: path, on: template)
700
- }
701
- }
702
-
703
- /// Renders a navigation error as the template's centered empty/error view,
704
- /// formatted via the app's `formatNavigationError` (or the built-in default):
705
- /// `title → view title`, `message → subtitle`. Used for both real failures and
706
- /// the empty-content case (ADR 0001).
707
- @MainActor
708
- private func showNavigationErrorView(
709
- _ navError: NavigationError,
710
- path: String,
711
- on template: CPListTemplate,
712
- ) async {
713
- let formatted = await formattedNavigationError(navError, path: path)
714
- showMessage(
715
- on: template,
716
- title: formatted.title,
717
- subtitle: formatted.message.flatMap { $0.isEmpty ? nil : $0 },
718
- )
719
- }
720
-
721
- /// Resolves a navigation error to its display form via the app's
722
- /// `formatNavigationError` callback, falling back to the default if unset/failed.
723
- @MainActor
724
- private func formattedNavigationError(
725
- _ navError: NavigationError,
726
- path: String,
727
- ) async -> FormattedNavigationError {
728
- let fallback = navError.defaultFormatted()
729
- guard let formatter = config.formatNavigationError else { return fallback }
730
- let params = FormatNavigationErrorParams(
731
- error: navError, defaultFormatted: fallback, path: path,
732
- )
733
- do {
734
- let custom = try await formatter(params).await()
735
- return custom ?? fallback
736
- } catch {
737
- return fallback
738
- }
739
- }
740
-
741
- // MARK: - Selection Handling
742
-
743
- private func handleItemSelection(track: Track, completion: @escaping () -> Void) {
744
- logger.info("Selected track: \(track.title)")
745
-
746
- guard let audioBrowser else {
747
- completion()
748
- return
749
- }
750
-
751
- // If this track is already loaded, resume playback and show Now Playing.
752
- if track.src != nil, isActiveTrack(id: track.id, src: track.src) {
753
- try? audioBrowser.play()
754
- nowPlayingManager.showNowPlaying()
755
- completion()
756
- return
757
- }
758
-
759
- guard let player = audioBrowser.getPlayer(), let trackSelector else {
760
- completion()
761
- return
762
- }
763
-
764
- // A playable track always lands on the Now Playing surface (selection
765
- // resolves to .play or .intercepted, never .browse) — push it now and stamp
766
- // the tapped track's metadata, instead of after selection resolves: queue
767
- // expansion and the media URL resolve can both hit the network, and waiting
768
- // animates in a blank Now Playing screen. The load pipeline re-publishes
769
- // the same fields and dedupes.
770
- if track.src != nil {
771
- player.loadNowPlayingMetadata(for: track)
772
- // Derive the album line's destination from the tapped track before the
773
- // push — CarPlay reads isAlbumArtistButtonEnabled at display time.
774
- nowPlayingManager.prepareAlbumArtistButton(for: track)
775
- nowPlayingManager.showNowPlaying()
776
- }
777
-
778
- // Release CarPlay immediately so the list never locks up. Apple's handler
779
- // guidance is to finish processing the tap promptly; for a browse we "finish"
780
- // by pushing the destination and filling it in (see navigateToUrl), and for
781
- // playback the Now Playing surface owns its own loading state.
782
- completion()
783
-
784
- Task { [weak self] in
785
- let result = await trackSelector.select(track: track, player: player)
786
- guard let self else { return }
787
- switch result {
788
- case let .play(intent):
789
- self.executePlayback(intent, player: player)
790
- self.nowPlayingManager.showNowPlaying(popToFront: false)
791
- case .intercepted:
792
- self.nowPlayingManager.showNowPlaying(popToFront: false)
793
- case let .browse(url):
794
- await self.navigateToUrl(url, title: track.title)
795
- case .none:
796
- break
797
- }
798
- }
799
- }
800
-
801
- private func executePlayback(_ intent: TrackSelector.PlaybackIntent, player: TrackPlayer) {
802
- switch intent {
803
- case let .skipTo(index):
804
- try? player.skipTo(index, playWhenReady: true)
805
- case let .setQueue(tracks, startIndex, sourcePath):
806
- player.setQueue(tracks, initialIndex: startIndex, playWhenReady: true, sourcePath: sourcePath)
807
- case let .loadTrack(track):
808
- player.load(track, playWhenReady: true)
809
- }
810
- }
811
-
812
- /// Pushes a browsable URL's destination immediately as an empty, spinning list
813
- /// template — so the list the user tapped from is never blocked. The content is
814
- /// filled by `templateDidAppear` → `loadContentIfNeeded` → `loadContent`, which
815
- /// runs once the template is on screen (the timing CarPlay needs: updates made
816
- /// right after a push are dropped). Backing out and re-tapping retries.
817
- @MainActor
818
- private func navigateToUrl(_ url: String, title: String) async {
819
- // The gate is resolved per-request: a gated path pushes the gate page
820
- // (with this request's chrome) instead of browsing into content; this also
821
- // blocks indirect entries like the Now Playing album line. An allowed path
822
- // browses normally even while a gate is active.
823
- if isGated, let audioBrowser {
824
- let outcome = await audioBrowser.gateDecision(
825
- for: NativeGateRequest(reason: .browse, path: url, search: nil),
826
- )
827
- if outcome.gated {
828
- audioBrowser.onGate(GateEvent(reason: .browse))
829
- if let top = interfaceController.topTemplate, getPath(from: top) == url { return }
830
- interfaceController.safePush(
831
- makeGateTemplate(gate: outcome.chrome, tab: nil), animated: true,
832
- )
833
- return
834
- }
835
- }
836
- // Avoid pushing a duplicate if the top template already shows this path.
837
- if let top = interfaceController.topTemplate, getPath(from: top) == url {
838
- return
839
- }
840
- // …and guard a rapid double-tap whose first push hasn't appeared yet (so the
841
- // check above can't see it). Cleared when the pushed template appears.
842
- guard !navigatingPaths.contains(url) else { return }
843
- navigatingPaths.insert(url)
844
-
845
- let template = makeLoadingTemplate(title: title, path: url)
846
- interfaceController.safePush(template, animated: true) { [weak self] pushed, error in
847
- guard !pushed else { return }
848
- // The push can fail (e.g. CarPlay's template stack depth limit). The
849
- // appear callback that normally clears the guard will never fire, so
850
- // clear it here — otherwise the destination stays unreachable until
851
- // CarPlay reconnects.
852
- self?.navigatingPaths.remove(url)
853
- if let error {
854
- self?.logger.error("pushTemplate failed for \(url): \(error.localizedDescription)")
855
- }
856
- }
857
- }
858
-
859
- /// Builds an empty list template for content that is still resolving.
860
- /// On iOS 18.4+ it shows the system spinner (`showsSpinnerWhileEmpty`). On older
861
- /// iOS it shows the app-localized `carPlayLoadingTitle` as the centered empty
862
- /// state when configured; otherwise it stays blank rather than ship a
863
- /// hardcoded, un-localized "Loading…". The empty view is set at creation —
864
- /// the timing CarPlay renders reliably (see `replaceWithMessage`).
865
- private func makeLoadingTemplate(title: String?, path: String?) -> CPListTemplate {
866
- let template = CPListTemplate(title: title, sections: [])
867
- if let path {
868
- template.userInfo = ["path": path] as [String: Any]
869
- }
870
- if let loadingTitle = config.carPlayLoadingTitle {
871
- // Subtitle slot on purpose: the title slot renders at CarPlay's large
872
- // empty-state size, which is too heavy for a transient loading hint.
873
- template.emptyViewSubtitleVariants = [loadingTitle]
874
- }
875
- if #available(iOS 18.4, *) {
876
- template.showsSpinnerWhileEmpty = true
877
- }
878
- return template
879
- }
880
-
881
- /// Shows a centered empty/error message on a loading template. A *pushed*
882
- /// template is replaced (see `replaceWithMessage`); a *tab root* can't be
883
- /// popped, so its empty view is set in place (best-effort — a tab renders its
884
- /// empty view when selected).
885
- private func showMessage(on template: CPListTemplate, title: String, subtitle: String?) {
886
- if isTabRoot(template) {
887
- if #available(iOS 18.4, *) {
888
- template.showsSpinnerWhileEmpty = false
889
- }
890
- template.emptyViewTitleVariants = [title.isEmpty ? "Couldn't load" : title]
891
- template.emptyViewSubtitleVariants = subtitle.map { [$0] } ?? []
892
- template.updateSections([])
893
- } else {
894
- replaceWithMessage(template, title: title, subtitle: subtitle)
895
- }
896
- }
897
-
898
- /// Whether `template` is a tab's root template (a child of the tab bar). Such
899
- /// templates can't be popped, so the replace-by-pop strategy doesn't apply.
900
- private func isTabRoot(_ template: CPTemplate) -> Bool {
901
- guard let tabBar = interfaceController.rootTemplate as? CPTabBarTemplate else { return false }
902
- return tabBar.templates.contains { $0 === template }
903
- }
904
-
905
- /// Replaces an on-screen loading template with a fresh list template showing a
906
- /// centered empty-state message (empty result, or an error).
907
- ///
908
- /// We *replace* rather than mutate because CarPlay reliably renders a list
909
- /// template's empty view only as its **initial** state at push time — changing
910
- /// `emptyViewTitleVariants` / `showsSpinnerWhileEmpty` on an already-pushed
911
- /// template is unreliable (it renders late, or not at all). The replacement
912
- /// carries no `path`, so `loadContentIfNeeded` won't lazy-reload it; backing
913
- /// out and re-tapping retries.
914
- private func replaceWithMessage(_ loadingTemplate: CPListTemplate, title: String, subtitle: String?) {
915
- // Only replace if the loading template is still the visible top template
916
- // (the user may have backed out while the resolve was in flight).
917
- guard interfaceController.topTemplate === loadingTemplate else { return }
918
-
919
- let message = CPListTemplate(title: loadingTemplate.title, sections: [])
920
- message.emptyViewTitleVariants = [title.isEmpty ? "Couldn't load" : title]
921
- message.emptyViewSubtitleVariants = subtitle.map { [$0] } ?? []
922
-
923
- interfaceController.safePop(animated: false) { [weak self] _, _ in
924
- self?.interfaceController.safePush(message, animated: false)
925
- }
926
- }
927
-
928
- // MARK: - Content Change Handlers
929
-
930
- @MainActor
931
- private func handleTabsChanged(_ tabs: [Track]) {
932
- logger.debug("Tabs changed: \(tabs.count) tabs")
933
- // Same tabs by URL while ungated → only the tab-bar entries changed
934
- // (e.g. titles after a locale switch): restamp them in place. No rebuild,
935
- // so the selected tab and any pushed navigation stack survive. (Gate
936
- // templates carry no path, so a gated tab bar never matches here.)
937
- if !isGated,
938
- let tabBar = interfaceController.rootTemplate as? CPTabBarTemplate,
939
- tabBar.templates.count == min(tabs.count, CPTabBarTemplate.maximumTabCount),
940
- zip(tabBar.templates, tabs).allSatisfy({ getPath(from: $0) == $1.url })
941
- {
942
- for (template, tab) in zip(tabBar.templates, tabs) {
943
- applyTabBarEntry(to: template, for: tab)
944
- }
945
- pendingTabs = nil
946
- return
947
- }
948
- // Rebuilding replaces the root template, tearing down any pushed
949
- // navigation stack. If the user is browsing, defer until they're back at
950
- // the tab bar (applied from templateDidAppear).
951
- guard interfaceController.templates.count <= 1 else {
952
- pendingTabs = tabs
953
- return
954
- }
955
- pendingTabs = nil
956
- Task {
957
- await showTabBar(tabs: tabs)
958
- }
959
- }
960
-
961
- /// Applies a deferred tab change once the user has popped back to the tab
962
- /// bar. Returns true if a rebuild was kicked off (the appeared template is
963
- /// about to be replaced, so callers should skip further work on it).
964
- @MainActor
965
- fileprivate func applyPendingTabsIfAtRoot() -> Bool {
966
- guard let tabs = pendingTabs, interfaceController.templates.count <= 1 else {
967
- return false
968
- }
969
- pendingTabs = nil
970
- Task {
971
- await showTabBar(tabs: tabs)
972
- }
973
- return true
974
- }
975
-
976
- @MainActor
977
- private func handleContentChanged(_ content: ResolvedTrack?) {
978
- // This callback fires when the main browser's content changes.
979
- // For CarPlay-specific refreshes (e.g., favorites), use notifyContentChanged instead.
980
- guard let content else { return }
981
- refreshTemplatesForPath(content.url, with: content)
982
- }
983
-
984
- @MainActor
985
- private func handleActiveTrackChanged(_ event: PlaybackActiveTrackChangedEvent) {
986
- logger.debug("handleActiveTrackChanged: \(event.lastTrack?.src ?? "nil") → \(event.track?.src ?? "nil")")
987
- updatePlayingIndicators()
988
- // Refresh per-track button state: favorite heart, Up Next availability,
989
- // and the album line's pre-resolved destination.
990
- nowPlayingManager.handleActiveTrackChanged()
991
- }
992
-
993
- /// Updates the isPlaying state on all list items based on the current active track.
994
- @MainActor
995
- fileprivate func updatePlayingIndicators() {
996
- var templates: [CPListTemplate] = []
997
-
998
- if let tabBar = interfaceController.rootTemplate as? CPTabBarTemplate {
999
- for template in tabBar.templates {
1000
- if let listTemplate = template as? CPListTemplate {
1001
- templates.append(listTemplate)
1002
- }
1003
- }
1004
- }
1005
-
1006
- if let topTemplate = interfaceController.topTemplate as? CPListTemplate,
1007
- !templates.contains(where: { $0 === topTemplate })
1008
- {
1009
- templates.append(topTemplate)
1010
- }
1011
-
1012
- for template in templates {
1013
- var templateChanged = false
1014
- for section in template.sections {
1015
- for item in section.items {
1016
- guard let listItem = item as? CPListItem,
1017
- let info = listItem.carPlayItemInfo,
1018
- info.src != nil
1019
- else { continue }
1020
-
1021
- let isPlaying = isActiveTrack(id: info.id, src: info.src)
1022
- if listItem.isPlaying != isPlaying {
1023
- logger.debug(
1024
- "Updating isPlaying for \(info.id ?? info.src ?? ""): \(listItem.isPlaying) → \(isPlaying)")
1025
- listItem.isPlaying = isPlaying
1026
- templateChanged = true
1027
- }
1028
- }
1029
- }
1030
- // Setting isPlaying on an already-displayed item does not reliably reach
1031
- // the CarPlay screen (rdar-known; the flag only renders when the item is
1032
- // pushed with it already set). Re-pushing the same sections re-serializes
1033
- // every item with its current flags — done only when something actually
1034
- // changed, so routine appear-time passes don't churn the display.
1035
- if templateChanged {
1036
- template.updateSections(template.sections)
1037
- }
1038
- }
1039
- }
1040
-
1041
- // MARK: - Public Content Notification
1042
-
1043
- /// Notifies CarPlay that content at the given path has changed and should be refreshed.
1044
- /// Called from HybridAudioBrowser.notifyContentChanged() to update CarPlay lists.
1045
- ///
1046
- /// - Parameter path: The path where content has changed (e.g., "/favorites")
1047
- @objc
1048
- public func notifyContentChanged(path: String) {
1049
- guard isStarted else { return }
1050
-
1051
- Task { @MainActor in
1052
- await refreshContentForPath(path)
1053
- }
1054
- }
1055
-
1056
- /// Fetches fresh content for a path and updates any matching CarPlay templates.
1057
- @MainActor
1058
- private func refreshContentForPath(_ path: String) async {
1059
- guard let audioBrowser else { return }
1060
-
1061
- logger.debug("Refreshing CarPlay content for path: \(path)")
1062
-
1063
- // Fetch fresh content (bypassing cache since content changed)
1064
- do {
1065
- let resolved = try await audioBrowser.browserManager.resolve(path, useCache: false)
1066
- refreshTemplatesForPath(path, with: resolved)
1067
- } catch {
1068
- logger.error("Failed to refresh content for \(path): \(error.localizedDescription)")
1069
- }
1070
- }
1071
-
1072
- /// Updates all CarPlay templates that are displaying the given path.
1073
- @MainActor
1074
- private func refreshTemplatesForPath(_ path: String, with content: ResolvedTrack) {
1075
- logger.debug("Content changed for path: \(path)")
1076
-
1077
- // Check if the root template is a tab bar and refresh matching tabs
1078
- if let tabBar = interfaceController.rootTemplate as? CPTabBarTemplate {
1079
- for template in tabBar.templates {
1080
- guard let listTemplate = template as? CPListTemplate,
1081
- let templatePath = getPath(from: listTemplate),
1082
- templatePath == path
1083
- else { continue }
1084
-
1085
- logger.info("Refreshing tab template for path: \(path)")
1086
- updateTemplate(listTemplate, with: content)
1087
- }
1088
- }
1089
-
1090
- // Check if any template in the navigation stack matches the changed path
1091
- // The top template is the currently visible one
1092
- if let topTemplate = interfaceController.topTemplate as? CPListTemplate,
1093
- let templatePath = getPath(from: topTemplate),
1094
- templatePath == path
1095
- {
1096
- logger.info("Refreshing top template for path: \(path)")
1097
- updateTemplate(topTemplate, with: content)
1098
- }
1099
- }
1100
-
1101
- /// Refreshes every currently-displayed template (tabs + navigation stack) by
1102
- /// re-fetching each path with the cache bypassed. Called when all content was
1103
- /// invalidated app-wide (e.g. a locale change).
1104
- @MainActor
1105
- public func invalidateAllContent() {
1106
- guard isStarted else { return }
1107
- Task { @MainActor in await refreshAllDisplayedTemplates() }
1108
- }
1109
-
1110
- @MainActor
1111
- private func refreshAllDisplayedTemplates() async {
1112
- guard let audioBrowser else { return }
1113
-
1114
- var templates: [CPListTemplate] = []
1115
- if let tabBar = interfaceController.rootTemplate as? CPTabBarTemplate {
1116
- for template in tabBar.templates {
1117
- if let listTemplate = template as? CPListTemplate {
1118
- templates.append(listTemplate)
1119
- }
1120
- }
1121
- }
1122
- for template in interfaceController.templates {
1123
- if let listTemplate = template as? CPListTemplate,
1124
- !templates.contains(where: { $0 === listTemplate })
1125
- {
1126
- templates.append(listTemplate)
1127
- }
1128
- }
1129
-
1130
- for template in templates {
1131
- guard let path = getPath(from: template) else { continue }
1132
- do {
1133
- let resolved = try await audioBrowser.browserManager.resolve(path, useCache: false)
1134
- updateTemplate(template, with: resolved)
1135
- } catch {
1136
- logger.error("invalidateAllContent: failed to refresh \(path): \(error.localizedDescription)")
1137
- }
1138
- }
1139
- }
1140
-
1141
- // MARK: - Gate
1142
-
1143
- /// Applies a gate state change: set or clear. The per-request chrome is
1144
- /// obtained at render time via `gateDecision(for:)`, so this just tears down
1145
- /// any pushed navigation and rebuilds the tab bar — `showTabBar` routes to the
1146
- /// gated build while a gate is active and to the normal build once cleared.
1147
- /// Custom Now Playing buttons (e.g. favorite) hide while gated and return on
1148
- /// clear.
1149
- @MainActor
1150
- private func handleGateChanged(_ active: Bool) {
1151
- isGated = active
1152
- let tabs = audioBrowser?.browserManager.getTabs() ?? []
1153
- if tabs.isEmpty, !active {
1154
- // Never had tabs (gate was up since before config) — full initial build.
1155
- Task { await buildInitialInterface() }
1156
- } else {
1157
- // Tear down any pushed navigation — gated content must not stay reachable
1158
- // behind the gate, and on clear we return to the root. (No-op at root.)
1159
- interfaceController.safePopToRoot(animated: false)
1160
- Task { await showTabBar(tabs: tabs) }
1161
- }
1162
- nowPlayingManager.setupNowPlayingButtons()
1163
- nowPlayingManager.updateNowPlayingButtonStates()
1164
- }
1165
-
1166
- /// Builds the gate page for one tab-bar slot, carrying the tab's entry so
1167
- /// the tab bar itself stays familiar.
1168
- ///
1169
- /// A CPListTemplate, NOT a CPInformationTemplate: CarPlay enforces
1170
- /// per-entitlement template allowances at runtime, and the audio
1171
- /// entitlement does not include the information template — handing one to
1172
- /// CPTabBarTemplate throws an unhandled ObjC exception (crash at init).
1173
- ///
1174
- /// Within a list, tab children never show navigation-bar buttons (the tab
1175
- /// bar owns that chrome), and the centered empty view renders only when the
1176
- /// list has NO rows. The gate is a plain centered message — `title` as the
1177
- /// empty-view title and `message` as its subtitle, where newlines collapse
1178
- /// to spaces (the "variants" are width alternatives, not lines). No action
1179
- /// button: a gate withholds content; the consumer surfaces any "subscribe"
1180
- /// affordance in its own UI, not on the car surface.
1181
- private func makeGateTemplate(gate gateChrome: Gate?, tab: Track?) -> CPListTemplate {
1182
- // A gated decision with neither an override nor a stored default chrome
1183
- // (resolver-only `true`) falls back to the built-in minimal gate.
1184
- let gate = gateChrome ?? HybridAudioBrowser.builtInGate
1185
- let template = CPListTemplate(title: gate.title, sections: [])
1186
- // Set at creation — the timing CarPlay renders the empty view reliably
1187
- // (see replaceWithMessage).
1188
- template.emptyViewTitleVariants = [gate.title]
1189
- if let message = gate.message, !message.isEmpty {
1190
- template.emptyViewSubtitleVariants = [
1191
- message.replacingOccurrences(of: "\n", with: " "),
1192
- ]
1193
- }
1194
- // Marks the page as a gate (vs. a content tab, which carries a `path`),
1195
- // so the lazy-loader and refresh paths never try to fill it.
1196
- template.userInfo = ["gate": true] as [String: Any]
1197
- if let tab {
1198
- applyTabBarEntry(to: template, for: tab)
1199
- }
1200
- return template
1201
- }
1202
-
1203
- // MARK: - Error Handling
1204
-
1205
- /// Shows a navigation error using CPActionSheetTemplate.
1206
- /// - Parameters:
1207
- /// - error: The NavigationError to display
1208
- /// - path: The path that was being navigated to when the error occurred
1209
- private func showNavigationError(_ error: NavigationError, path: String) {
1210
- let defaultFormatted = error.defaultFormatted()
1211
-
1212
- // Check if custom formatter is configured
1213
- logger.debug("showNavigationError: formatNavigationError is \(self.config.formatNavigationError != nil ? "set" : "nil")")
1214
- if let formatter = config.formatNavigationError {
1215
- logger.debug("Calling formatNavigationError callback...")
1216
- // Call the JS callback and handle result
1217
- let params = FormatNavigationErrorParams(error: error, defaultFormatted: defaultFormatted, path: path)
1218
- formatter(params)
1219
- .then { [weak self] customDisplay in
1220
- self?.logger.debug("formatNavigationError returned: \(String(describing: customDisplay))")
1221
- self?.presentErrorActionSheet(customDisplay: customDisplay ?? defaultFormatted)
1222
- }
1223
- .catch { [weak self] callbackError in
1224
- self?.logger.error("formatNavigationError failed: \(callbackError)")
1225
- // On error, fall back to defaults
1226
- self?.presentErrorActionSheet(customDisplay: defaultFormatted)
1227
- }
1228
- } else {
1229
- presentErrorActionSheet(customDisplay: defaultFormatted)
1230
- }
1231
- }
1232
-
1233
- /// Presents the error action sheet with the given display info.
1234
- /// - Parameter customDisplay: The formatted error to display
1235
- private func presentErrorActionSheet(customDisplay: FormattedNavigationError) {
1236
- // If another template is already presented, dismiss it first
1237
- if interfaceController.presentedTemplate != nil {
1238
- interfaceController.safeDismiss(animated: false) { [weak self] _, _ in
1239
- self?.showErrorActionSheet(customDisplay: customDisplay)
1240
- }
1241
- } else {
1242
- showErrorActionSheet(customDisplay: customDisplay)
1243
- }
1244
- }
1245
-
1246
- /// Actually shows the error action sheet (called after safety checks)
1247
- private func showErrorActionSheet(customDisplay: FormattedNavigationError) {
1248
- // OK action - dismiss the action sheet (use system-localized "OK")
1249
- let okTitle = Bundle(for: UIAlertController.self).localizedString(forKey: "OK", value: "OK", table: nil)
1250
- let ok = CPAlertAction(title: okTitle, style: .cancel) { [weak self] _ in
1251
- self?.interfaceController.safeDismiss(animated: true)
1252
- }
1253
-
1254
- let actionSheet = CPActionSheetTemplate(
1255
- title: customDisplay.title,
1256
- message: customDisplay.message,
1257
- actions: [ok],
1258
- )
1259
-
1260
- interfaceController.safePresent(actionSheet, animated: true)
1261
- }
1262
-
1263
- /// Shows an initialization failure as the root template — a centered empty
1264
- /// view formatted via the app's `formatNavigationError` (path "/"), like every
1265
- /// other browse failure. The empty view renders reliably here because it's the
1266
- /// template's initial state at set-root time.
1267
- @MainActor
1268
- private func showRootNavigationError(_ navError: NavigationError) async {
1269
- let formatted = await formattedNavigationError(navError, path: "/")
1270
- let template = CPListTemplate(title: nil, sections: [])
1271
- template.emptyViewTitleVariants = [formatted.title]
1272
- template.emptyViewSubtitleVariants = formatted.message.flatMap { $0.isEmpty ? nil : [$0] } ?? []
1273
- interfaceController.safeSetRoot(template, animated: true)
1274
- }
1275
89
  }
1276
90
 
1277
- // MARK: - CPInterfaceControllerDelegate
1278
-
1279
- /// Separate delegate class to avoid exposing CPInterfaceControllerDelegate to Obj-C header
91
+ /// Keeps `CPInterfaceControllerDelegate` out of the Objective-C header.
1280
92
  private final class InterfaceControllerDelegate: NSObject, CPInterfaceControllerDelegate {
1281
93
  private weak var controller: RNABCarPlayController?
1282
94
 
@@ -1286,76 +98,10 @@ private final class InterfaceControllerDelegate: NSObject, CPInterfaceController
1286
98
  }
1287
99
 
1288
100
  func templateDidAppear(_ aTemplate: CPTemplate, animated _: Bool) {
1289
- // Popping back from Now Playing surfaces the tab bar (not a list), so the
1290
- // guard must let both through — the playing indicator was toggled while
1291
- // the list was covered, and this appearance is the moment to repaint it.
1292
- guard aTemplate is CPListTemplate || aTemplate is CPTabBarTemplate else { return }
1293
-
1294
- // A tab change deferred while the user was browsing replaces the whole
1295
- // tab bar — no point updating a template that's about to be discarded.
1296
- if controller?.applyPendingTabsIfAtRoot() == true { return }
1297
-
1298
- // Update playing indicators when navigating back to a list template
1299
- controller?.updatePlayingIndicators()
1300
-
1301
- // Lazy load content for tabs that haven't been loaded yet
1302
- if let listTemplate = aTemplate as? CPListTemplate {
1303
- controller?.loadContentIfNeeded(for: listTemplate)
1304
- }
101
+ controller?.session?.templateDidAppear(aTemplate)
1305
102
  }
1306
103
 
1307
104
  func templateDidDisappear(_ aTemplate: CPTemplate, animated _: Bool) {
1308
- // The complement of the appear hook: whether popping Now Playing reports
1309
- // "tab bar appeared" or only "now playing disappeared" is a platform
1310
- // detail — cover both so the revealed list always repaints its indicator.
1311
- guard aTemplate is CPNowPlayingTemplate else { return }
1312
- controller?.updatePlayingIndicators()
1313
- }
1314
- }
1315
-
1316
- // MARK: - Lazy Loading
1317
-
1318
- private extension RNABCarPlayController {
1319
- /// Loads content for a template if it hasn't been loaded yet (lazy loading for tabs)
1320
- func loadContentIfNeeded(for template: CPListTemplate) {
1321
- // Get path from userInfo
1322
- guard let path = getPath(from: template) else { return }
1323
-
1324
- // The template is now on screen, so a queued duplicate navigation to it is
1325
- // no longer a concern (the top-template check will catch any further taps).
1326
- navigatingPaths.remove(path)
1327
-
1328
- // Skip if already has content (single-flight in loadContent guards re-entry).
1329
- guard template.sections.isEmpty else { return }
1330
-
1331
- logger.debug("Lazy loading content for tab: \(path)")
1332
-
1333
- Task {
1334
- await loadContent(for: path, into: template)
1335
- }
1336
- }
1337
- }
1338
-
1339
- // MARK: - UIImage Resize
1340
-
1341
- extension UIImage {
1342
- /// Draws the image centered within the target size, maintaining aspect ratio
1343
- func resized(to targetSize: CGSize) -> UIImage? {
1344
- UIGraphicsBeginImageContextWithOptions(targetSize, false, 0.0)
1345
- defer { UIGraphicsEndImageContext() }
1346
-
1347
- // Scale to fit while maintaining aspect ratio
1348
- let widthRatio = targetSize.width / size.width
1349
- let heightRatio = targetSize.height / size.height
1350
- let scale = min(widthRatio, heightRatio)
1351
-
1352
- let scaledSize = CGSize(width: size.width * scale, height: size.height * scale)
1353
- let origin = CGPoint(
1354
- x: (targetSize.width - scaledSize.width) / 2,
1355
- y: (targetSize.height - scaledSize.height) / 2,
1356
- )
1357
-
1358
- draw(in: CGRect(origin: origin, size: scaledSize))
1359
- return UIGraphicsGetImageFromCurrentImageContext()?.withRenderingMode(.alwaysTemplate)
105
+ controller?.session?.templateDidDisappear(aTemplate)
1360
106
  }
1361
107
  }