react-native-audio-browser 0.1.0 → 0.2.0-next.1

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 (902) hide show
  1. package/AudioBrowser.podspec +1 -1
  2. package/README.md +180 -2
  3. package/android/build.gradle +10 -0
  4. package/android/gradle.properties +1 -1
  5. package/android/src/main/AndroidManifest.xml +15 -0
  6. package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +401 -141
  7. package/android/src/main/java/com/audiobrowser/Callbacks.kt +4 -6
  8. package/android/src/main/java/com/audiobrowser/Service.kt +67 -18
  9. package/android/src/main/java/com/audiobrowser/browser/ArtworkResolutionRegistry.kt +37 -0
  10. package/android/src/main/java/com/audiobrowser/browser/BrowseArtworkRegistry.kt +34 -0
  11. package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +443 -178
  12. package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +300 -0
  13. package/android/src/main/java/com/audiobrowser/browser/JsonArtwork.kt +79 -0
  14. package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +41 -4
  15. package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +43 -0
  16. package/android/src/main/java/com/audiobrowser/browser/TrackLoadHandler.kt +4 -4
  17. package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +35 -0
  18. package/android/src/main/java/com/audiobrowser/http/RequestConfigBuilder.kt +109 -97
  19. package/android/src/main/java/com/audiobrowser/model/PlaybackMetadata.kt +6 -10
  20. package/android/src/main/java/com/audiobrowser/model/PlayerSetupOptions.kt +40 -8
  21. package/android/src/main/java/com/audiobrowser/model/PlayerUpdateOptions.kt +31 -37
  22. package/android/src/main/java/com/audiobrowser/player/AutomaticBufferManager.kt +4 -1
  23. package/android/src/main/java/com/audiobrowser/player/CapabilityControls.kt +119 -0
  24. package/android/src/main/java/com/audiobrowser/player/DynamicLoadControl.kt +18 -1
  25. package/android/src/main/java/com/audiobrowser/player/EqualizerController.kt +153 -0
  26. package/android/src/main/java/com/audiobrowser/player/InterceptingPlayer.kt +141 -0
  27. package/android/src/main/java/com/audiobrowser/player/MediaFactory.kt +15 -3
  28. package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +415 -131
  29. package/android/src/main/java/com/audiobrowser/player/MediaSessionCommandManager.kt +154 -276
  30. package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +348 -0
  31. package/android/src/main/java/com/audiobrowser/player/PlaybackErrorClassifier.kt +129 -0
  32. package/android/src/main/java/com/audiobrowser/player/PlaybackStateMachine.kt +85 -0
  33. package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +27 -5
  34. package/android/src/main/java/com/audiobrowser/player/PlaybackTimer.kt +56 -0
  35. package/android/src/main/java/com/audiobrowser/player/Player.kt +452 -576
  36. package/android/src/main/java/com/audiobrowser/player/PlayerCapabilitiesExt.kt +20 -0
  37. package/android/src/main/java/com/audiobrowser/player/PlayerEngineFactory.kt +130 -0
  38. package/android/src/main/java/com/audiobrowser/player/PlayerListener.kt +190 -67
  39. package/android/src/main/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicy.kt +117 -53
  40. package/android/src/main/java/com/audiobrowser/player/SleepTimer.kt +37 -3
  41. package/android/src/main/java/com/audiobrowser/player/SleepTimerManager.kt +80 -0
  42. package/android/src/main/java/com/audiobrowser/player/StuckRecoveryPolicy.kt +50 -0
  43. package/android/src/main/java/com/audiobrowser/player/TransformingDataSource.kt +84 -34
  44. package/android/src/main/java/com/audiobrowser/player/VolumeFader.kt +81 -0
  45. package/android/src/main/java/com/audiobrowser/tls/AiaCertChaser.kt +141 -0
  46. package/android/src/main/java/com/audiobrowser/tls/AiaChasingTrustManager.kt +37 -0
  47. package/android/src/main/java/com/audiobrowser/tls/AiaTls.kt +41 -0
  48. package/android/src/main/java/com/audiobrowser/tls/CachingCertificateFetcher.kt +209 -0
  49. package/android/src/main/java/com/audiobrowser/util/ArtworkContentProvider.kt +161 -0
  50. package/android/src/main/java/com/audiobrowser/util/ArtworkUris.kt +28 -0
  51. package/android/src/main/java/com/audiobrowser/util/ArtworkVariant.kt +26 -0
  52. package/android/src/main/java/com/audiobrowser/util/BrowserPathHelper.kt +24 -0
  53. package/android/src/main/java/com/audiobrowser/util/CoilArtworkLoader.kt +49 -0
  54. package/android/src/main/java/com/audiobrowser/util/CoilArtworkLoaderHolder.kt +36 -0
  55. package/android/src/main/java/com/audiobrowser/util/CoilBitmapLoader.kt +23 -300
  56. package/android/src/main/java/com/audiobrowser/util/EqualizerManager.kt +13 -23
  57. package/android/src/main/java/com/audiobrowser/util/MediaExtrasBuilder.kt +2 -2
  58. package/android/src/main/java/com/audiobrowser/util/MetadataAdapter.kt +4 -3
  59. package/android/src/main/java/com/audiobrowser/util/OutputMonitor.kt +211 -0
  60. package/android/src/main/java/com/audiobrowser/util/OutputSwitcher.kt +58 -0
  61. package/android/src/main/java/com/audiobrowser/util/PlayingStateFactory.kt +2 -1
  62. package/android/src/main/java/com/audiobrowser/util/RatingFavorites.kt +14 -0
  63. package/android/src/main/java/com/audiobrowser/util/SvgArtworkRenderer.kt +6 -111
  64. package/android/src/main/java/com/audiobrowser/util/SystemVolumeMonitor.kt +22 -8
  65. package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +111 -80
  66. package/android/src/main/res/values/strings.xml +4 -0
  67. package/android/src/test/java/com/audiobrowser/TestFixtures.kt +166 -0
  68. package/android/src/test/java/com/audiobrowser/browser/ArtworkResolutionRegistryTest.kt +46 -0
  69. package/android/src/test/java/com/audiobrowser/browser/BrowseArtworkRegistryTest.kt +39 -0
  70. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerBuildApiRequestTest.kt +114 -0
  71. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerLayerResolutionTest.kt +141 -0
  72. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +4 -0
  73. package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +177 -0
  74. package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +54 -0
  75. package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +96 -0
  76. package/android/src/test/java/com/audiobrowser/browser/SimpleRouterTest.kt +1 -2
  77. package/android/src/test/java/com/audiobrowser/http/RequestConfigBuilderTest.kt +72 -0
  78. package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +159 -0
  79. package/android/src/test/java/com/audiobrowser/player/DynamicLoadControlTest.kt +56 -0
  80. package/android/src/test/java/com/audiobrowser/player/EqualizerControllerTest.kt +216 -0
  81. package/android/src/test/java/com/audiobrowser/player/NowPlayingUpdaterTest.kt +442 -0
  82. package/android/src/test/java/com/audiobrowser/player/PlaybackErrorClassifierTest.kt +191 -0
  83. package/android/src/test/java/com/audiobrowser/player/PlaybackStateMachineTest.kt +198 -0
  84. package/android/src/test/java/com/audiobrowser/player/PlaybackTimerTest.kt +137 -0
  85. package/android/src/test/java/com/audiobrowser/player/PlayerEngineFactoryTest.kt +36 -0
  86. package/android/src/test/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicyTest.kt +268 -0
  87. package/android/src/test/java/com/audiobrowser/player/SessionErrorMaskTest.kt +68 -0
  88. package/android/src/test/java/com/audiobrowser/player/StuckRecoveryPolicyTest.kt +40 -0
  89. package/android/src/test/java/com/audiobrowser/player/TransformingDataSourceTest.kt +193 -0
  90. package/android/src/test/java/com/audiobrowser/tls/AiaCertChaserTest.kt +107 -0
  91. package/android/src/test/java/com/audiobrowser/tls/AiaChasingTrustManagerTest.kt +73 -0
  92. package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherTest.kt +75 -0
  93. package/android/src/test/java/com/audiobrowser/tls/CertFixtures.kt +18 -0
  94. package/android/src/test/java/com/audiobrowser/util/ArtworkContentProviderTest.kt +163 -0
  95. package/android/src/test/java/com/audiobrowser/util/ArtworkUrisTest.kt +34 -0
  96. package/android/src/test/java/com/audiobrowser/util/BrowserPathHelperTest.kt +28 -0
  97. package/android/src/test/java/com/audiobrowser/util/CoilArtworkLoaderHolderTest.kt +53 -0
  98. package/android/src/test/java/com/audiobrowser/util/CoilArtworkLoaderTest.kt +58 -0
  99. package/android/src/test/java/com/audiobrowser/util/FakeImageLoader.kt +57 -0
  100. package/android/src/test/java/com/audiobrowser/util/PlayingStateFactoryTest.kt +40 -0
  101. package/android/src/test/java/com/audiobrowser/util/RatingFavoritesTest.kt +34 -0
  102. package/android/src/test/java/com/audiobrowser/util/TrackFactoryBrowseTest.kt +50 -0
  103. package/android/src/test/java/com/audiobrowser/util/TrackFactoryMediaIdTest.kt +63 -0
  104. package/android/src/test/java/com/audiobrowser/util/TrackFactoryRatingTest.kt +35 -0
  105. package/android/src/test/resources/certs/isrg-root-x1.pem +31 -0
  106. package/android/src/test/resources/certs/letsencrypt-r13.der +0 -0
  107. package/android/src/test/resources/certs/letsencrypt-r13.pem +29 -0
  108. package/android/src/test/resources/certs/stationplaylist-leaf.pem +34 -0
  109. package/ios/Browser/BrowserConfig.swift +41 -11
  110. package/ios/Browser/BrowserManager+URLResolution.swift +213 -93
  111. package/ios/Browser/BrowserManager.swift +303 -110
  112. package/ios/Browser/BrowserPathHelper.swift +32 -3
  113. package/ios/Browser/JsonModels.swift +287 -66
  114. package/ios/Browser/MediaResolveComposer.swift +114 -0
  115. package/ios/Browser/SearchDrillIn.swift +44 -0
  116. package/ios/Browser/SectionScope.swift +35 -0
  117. package/ios/Browser/TabBarEntries.swift +15 -0
  118. package/ios/CarPlay/CPInterfaceController+Safe.swift +126 -0
  119. package/ios/CarPlay/CarPlayArtworkResolver.swift +6 -6
  120. package/ios/CarPlay/CarPlayController.swift +739 -149
  121. package/ios/CarPlay/CarPlayImageLoader.swift +57 -8
  122. package/ios/CarPlay/CarPlayListItemFactory.swift +44 -35
  123. package/ios/CarPlay/CarPlayNowPlayingManager.swift +180 -36
  124. package/ios/CarPlay/ImageRowItem+CarPlay.swift +19 -0
  125. package/ios/CarPlay/RNABAudioBrowser.h +15 -10
  126. package/ios/CarPlay/RNABAudioBrowser.m +15 -14
  127. package/ios/CarPlay/RNABCarPlaySceneDelegate.m +2 -0
  128. package/ios/CarPlay/RNABMediaAddHandler.swift +75 -0
  129. package/ios/CarPlay/RNABMediaAffinityHandler.swift +69 -0
  130. package/ios/CarPlay/RNABMediaIntentHandler.swift +53 -43
  131. package/ios/Extension/Capability+RemoteCommand.swift +7 -9
  132. package/ios/Extension/ChapterMetadata+AVFoundation.swift +1 -1
  133. package/ios/Extension/NavigationError+Conversion.swift +8 -1
  134. package/ios/Extension/PlayerCapabilities+Favorite.swift +20 -0
  135. package/ios/Extension/RequestConfig+Copying.swift +31 -0
  136. package/ios/Extension/ResolvedTrack+Copying.swift +10 -4
  137. package/ios/Extension/TimedMetadata+AVFoundation.swift +1 -1
  138. package/ios/Extension/Track+AVPlayer.swift +4 -4
  139. package/ios/Extension/Track+Copying.swift +9 -3
  140. package/ios/Extension/TrackMetadata+AVFoundation.swift +1 -1
  141. package/ios/Extension/Variant+Artwork.swift +49 -0
  142. package/ios/HybridAudioBrowser.swift +750 -173
  143. package/ios/Model/NitroTypeStubs.swift +299 -224
  144. package/ios/Model/PlayerUpdateOptions.swift +23 -23
  145. package/ios/Model/RemoteCommand.swift +6 -6
  146. package/ios/Model/TrackPlayerError.swift +82 -18
  147. package/ios/NowPlayingInfo/NowPlayingInfoController.swift +99 -121
  148. package/ios/NowPlayingInfo/SilentAudioPrimer.swift +73 -0
  149. package/ios/Observer/PlayerItemNotificationObserver.swift +45 -5
  150. package/ios/Observer/PlayerItemPropertyObserver.swift +27 -4
  151. package/ios/Observer/PlayerTimeObserver.swift +3 -53
  152. package/ios/Option/SessionCategories.swift +33 -11
  153. package/ios/Player/AirPlayStallJudgement.swift +25 -0
  154. package/ios/Player/EndOfTrackJudgement.swift +32 -0
  155. package/ios/Player/LoadSeekCoordinator.swift +16 -1
  156. package/ios/Player/MediaIntentCriteria.swift +139 -0
  157. package/ios/Player/MediaLoader.swift +28 -10
  158. package/ios/Player/NowPlayingUpdater.swift +119 -26
  159. package/ios/Player/PersistedPlaybackState.swift +11 -0
  160. package/ios/Player/PlaybackCoordinator.swift +282 -51
  161. package/ios/Player/PlaybackCoordinatorCallbacks.swift +6 -1
  162. package/ios/Player/PlaybackEffectHandler.swift +19 -9
  163. package/ios/Player/PlaybackErrorHandler.swift +97 -13
  164. package/ios/Player/PlaybackStateMachine.swift +18 -13
  165. package/ios/Player/PlaybackStateStore.swift +27 -0
  166. package/ios/Player/PlaybackTimer.swift +61 -0
  167. package/ios/Player/PlayingStateManager.swift +3 -2
  168. package/ios/Player/QueueManager.swift +67 -61
  169. package/ios/Player/RetryManager.swift +97 -21
  170. package/ios/Player/SleepTimerHandling.swift +8 -0
  171. package/ios/Player/SleepTimerManager.swift +38 -5
  172. package/ios/Player/VolumeFader.swift +77 -0
  173. package/ios/RemoteCommand/RemoteCommandController.swift +31 -15
  174. package/ios/Tests/AirPlayStallJudgementTests.swift +52 -0
  175. package/ios/Tests/BrowserPathHelperTests.swift +28 -0
  176. package/ios/Tests/CarPlayArtworkResolverTests.swift +39 -39
  177. package/ios/Tests/ChapterMetadataAVFoundationTests.swift +2 -2
  178. package/ios/Tests/EndOfTrackJudgementTests.swift +35 -0
  179. package/ios/Tests/JsonModelsTests.swift +58 -0
  180. package/ios/Tests/LoadSeekCoordinatorTests.swift +38 -7
  181. package/ios/Tests/MediaIntentCriteriaTests.swift +170 -0
  182. package/ios/Tests/MediaLoaderAssetOptionsTests.swift +46 -0
  183. package/ios/Tests/MediaLoaderTests.swift +23 -8
  184. package/ios/Tests/MediaResolveTests.swift +126 -0
  185. package/ios/Tests/OnceValueTests.swift +79 -0
  186. package/ios/Tests/PlaybackCoordinatorTests.swift +784 -13
  187. package/ios/Tests/PlaybackErrorHandlerTests.swift +380 -46
  188. package/ios/Tests/PlaybackStateMachineTests.swift +42 -10
  189. package/ios/Tests/PlaybackStateStoreTests.swift +48 -0
  190. package/ios/Tests/PlaybackTimerTests.swift +55 -0
  191. package/ios/Tests/PlayingStateManagerTests.swift +57 -0
  192. package/ios/Tests/QueueManagerTests.swift +149 -18
  193. package/ios/Tests/ResolvedTrackCopyingTests.swift +15 -8
  194. package/ios/Tests/RetryManagerTests.swift +89 -0
  195. package/ios/Tests/SearchDrillInTests.swift +63 -0
  196. package/ios/Tests/SectionScopeTests.swift +82 -0
  197. package/ios/Tests/SleepTimerManagerTests.swift +104 -0
  198. package/ios/Tests/Support/TestTypes.swift +61 -20
  199. package/ios/Tests/TabBarEntriesTests.swift +71 -0
  200. package/ios/Tests/TrackSelectorTests.swift +15 -15
  201. package/ios/TrackPlayer.swift +380 -58
  202. package/ios/TrackPlayerCallbacks.swift +0 -6
  203. package/ios/TrackSelector.swift +5 -5
  204. package/ios/Util/ArtworkImageFetcher.swift +4 -2
  205. package/ios/Util/NetworkMonitor.swift +7 -3
  206. package/ios/Util/OnceValue.swift +34 -0
  207. package/lib/commonjs/features/browser-config.js +227 -0
  208. package/lib/commonjs/features/browser-config.js.map +1 -0
  209. package/lib/commonjs/features/browser.js +59 -158
  210. package/lib/commonjs/features/browser.js.map +1 -1
  211. package/lib/commonjs/features/carConnection.js +42 -0
  212. package/lib/commonjs/features/carConnection.js.map +1 -0
  213. package/lib/commonjs/features/errors.js +34 -2
  214. package/lib/commonjs/features/errors.js.map +1 -1
  215. package/lib/commonjs/features/gate.js +149 -0
  216. package/lib/commonjs/features/gate.js.map +1 -0
  217. package/lib/commonjs/features/index.js +22 -0
  218. package/lib/commonjs/features/index.js.map +1 -1
  219. package/lib/commonjs/features/metadata.js.map +1 -1
  220. package/lib/commonjs/features/nowPlaying.js +35 -0
  221. package/lib/commonjs/features/nowPlaying.js.map +1 -1
  222. package/lib/commonjs/features/output.js +37 -23
  223. package/lib/commonjs/features/output.js.map +1 -1
  224. package/lib/commonjs/features/playback/controls.js +9 -0
  225. package/lib/commonjs/features/playback/controls.js.map +1 -1
  226. package/lib/commonjs/features/playback/index.js +11 -0
  227. package/lib/commonjs/features/playback/index.js.map +1 -1
  228. package/lib/commonjs/features/playback/progress.js +28 -12
  229. package/lib/commonjs/features/playback/progress.js.map +1 -1
  230. package/lib/commonjs/features/playback/trackPlaybackTime.js +92 -0
  231. package/lib/commonjs/features/playback/trackPlaybackTime.js.map +1 -0
  232. package/lib/commonjs/features/playback/volume.js +1 -1
  233. package/lib/commonjs/features/player/options.js +49 -13
  234. package/lib/commonjs/features/player/options.js.map +1 -1
  235. package/lib/commonjs/features/player/setup.js +249 -3
  236. package/lib/commonjs/features/player/setup.js.map +1 -1
  237. package/lib/commonjs/features/queue/queue.js +10 -3
  238. package/lib/commonjs/features/queue/queue.js.map +1 -1
  239. package/lib/commonjs/features/remoteControls.js +1 -12
  240. package/lib/commonjs/features/remoteControls.js.map +1 -1
  241. package/lib/commonjs/features/sleepTimer.js +9 -4
  242. package/lib/commonjs/features/sleepTimer.js.map +1 -1
  243. package/lib/commonjs/native.js +5 -0
  244. package/lib/commonjs/native.js.map +1 -1
  245. package/lib/commonjs/native.web.js +4 -1
  246. package/lib/commonjs/native.web.js.map +1 -1
  247. package/lib/commonjs/utils/LazyNativeEmitter.js +0 -3
  248. package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
  249. package/lib/commonjs/utils/NativeUpdatedValue.js +0 -3
  250. package/lib/commonjs/utils/NativeUpdatedValue.js.map +1 -1
  251. package/lib/commonjs/utils/artwork.js +31 -0
  252. package/lib/commonjs/utils/artwork.js.map +1 -0
  253. package/lib/commonjs/web/NativeAudioBrowser.js +301 -124
  254. package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
  255. package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js +71 -0
  256. package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js.map +1 -0
  257. package/lib/commonjs/web/TrackPlayer/PlaybackTimer.js +34 -0
  258. package/lib/commonjs/web/TrackPlayer/PlaybackTimer.js.map +1 -0
  259. package/lib/commonjs/web/TrackPlayer/Player.js +90 -26
  260. package/lib/commonjs/web/TrackPlayer/Player.js.map +1 -1
  261. package/lib/commonjs/web/TrackPlayer/PlayingStateFactory.js +21 -0
  262. package/lib/commonjs/web/TrackPlayer/PlayingStateFactory.js.map +1 -0
  263. package/lib/commonjs/web/TrackPlayer/QueueManager.js +273 -0
  264. package/lib/commonjs/web/TrackPlayer/QueueManager.js.map +1 -0
  265. package/lib/commonjs/web/TrackPlayer/QueuePlayer.js +158 -0
  266. package/lib/commonjs/web/TrackPlayer/QueuePlayer.js.map +1 -0
  267. package/lib/commonjs/web/TrackPlayer/RepeatMode.js +1 -1
  268. package/lib/commonjs/web/TrackPlayer/RepeatMode.js.map +1 -1
  269. package/lib/commonjs/web/TrackPlayer/SleepTimer.js +34 -3
  270. package/lib/commonjs/web/TrackPlayer/SleepTimer.js.map +1 -1
  271. package/lib/commonjs/web/TrackPlayer/VolumeFader.js +75 -0
  272. package/lib/commonjs/web/TrackPlayer/VolumeFader.js.map +1 -0
  273. package/lib/commonjs/web/TrackPlayer/index.js +13 -5
  274. package/lib/commonjs/web/TrackPlayer/index.js.map +1 -1
  275. package/lib/commonjs/web/browser/BrowserManager.js +176 -50
  276. package/lib/commonjs/web/browser/BrowserManager.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 +11 -9
  280. package/lib/commonjs/web/browser/SearchManager.js.map +1 -1
  281. package/lib/commonjs/web/browser/assertBrowsePageShape.js +22 -0
  282. package/lib/commonjs/web/browser/assertBrowsePageShape.js.map +1 -0
  283. package/lib/commonjs/web/browser/classifyTrackNavigation.js +41 -0
  284. package/lib/commonjs/web/browser/classifyTrackNavigation.js.map +1 -0
  285. package/lib/commonjs/web/browser/parseSearchResponse.js +20 -0
  286. package/lib/commonjs/web/browser/parseSearchResponse.js.map +1 -0
  287. package/lib/commonjs/web/http/HttpClient.js +4 -23
  288. package/lib/commonjs/web/http/HttpClient.js.map +1 -1
  289. package/lib/commonjs/web/http/RequestConfigBuilder.js +107 -58
  290. package/lib/commonjs/web/http/RequestConfigBuilder.js.map +1 -1
  291. package/lib/commonjs/web/playbackErrorKind.js +57 -0
  292. package/lib/commonjs/web/playbackErrorKind.js.map +1 -0
  293. package/lib/commonjs/web/player/MediaSessionManager.js +121 -0
  294. package/lib/commonjs/web/player/MediaSessionManager.js.map +1 -0
  295. package/lib/commonjs/web/player/NowPlayingManager.js +6 -5
  296. package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
  297. package/lib/commonjs/web/player/OptionsManager.js +9 -17
  298. package/lib/commonjs/web/player/OptionsManager.js.map +1 -1
  299. package/lib/commonjs/web/player/RemoteCommandController.js +96 -0
  300. package/lib/commonjs/web/player/RemoteCommandController.js.map +1 -0
  301. package/lib/module/features/browser-config.js +221 -0
  302. package/lib/module/features/browser-config.js.map +1 -0
  303. package/lib/module/features/browser.js +53 -156
  304. package/lib/module/features/browser.js.map +1 -1
  305. package/lib/module/features/carConnection.js +37 -0
  306. package/lib/module/features/carConnection.js.map +1 -0
  307. package/lib/module/features/errors.js +34 -2
  308. package/lib/module/features/errors.js.map +1 -1
  309. package/lib/module/features/gate.js +144 -0
  310. package/lib/module/features/gate.js.map +1 -0
  311. package/lib/module/features/index.js +2 -0
  312. package/lib/module/features/index.js.map +1 -1
  313. package/lib/module/features/metadata.js.map +1 -1
  314. package/lib/module/features/nowPlaying.js +33 -0
  315. package/lib/module/features/nowPlaying.js.map +1 -1
  316. package/lib/module/features/output.js +32 -19
  317. package/lib/module/features/output.js.map +1 -1
  318. package/lib/module/features/playback/controls.js +8 -0
  319. package/lib/module/features/playback/controls.js.map +1 -1
  320. package/lib/module/features/playback/index.js +1 -0
  321. package/lib/module/features/playback/index.js.map +1 -1
  322. package/lib/module/features/playback/progress.js +28 -12
  323. package/lib/module/features/playback/progress.js.map +1 -1
  324. package/lib/module/features/playback/trackPlaybackTime.js +88 -0
  325. package/lib/module/features/playback/trackPlaybackTime.js.map +1 -0
  326. package/lib/module/features/playback/volume.js +1 -1
  327. package/lib/module/features/player/options.js +48 -13
  328. package/lib/module/features/player/options.js.map +1 -1
  329. package/lib/module/features/player/setup.js +249 -3
  330. package/lib/module/features/player/setup.js.map +1 -1
  331. package/lib/module/features/queue/queue.js +10 -3
  332. package/lib/module/features/queue/queue.js.map +1 -1
  333. package/lib/module/features/remoteControls.js +0 -11
  334. package/lib/module/features/remoteControls.js.map +1 -1
  335. package/lib/module/features/sleepTimer.js +9 -4
  336. package/lib/module/features/sleepTimer.js.map +1 -1
  337. package/lib/module/native.js +5 -0
  338. package/lib/module/native.js.map +1 -1
  339. package/lib/module/native.web.js +4 -1
  340. package/lib/module/native.web.js.map +1 -1
  341. package/lib/module/utils/LazyNativeEmitter.js +0 -3
  342. package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
  343. package/lib/module/utils/NativeUpdatedValue.js +0 -3
  344. package/lib/module/utils/NativeUpdatedValue.js.map +1 -1
  345. package/lib/module/utils/artwork.js +26 -0
  346. package/lib/module/utils/artwork.js.map +1 -0
  347. package/lib/module/web/NativeAudioBrowser.js +302 -125
  348. package/lib/module/web/NativeAudioBrowser.js.map +1 -1
  349. package/lib/module/web/TrackPlayer/PlaybackStateMachine.js +67 -0
  350. package/lib/module/web/TrackPlayer/PlaybackStateMachine.js.map +1 -0
  351. package/lib/module/web/TrackPlayer/PlaybackTimer.js +29 -0
  352. package/lib/module/web/TrackPlayer/PlaybackTimer.js.map +1 -0
  353. package/lib/module/web/TrackPlayer/Player.js +90 -26
  354. package/lib/module/web/TrackPlayer/Player.js.map +1 -1
  355. package/lib/module/web/TrackPlayer/PlayingStateFactory.js +17 -0
  356. package/lib/module/web/TrackPlayer/PlayingStateFactory.js.map +1 -0
  357. package/lib/module/web/TrackPlayer/QueueManager.js +269 -0
  358. package/lib/module/web/TrackPlayer/QueueManager.js.map +1 -0
  359. package/lib/module/web/TrackPlayer/QueuePlayer.js +154 -0
  360. package/lib/module/web/TrackPlayer/QueuePlayer.js.map +1 -0
  361. package/lib/module/web/TrackPlayer/RepeatMode.js +1 -1
  362. package/lib/module/web/TrackPlayer/RepeatMode.js.map +1 -1
  363. package/lib/module/web/TrackPlayer/SleepTimer.js +34 -3
  364. package/lib/module/web/TrackPlayer/SleepTimer.js.map +1 -1
  365. package/lib/module/web/TrackPlayer/VolumeFader.js +70 -0
  366. package/lib/module/web/TrackPlayer/VolumeFader.js.map +1 -0
  367. package/lib/module/web/TrackPlayer/index.js +2 -1
  368. package/lib/module/web/TrackPlayer/index.js.map +1 -1
  369. package/lib/module/web/browser/BrowserManager.js +176 -50
  370. package/lib/module/web/browser/BrowserManager.js.map +1 -1
  371. package/lib/module/web/browser/NavigationErrorManager.js +3 -1
  372. package/lib/module/web/browser/NavigationErrorManager.js.map +1 -1
  373. package/lib/module/web/browser/SearchManager.js +11 -9
  374. package/lib/module/web/browser/SearchManager.js.map +1 -1
  375. package/lib/module/web/browser/assertBrowsePageShape.js +18 -0
  376. package/lib/module/web/browser/assertBrowsePageShape.js.map +1 -0
  377. package/lib/module/web/browser/classifyTrackNavigation.js +38 -0
  378. package/lib/module/web/browser/classifyTrackNavigation.js.map +1 -0
  379. package/lib/module/web/browser/parseSearchResponse.js +16 -0
  380. package/lib/module/web/browser/parseSearchResponse.js.map +1 -0
  381. package/lib/module/web/http/HttpClient.js +4 -23
  382. package/lib/module/web/http/HttpClient.js.map +1 -1
  383. package/lib/module/web/http/RequestConfigBuilder.js +107 -58
  384. package/lib/module/web/http/RequestConfigBuilder.js.map +1 -1
  385. package/lib/module/web/playbackErrorKind.js +52 -0
  386. package/lib/module/web/playbackErrorKind.js.map +1 -0
  387. package/lib/module/web/player/MediaSessionManager.js +116 -0
  388. package/lib/module/web/player/MediaSessionManager.js.map +1 -0
  389. package/lib/module/web/player/NowPlayingManager.js +7 -5
  390. package/lib/module/web/player/NowPlayingManager.js.map +1 -1
  391. package/lib/module/web/player/OptionsManager.js +9 -17
  392. package/lib/module/web/player/OptionsManager.js.map +1 -1
  393. package/lib/module/web/player/RemoteCommandController.js +92 -0
  394. package/lib/module/web/player/RemoteCommandController.js.map +1 -0
  395. package/lib/typescript/src/features/browser-config.d.ts +12 -0
  396. package/lib/typescript/src/features/browser-config.d.ts.map +1 -0
  397. package/lib/typescript/src/features/browser.d.ts +40 -1
  398. package/lib/typescript/src/features/browser.d.ts.map +1 -1
  399. package/lib/typescript/src/features/carConnection.d.ts +20 -0
  400. package/lib/typescript/src/features/carConnection.d.ts.map +1 -0
  401. package/lib/typescript/src/features/errors.d.ts +58 -3
  402. package/lib/typescript/src/features/errors.d.ts.map +1 -1
  403. package/lib/typescript/src/features/gate.d.ts +77 -0
  404. package/lib/typescript/src/features/gate.d.ts.map +1 -0
  405. package/lib/typescript/src/features/index.d.ts +2 -0
  406. package/lib/typescript/src/features/index.d.ts.map +1 -1
  407. package/lib/typescript/src/features/metadata.d.ts +6 -4
  408. package/lib/typescript/src/features/metadata.d.ts.map +1 -1
  409. package/lib/typescript/src/features/nowPlaying.d.ts +27 -0
  410. package/lib/typescript/src/features/nowPlaying.d.ts.map +1 -1
  411. package/lib/typescript/src/features/output.d.ts +28 -18
  412. package/lib/typescript/src/features/output.d.ts.map +1 -1
  413. package/lib/typescript/src/features/playback/controls.d.ts +5 -0
  414. package/lib/typescript/src/features/playback/controls.d.ts.map +1 -1
  415. package/lib/typescript/src/features/playback/index.d.ts +1 -0
  416. package/lib/typescript/src/features/playback/index.d.ts.map +1 -1
  417. package/lib/typescript/src/features/playback/progress.d.ts +3 -0
  418. package/lib/typescript/src/features/playback/progress.d.ts.map +1 -1
  419. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts +34 -0
  420. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts.map +1 -0
  421. package/lib/typescript/src/features/playback/volume.d.ts +1 -1
  422. package/lib/typescript/src/features/player/options.d.ts +92 -93
  423. package/lib/typescript/src/features/player/options.d.ts.map +1 -1
  424. package/lib/typescript/src/features/player/setup.d.ts +206 -167
  425. package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
  426. package/lib/typescript/src/features/queue/queue.d.ts +9 -2
  427. package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
  428. package/lib/typescript/src/features/remoteControls.d.ts +0 -13
  429. package/lib/typescript/src/features/remoteControls.d.ts.map +1 -1
  430. package/lib/typescript/src/features/sleepTimer.d.ts +18 -3
  431. package/lib/typescript/src/features/sleepTimer.d.ts.map +1 -1
  432. package/lib/typescript/src/index.d.ts.map +1 -1
  433. package/lib/typescript/src/native.d.ts +1 -2
  434. package/lib/typescript/src/native.d.ts.map +1 -1
  435. package/lib/typescript/src/specs/audio-browser.nitro.d.ts +17 -225
  436. package/lib/typescript/src/specs/audio-browser.nitro.d.ts.map +1 -1
  437. package/lib/typescript/src/types/browser-native.d.ts +9 -3
  438. package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
  439. package/lib/typescript/src/types/browser-nodes.d.ts +145 -13
  440. package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
  441. package/lib/typescript/src/types/browser.d.ts +349 -85
  442. package/lib/typescript/src/types/browser.d.ts.map +1 -1
  443. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +0 -2
  444. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
  445. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts +0 -2
  446. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts.map +1 -1
  447. package/lib/typescript/src/utils/artwork.d.ts +19 -0
  448. package/lib/typescript/src/utils/artwork.d.ts.map +1 -0
  449. package/lib/typescript/src/utils/resolveAssetSource.d.ts.map +1 -1
  450. package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +3 -1
  451. package/nitrogen/generated/android/AudioBrowserOnLoad.cpp +24 -10
  452. package/nitrogen/generated/android/c++/JAndroidAudioContentType.hpp +6 -6
  453. package/nitrogen/generated/android/c++/JAndroidOptions.hpp +1 -11
  454. package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +33 -1
  455. package/nitrogen/generated/android/c++/JArtworkVariants.hpp +61 -0
  456. package/nitrogen/generated/android/c++/JBrowseResult.hpp +5 -0
  457. package/nitrogen/generated/android/c++/JCarPlayNowPlayingButton.hpp +3 -3
  458. package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +6 -0
  459. package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +58 -0
  460. package/nitrogen/generated/android/c++/JFavoritesMatchMode.hpp +58 -0
  461. package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +100 -0
  462. package/nitrogen/generated/android/c++/JFormattedNavigationError.hpp +3 -2
  463. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___MediaTransformParams.hpp +112 -0
  464. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___RequestConfig_std__optional_std__unordered_map_std__string__std__string__.hpp +121 -0
  465. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +123 -0
  466. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +133 -0
  467. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +121 -0
  468. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_GateDecision_____NativeGateRequest.hpp +139 -0
  469. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +6 -0
  470. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_TransformableRequestConfig____.hpp +134 -0
  471. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +8 -0
  472. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +6 -0
  473. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.hpp +131 -0
  474. package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +6 -0
  475. package/nitrogen/generated/android/c++/JFunc_void_GateEvent.hpp +79 -0
  476. package/nitrogen/generated/android/c++/JFunc_void_NowPlayingMetadata.hpp +0 -10
  477. package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +9 -4
  478. package/nitrogen/generated/android/c++/JFunc_void_Output.hpp +80 -0
  479. package/nitrogen/generated/android/c++/JFunc_void_Playback.hpp +2 -0
  480. package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +6 -0
  481. package/nitrogen/generated/android/c++/JFunc_void_PlaybackErrorEvent.hpp +2 -0
  482. package/nitrogen/generated/android/c++/JFunc_void_bool.hpp +10 -10
  483. package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +6 -0
  484. package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +6 -0
  485. package/nitrogen/generated/android/c++/JGate.hpp +62 -0
  486. package/nitrogen/generated/android/c++/JGateDecision.hpp +64 -0
  487. package/nitrogen/generated/android/c++/{JThumbsRating.hpp → JGateEvent.hpp} +16 -15
  488. package/nitrogen/generated/android/c++/JGateReason.hpp +58 -0
  489. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +236 -212
  490. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.hpp +25 -22
  491. package/nitrogen/generated/android/c++/JIOSOptions.hpp +95 -0
  492. package/nitrogen/generated/android/c++/JImageRowItem.hpp +33 -3
  493. package/nitrogen/generated/android/c++/JMediaReference.hpp +58 -0
  494. package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +33 -1
  495. package/nitrogen/generated/android/c++/{JPartialAndroidSetupPlayerOptions.hpp → JNativeAndroidSetupOptions.hpp} +10 -10
  496. package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +61 -28
  497. package/nitrogen/generated/android/c++/JNativeGateRequest.hpp +74 -0
  498. package/nitrogen/generated/android/c++/{JPartialIOSSetupPlayerOptions.hpp → JNativeIOSSetupOptions.hpp} +10 -10
  499. package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +20 -9
  500. package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +182 -0
  501. package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +16 -19
  502. package/nitrogen/generated/android/c++/JNavigationErrorType.hpp +6 -0
  503. package/nitrogen/generated/android/c++/JNitroAndroidUpdateOptions.hpp +1 -11
  504. package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +96 -0
  505. package/nitrogen/generated/android/c++/JNotificationButton.hpp +3 -3
  506. package/nitrogen/generated/android/c++/JNowPlayingMetadata.hpp +3 -17
  507. package/nitrogen/generated/android/c++/JNowPlayingUpdate.hpp +7 -3
  508. package/nitrogen/generated/android/c++/JOptions.hpp +14 -9
  509. package/nitrogen/generated/android/c++/{JIosOutput.hpp → JOutput.hpp} +15 -15
  510. package/nitrogen/generated/android/c++/JOutputType.hpp +82 -0
  511. package/nitrogen/generated/android/c++/JPlayback.hpp +2 -0
  512. package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +6 -0
  513. package/nitrogen/generated/android/c++/JPlaybackError.hpp +18 -3
  514. package/nitrogen/generated/android/c++/JPlaybackErrorEvent.hpp +2 -0
  515. package/nitrogen/generated/android/c++/JPlaybackErrorKind.hpp +76 -0
  516. package/nitrogen/generated/android/c++/JPlaybackState.hpp +3 -3
  517. package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +11 -5
  518. package/nitrogen/generated/android/c++/JResolvedTrack.hpp +23 -5
  519. package/nitrogen/generated/android/c++/JRetryConfig.hpp +11 -7
  520. package/nitrogen/generated/android/c++/JSearchMode.hpp +27 -9
  521. package/nitrogen/generated/android/c++/JSearchParams.hpp +9 -3
  522. package/nitrogen/generated/android/c++/JStallReason.hpp +58 -0
  523. package/nitrogen/generated/android/c++/JTrack.hpp +23 -5
  524. package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +6 -0
  525. package/nitrogen/generated/android/c++/JTrackRequest.hpp +93 -0
  526. package/nitrogen/generated/android/c++/JTransformableRequestConfig.hpp +14 -1
  527. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.cpp +26 -0
  528. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.hpp +71 -0
  529. package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.cpp +41 -0
  530. package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +121 -0
  531. package/nitrogen/generated/android/c++/JVariant_String_ArtworkVariants.cpp +26 -0
  532. package/nitrogen/generated/android/c++/JVariant_String_ArtworkVariants.hpp +70 -0
  533. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidAudioContentType.kt +4 -4
  534. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidOptions.kt +2 -8
  535. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ArtworkRequestConfig.kt +10 -4
  536. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ArtworkVariants.kt +41 -0
  537. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CarPlayNowPlayingButton.kt +3 -3
  538. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{ThumbsRating.kt → FavoriteConfig.kt} +6 -6
  539. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{RatingType.kt → FavoritesMatchMode.kt} +5 -10
  540. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FormatNowPlayingParams.kt +50 -0
  541. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FormattedNavigationError.kt +2 -2
  542. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_RequestConfig___MediaTransformParams.kt +80 -0
  543. 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 +80 -0
  544. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_RequestConfig___Track.kt +80 -0
  545. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.kt +80 -0
  546. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__optional_std__string____Track.kt +80 -0
  547. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_GateDecision_____NativeGateRequest.kt +80 -0
  548. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_TransformableRequestConfig____.kt +80 -0
  549. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError______BrowserSourceCallbackParam.kt → Func_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.kt} +12 -12
  550. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Func_void_RemoteSetRatingEvent.kt → Func_void_GateEvent.kt} +9 -9
  551. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Func_void_IosOutput.kt → Func_void_Output.kt} +9 -9
  552. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_bool.kt +9 -9
  553. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{HeartRating.kt → Gate.kt} +9 -6
  554. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{PercentageRating.kt → GateDecision.kt} +9 -6
  555. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{StarRating.kt → GateEvent.kt} +6 -6
  556. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GateReason.kt +23 -0
  557. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/HybridAudioBrowserSpec.kt +103 -109
  558. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IOSOptions.kt +44 -0
  559. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageRowItem.kt +24 -3
  560. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/MediaReference.kt +23 -0
  561. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/MediaRequestConfig.kt +10 -4
  562. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{PartialAndroidSetupPlayerOptions.kt → NativeAndroidSetupOptions.kt} +5 -5
  563. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +18 -6
  564. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{RemoteSetRatingEvent.kt → NativeGateRequest.kt} +12 -6
  565. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{PartialIOSSetupPlayerOptions.kt → NativeIOSSetupOptions.kt} +5 -5
  566. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeRouteEntry.kt +4 -4
  567. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeSetupPlayerOptions.kt +66 -0
  568. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeUpdateOptions.kt +6 -6
  569. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NavigationErrorType.kt +3 -1
  570. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroAndroidUpdateOptions.kt +2 -8
  571. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroIOSUpdateOptions.kt +44 -0
  572. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NotificationButton.kt +5 -5
  573. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NowPlayingMetadata.kt +3 -6
  574. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NowPlayingUpdate.kt +6 -3
  575. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Options.kt +3 -3
  576. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{IosOutput.kt → Output.kt} +6 -6
  577. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/OutputType.kt +31 -0
  578. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackError.kt +12 -3
  579. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackErrorKind.kt +29 -0
  580. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackState.kt +6 -6
  581. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayerCapabilities.kt +2 -2
  582. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ResolvedTrack.kt +12 -3
  583. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RetryConfig.kt +7 -4
  584. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SearchMode.kt +12 -6
  585. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SearchParams.kt +6 -3
  586. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/StallReason.kt +23 -0
  587. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +12 -3
  588. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackRequest.kt +44 -0
  589. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TransformableRequestConfig.kt +7 -4
  590. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_FavoriteConfig.kt +53 -0
  591. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_ResolvedTrack_BrowseError_Promise_BrowseResult_.kt +65 -0
  592. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_String_ArtworkVariants.kt +53 -0
  593. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.cpp +139 -28
  594. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +1098 -447
  595. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +60 -39
  596. package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +170 -112
  597. package/nitrogen/generated/ios/swift/AndroidAudioContentType.swift +8 -8
  598. package/nitrogen/generated/ios/swift/AndroidOptions.swift +2 -12
  599. package/nitrogen/generated/ios/swift/ArtworkRequestConfig.swift +95 -1
  600. package/nitrogen/generated/ios/swift/ArtworkVariants.swift +34 -0
  601. package/nitrogen/generated/ios/swift/CarPlayNowPlayingButton.swift +4 -4
  602. package/nitrogen/generated/ios/swift/FavoriteConfig.swift +29 -0
  603. package/nitrogen/generated/ios/swift/FavoritesMatchMode.swift +40 -0
  604. package/nitrogen/generated/ios/swift/FormatNowPlayingParams.swift +67 -0
  605. package/nitrogen/generated/ios/swift/FormattedNavigationError.swift +17 -4
  606. package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_RequestConfig___MediaTransformParams.swift +54 -0
  607. package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_RequestConfig___RequestConfig_std__optional_std__unordered_map_std__string__std__string__.swift +69 -0
  608. package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_RequestConfig___Track.swift +54 -0
  609. package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.swift +60 -0
  610. package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__optional_std__string____Track.swift +60 -0
  611. package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_GateDecision_____NativeGateRequest.swift +61 -0
  612. package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_TransformableRequestConfig____.swift +61 -0
  613. package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.swift +77 -0
  614. package/nitrogen/generated/ios/swift/Func_void_GateDecision.swift +46 -0
  615. package/nitrogen/generated/ios/swift/{Func_void_RemoteSetRatingEvent.swift → Func_void_GateEvent.swift} +10 -10
  616. package/nitrogen/generated/ios/swift/{Func_void_IosOutput.swift → Func_void_Output.swift} +10 -10
  617. package/nitrogen/generated/ios/swift/Func_void_TransformableRequestConfig.swift +46 -0
  618. package/nitrogen/generated/ios/swift/Func_void_bool.swift +5 -5
  619. package/nitrogen/generated/ios/swift/Func_void_std__optional_NowPlayingUpdate_.swift +46 -0
  620. package/nitrogen/generated/ios/swift/Func_void_std__optional_std__string_.swift +53 -0
  621. package/nitrogen/generated/ios/swift/{Func_void_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError___.swift → Func_void_std__shared_ptr_Promise_GateDecision__.swift} +17 -17
  622. package/nitrogen/generated/ios/swift/Func_void_std__shared_ptr_Promise_TransformableRequestConfig__.swift +66 -0
  623. package/nitrogen/generated/ios/swift/Func_void_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError____.swift +81 -0
  624. package/nitrogen/generated/ios/swift/Gate.swift +47 -0
  625. package/nitrogen/generated/ios/swift/GateDecision.swift +40 -0
  626. package/nitrogen/generated/ios/swift/GateEvent.swift +29 -0
  627. package/nitrogen/generated/ios/swift/GateReason.swift +40 -0
  628. package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec.swift +20 -14
  629. package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec_cxx.swift +230 -197
  630. package/nitrogen/generated/ios/swift/IOSOptions.swift +51 -0
  631. package/nitrogen/generated/ios/swift/ImageRowItem.swift +121 -2
  632. package/nitrogen/generated/ios/swift/MediaReference.swift +40 -0
  633. package/nitrogen/generated/ios/swift/MediaRequestConfig.swift +107 -1
  634. package/nitrogen/generated/ios/swift/{PartialAndroidSetupPlayerOptions.swift → NativeAndroidSetupOptions.swift} +5 -5
  635. package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +161 -22
  636. package/nitrogen/generated/ios/swift/NativeGateRequest.swift +58 -0
  637. package/nitrogen/generated/ios/swift/{PartialIOSSetupPlayerOptions.swift → NativeIOSSetupOptions.swift} +5 -5
  638. package/nitrogen/generated/ios/swift/NativeRouteEntry.swift +19 -19
  639. package/nitrogen/generated/ios/swift/NativeSetupPlayerOptions.swift +206 -0
  640. package/nitrogen/generated/ios/swift/NativeUpdateOptions.swift +12 -25
  641. package/nitrogen/generated/ios/swift/NavigationErrorType.swift +8 -0
  642. package/nitrogen/generated/ios/swift/NitroAndroidUpdateOptions.swift +1 -30
  643. package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +90 -0
  644. package/nitrogen/generated/ios/swift/NotificationButton.swift +4 -4
  645. package/nitrogen/generated/ios/swift/NowPlayingMetadata.swift +1 -48
  646. package/nitrogen/generated/ios/swift/NowPlayingUpdate.swift +19 -1
  647. package/nitrogen/generated/ios/swift/Options.swift +10 -4
  648. package/nitrogen/generated/ios/swift/{IosOutput.swift → Output.swift} +7 -7
  649. package/nitrogen/generated/ios/swift/OutputType.swift +72 -0
  650. package/nitrogen/generated/ios/swift/PlaybackError.swift +43 -2
  651. package/nitrogen/generated/ios/swift/PlaybackErrorKind.swift +64 -0
  652. package/nitrogen/generated/ios/swift/PlaybackState.swift +4 -4
  653. package/nitrogen/generated/ios/swift/PlayerCapabilities.swift +27 -8
  654. package/nitrogen/generated/ios/swift/ResolvedTrack.swift +74 -8
  655. package/nitrogen/generated/ios/swift/RetryConfig.swift +35 -4
  656. package/nitrogen/generated/ios/swift/SearchMode.swift +36 -12
  657. package/nitrogen/generated/ios/swift/SearchParams.swift +7 -2
  658. package/nitrogen/generated/ios/swift/StallReason.swift +40 -0
  659. package/nitrogen/generated/ios/swift/Track.swift +74 -8
  660. package/nitrogen/generated/ios/swift/TrackRequest.swift +106 -0
  661. package/nitrogen/generated/ios/swift/TransformableRequestConfig.swift +60 -1
  662. package/nitrogen/generated/ios/swift/Variant_Bool_FavoriteConfig.swift +18 -0
  663. package/nitrogen/generated/ios/swift/Variant_ResolvedTrack_BrowseError_Promise_BrowseResult_.swift +19 -0
  664. package/nitrogen/generated/ios/swift/Variant_String_ArtworkVariants.swift +18 -0
  665. package/nitrogen/generated/shared/c++/AndroidAudioContentType.hpp +10 -10
  666. package/nitrogen/generated/shared/c++/AndroidOptions.hpp +1 -12
  667. package/nitrogen/generated/shared/c++/ArtworkRequestConfig.hpp +10 -2
  668. package/nitrogen/generated/shared/c++/ArtworkVariants.hpp +87 -0
  669. package/nitrogen/generated/shared/c++/CarPlayNowPlayingButton.hpp +6 -6
  670. package/nitrogen/generated/shared/c++/EqualizerSettings.hpp +0 -2
  671. package/nitrogen/generated/shared/c++/{StarRating.hpp → FavoriteConfig.hpp} +18 -17
  672. package/nitrogen/generated/shared/c++/FavoritesMatchMode.hpp +76 -0
  673. package/nitrogen/generated/shared/c++/FormatNowPlayingParams.hpp +110 -0
  674. package/nitrogen/generated/shared/c++/FormattedNavigationError.hpp +6 -5
  675. package/nitrogen/generated/shared/c++/{HeartRating.hpp → Gate.hpp} +21 -16
  676. package/nitrogen/generated/shared/c++/{ThumbsRating.hpp → GateDecision.hpp} +23 -17
  677. package/nitrogen/generated/shared/c++/{PercentageRating.hpp → GateEvent.hpp} +18 -17
  678. package/nitrogen/generated/shared/c++/GateReason.hpp +76 -0
  679. package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.cpp +21 -18
  680. package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.hpp +44 -35
  681. package/nitrogen/generated/shared/c++/IOSOptions.hpp +91 -0
  682. package/nitrogen/generated/shared/c++/ImageRowItem.hpp +33 -2
  683. package/nitrogen/generated/shared/c++/MediaReference.hpp +76 -0
  684. package/nitrogen/generated/shared/c++/MediaRequestConfig.hpp +10 -2
  685. package/nitrogen/generated/shared/c++/{PartialAndroidSetupPlayerOptions.hpp → NativeAndroidSetupOptions.hpp} +11 -11
  686. package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +31 -15
  687. package/nitrogen/generated/shared/c++/NativeGateRequest.hpp +97 -0
  688. package/nitrogen/generated/shared/c++/{PartialIOSSetupPlayerOptions.hpp → NativeIOSSetupOptions.hpp} +11 -11
  689. package/nitrogen/generated/shared/c++/NativeRouteEntry.hpp +5 -5
  690. package/nitrogen/generated/shared/c++/NativeSetupPlayerOptions.hpp +138 -0
  691. package/nitrogen/generated/shared/c++/NativeUpdateOptions.hpp +9 -7
  692. package/nitrogen/generated/shared/c++/NavigationErrorType.hpp +8 -0
  693. package/nitrogen/generated/shared/c++/NitroAndroidUpdateOptions.hpp +1 -12
  694. package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +92 -0
  695. package/nitrogen/generated/shared/c++/NotificationButton.hpp +8 -8
  696. package/nitrogen/generated/shared/c++/NowPlayingMetadata.hpp +3 -19
  697. package/nitrogen/generated/shared/c++/NowPlayingUpdate.hpp +6 -2
  698. package/nitrogen/generated/shared/c++/Options.hpp +8 -8
  699. package/nitrogen/generated/shared/c++/{IosOutput.hpp → Output.hpp} +18 -18
  700. package/nitrogen/generated/shared/c++/OutputType.hpp +108 -0
  701. package/nitrogen/generated/shared/c++/PlaybackError.hpp +18 -3
  702. package/nitrogen/generated/shared/c++/PlaybackErrorKind.hpp +100 -0
  703. package/nitrogen/generated/shared/c++/PlaybackState.hpp +9 -9
  704. package/nitrogen/generated/shared/c++/PlayerCapabilities.hpp +9 -6
  705. package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +24 -5
  706. package/nitrogen/generated/shared/c++/RetryConfig.hpp +10 -6
  707. package/nitrogen/generated/shared/c++/SearchMode.hpp +39 -15
  708. package/nitrogen/generated/shared/c++/SearchParams.hpp +9 -2
  709. package/nitrogen/generated/shared/c++/StallReason.hpp +76 -0
  710. package/nitrogen/generated/shared/c++/Track.hpp +24 -5
  711. package/nitrogen/generated/shared/c++/TrackRequest.hpp +93 -0
  712. package/nitrogen/generated/shared/c++/TransformableRequestConfig.hpp +6 -2
  713. package/package.json +16 -16
  714. package/src/codegen-no-config-variant.test.ts +70 -0
  715. package/src/features/browser-config.test.ts +110 -0
  716. package/src/features/browser-config.ts +293 -0
  717. package/src/features/browser.test.ts +19 -0
  718. package/src/features/browser.ts +58 -201
  719. package/src/features/carConnection.ts +36 -0
  720. package/src/features/errors.ts +68 -2
  721. package/src/features/gate.test.ts +72 -0
  722. package/src/features/gate.ts +155 -0
  723. package/src/features/index.ts +2 -0
  724. package/src/features/metadata.ts +6 -12
  725. package/src/features/nowPlaying.ts +36 -0
  726. package/src/features/output.ts +35 -22
  727. package/src/features/playback/controls.ts +8 -0
  728. package/src/features/playback/index.ts +1 -0
  729. package/src/features/playback/progress.ts +31 -12
  730. package/src/features/playback/trackPlaybackTime.test.ts +66 -0
  731. package/src/features/playback/trackPlaybackTime.ts +106 -0
  732. package/src/features/playback/volume.ts +1 -1
  733. package/src/features/player/options.test.ts +38 -0
  734. package/src/features/player/options.ts +140 -75
  735. package/src/features/player/setup.test.ts +117 -0
  736. package/src/features/player/setup.ts +390 -175
  737. package/src/features/queue/queue.test.ts +36 -0
  738. package/src/features/queue/queue.ts +15 -4
  739. package/src/features/remoteControls.ts +0 -23
  740. package/src/features/sleepTimer.ts +23 -4
  741. package/src/native.ts +5 -0
  742. package/src/native.web.ts +4 -1
  743. package/src/specs/audio-browser.nitro.ts +131 -41
  744. package/src/types/browser-native.ts +19 -6
  745. package/src/types/browser-nodes.ts +160 -13
  746. package/src/types/browser.ts +377 -89
  747. package/src/utils/LazyNativeEmitter.ts +0 -2
  748. package/src/utils/NativeUpdatedValue.ts +0 -2
  749. package/src/utils/artwork.ts +30 -0
  750. package/src/web/NativeAudioBrowser.playWhenReady.test.ts +301 -0
  751. package/src/web/NativeAudioBrowser.ts +368 -157
  752. package/src/web/SimpleRouter.test.ts +9 -9
  753. package/src/web/TrackPlayer/PlaybackStateMachine.test.ts +139 -0
  754. package/src/web/TrackPlayer/PlaybackStateMachine.ts +82 -0
  755. package/src/web/TrackPlayer/PlaybackTimer.test.ts +67 -0
  756. package/src/web/TrackPlayer/PlaybackTimer.ts +33 -0
  757. package/src/web/TrackPlayer/Player.dispatch.test.ts +229 -0
  758. package/src/web/TrackPlayer/Player.load.test.ts +86 -0
  759. package/src/web/TrackPlayer/Player.ts +113 -39
  760. package/src/web/TrackPlayer/PlayingStateFactory.test.ts +51 -0
  761. package/src/web/TrackPlayer/PlayingStateFactory.ts +24 -0
  762. package/src/web/TrackPlayer/QueueManager.test.ts +289 -0
  763. package/src/web/TrackPlayer/QueueManager.ts +302 -0
  764. package/src/web/TrackPlayer/QueuePlayer.ts +188 -0
  765. package/src/web/TrackPlayer/RepeatMode.ts +1 -1
  766. package/src/web/TrackPlayer/SleepTimer.ts +38 -3
  767. package/src/web/TrackPlayer/VolumeFader.ts +74 -0
  768. package/src/web/TrackPlayer/index.ts +2 -1
  769. package/src/web/browser/BrowserManager.ts +231 -62
  770. package/src/web/browser/NavigationErrorManager.ts +3 -1
  771. package/src/web/browser/SearchManager.ts +11 -15
  772. package/src/web/browser/__tests__/layer-resolver.test.ts +69 -0
  773. package/src/web/browser/assertBrowsePageShape.test.ts +27 -0
  774. package/src/web/browser/assertBrowsePageShape.ts +27 -0
  775. package/src/web/browser/classifyTrackNavigation.test.ts +41 -0
  776. package/src/web/browser/classifyTrackNavigation.ts +35 -0
  777. package/src/web/browser/parseSearchResponse.test.ts +25 -0
  778. package/src/web/browser/parseSearchResponse.ts +15 -0
  779. package/src/web/http/HttpClient.ts +5 -35
  780. package/src/web/http/RequestConfigBuilder.test.ts +59 -0
  781. package/src/web/http/RequestConfigBuilder.ts +120 -63
  782. package/src/web/playbackErrorKind.test.ts +55 -0
  783. package/src/web/playbackErrorKind.ts +56 -0
  784. package/src/web/player/MediaSessionManager.test.ts +220 -0
  785. package/src/web/player/MediaSessionManager.ts +151 -0
  786. package/src/web/player/NowPlayingManager.test.ts +46 -0
  787. package/src/web/player/NowPlayingManager.ts +6 -5
  788. package/src/web/player/OptionsManager.ts +12 -18
  789. package/src/web/player/RemoteCommandController.test.ts +171 -0
  790. package/src/web/player/RemoteCommandController.ts +166 -0
  791. package/android/src/main/java/com/audiobrowser/player/PlaybackProgressUpdateManager.kt +0 -59
  792. package/android/src/main/java/com/audiobrowser/util/RatingFactory.kt +0 -72
  793. package/android/src/main/java/com/audiobrowser/util/ResolvedTrackFactory.kt +0 -36
  794. package/android/src/test/java/com/audiobrowser/player/PlaybackProgressUpdateManagerTest.kt +0 -146
  795. package/ios/CarPlay/Track+CarPlay.swift +0 -63
  796. package/ios/NowPlayingInfo/NowPlayingInfoCenter.swift +0 -9
  797. package/ios/Player/PlaybackProgressUpdateManager.swift +0 -68
  798. package/ios/Tests/PlaybackProgressUpdateManagerTests.swift +0 -166
  799. package/lib/commonjs/features/rating.js +0 -2
  800. package/lib/commonjs/features/rating.js.map +0 -1
  801. package/lib/commonjs/types/player.js +0 -2
  802. package/lib/commonjs/types/player.js.map +0 -1
  803. package/lib/commonjs/web/SimpleRouter.test.js +0 -174
  804. package/lib/commonjs/web/SimpleRouter.test.js.map +0 -1
  805. package/lib/commonjs/web/TrackPlayer/PlaylistPlayer.js +0 -293
  806. package/lib/commonjs/web/TrackPlayer/PlaylistPlayer.js.map +0 -1
  807. package/lib/module/features/rating.js +0 -2
  808. package/lib/module/features/rating.js.map +0 -1
  809. package/lib/module/types/player.js +0 -2
  810. package/lib/module/types/player.js.map +0 -1
  811. package/lib/module/web/SimpleRouter.test.js +0 -174
  812. package/lib/module/web/SimpleRouter.test.js.map +0 -1
  813. package/lib/module/web/TrackPlayer/PlaylistPlayer.js +0 -288
  814. package/lib/module/web/TrackPlayer/PlaylistPlayer.js.map +0 -1
  815. package/lib/typescript/src/features/rating.d.ts +0 -14
  816. package/lib/typescript/src/features/rating.d.ts.map +0 -1
  817. package/lib/typescript/src/native.web.d.ts +0 -3
  818. package/lib/typescript/src/native.web.d.ts.map +0 -1
  819. package/lib/typescript/src/types/player.d.ts +0 -64
  820. package/lib/typescript/src/types/player.d.ts.map +0 -1
  821. package/lib/typescript/src/web/NativeAudioBrowser.d.ts +0 -188
  822. package/lib/typescript/src/web/NativeAudioBrowser.d.ts.map +0 -1
  823. package/lib/typescript/src/web/SimpleRouter.d.ts +0 -40
  824. package/lib/typescript/src/web/SimpleRouter.d.ts.map +0 -1
  825. package/lib/typescript/src/web/SimpleRouter.test.d.ts +0 -2
  826. package/lib/typescript/src/web/SimpleRouter.test.d.ts.map +0 -1
  827. package/lib/typescript/src/web/TrackPlayer/Event.d.ts +0 -11
  828. package/lib/typescript/src/web/TrackPlayer/Event.d.ts.map +0 -1
  829. package/lib/typescript/src/web/TrackPlayer/Player.d.ts +0 -59
  830. package/lib/typescript/src/web/TrackPlayer/Player.d.ts.map +0 -1
  831. package/lib/typescript/src/web/TrackPlayer/PlaylistPlayer.d.ts +0 -39
  832. package/lib/typescript/src/web/TrackPlayer/PlaylistPlayer.d.ts.map +0 -1
  833. package/lib/typescript/src/web/TrackPlayer/RepeatMode.d.ts +0 -6
  834. package/lib/typescript/src/web/TrackPlayer/RepeatMode.d.ts.map +0 -1
  835. package/lib/typescript/src/web/TrackPlayer/SetupNotCalledError.d.ts +0 -4
  836. package/lib/typescript/src/web/TrackPlayer/SetupNotCalledError.d.ts.map +0 -1
  837. package/lib/typescript/src/web/TrackPlayer/SleepTimer.d.ts +0 -34
  838. package/lib/typescript/src/web/TrackPlayer/SleepTimer.d.ts.map +0 -1
  839. package/lib/typescript/src/web/TrackPlayer/State.d.ts +0 -13
  840. package/lib/typescript/src/web/TrackPlayer/State.d.ts.map +0 -1
  841. package/lib/typescript/src/web/TrackPlayer/index.d.ts +0 -5
  842. package/lib/typescript/src/web/TrackPlayer/index.d.ts.map +0 -1
  843. package/lib/typescript/src/web/browser/BrowserManager.d.ts +0 -154
  844. package/lib/typescript/src/web/browser/BrowserManager.d.ts.map +0 -1
  845. package/lib/typescript/src/web/browser/FavoriteManager.d.ts +0 -42
  846. package/lib/typescript/src/web/browser/FavoriteManager.d.ts.map +0 -1
  847. package/lib/typescript/src/web/browser/NavigationErrorManager.d.ts +0 -40
  848. package/lib/typescript/src/web/browser/NavigationErrorManager.d.ts.map +0 -1
  849. package/lib/typescript/src/web/browser/SearchManager.d.ts +0 -22
  850. package/lib/typescript/src/web/browser/SearchManager.d.ts.map +0 -1
  851. package/lib/typescript/src/web/http/HttpClient.d.ts +0 -25
  852. package/lib/typescript/src/web/http/HttpClient.d.ts.map +0 -1
  853. package/lib/typescript/src/web/http/RequestConfigBuilder.d.ts +0 -98
  854. package/lib/typescript/src/web/http/RequestConfigBuilder.d.ts.map +0 -1
  855. package/lib/typescript/src/web/player/NowPlayingManager.d.ts +0 -34
  856. package/lib/typescript/src/web/player/NowPlayingManager.d.ts.map +0 -1
  857. package/lib/typescript/src/web/player/OptionsManager.d.ts +0 -32
  858. package/lib/typescript/src/web/player/OptionsManager.d.ts.map +0 -1
  859. package/lib/typescript/src/web/util/BrowserPathHelper.d.ts +0 -69
  860. package/lib/typescript/src/web/util/BrowserPathHelper.d.ts.map +0 -1
  861. package/lib/typescript/src/web/util/shuffle.d.ts +0 -8
  862. package/lib/typescript/src/web/util/shuffle.d.ts.map +0 -1
  863. package/nitrogen/generated/android/c++/JAndroidUpdateOptions.hpp +0 -86
  864. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError______BrowserSourceCallbackParam.hpp +0 -147
  865. package/nitrogen/generated/android/c++/JFunc_void_IosOutput.hpp +0 -80
  866. package/nitrogen/generated/android/c++/JFunc_void_RemoteSetRatingEvent.hpp +0 -87
  867. package/nitrogen/generated/android/c++/JHeartRating.hpp +0 -57
  868. package/nitrogen/generated/android/c++/JIosOutputType.hpp +0 -85
  869. package/nitrogen/generated/android/c++/JPartialSetupPlayerOptions.hpp +0 -96
  870. package/nitrogen/generated/android/c++/JPercentageRating.hpp +0 -57
  871. package/nitrogen/generated/android/c++/JRatingType.hpp +0 -73
  872. package/nitrogen/generated/android/c++/JRemoteSetRatingEvent.hpp +0 -66
  873. package/nitrogen/generated/android/c++/JStarRating.hpp +0 -57
  874. package/nitrogen/generated/android/c++/JUpdateOptions.hpp +0 -105
  875. package/nitrogen/generated/android/c++/JVariant_HeartRating_ThumbsRating_StarRating_PercentageRating.cpp +0 -34
  876. package/nitrogen/generated/android/c++/JVariant_HeartRating_ThumbsRating_StarRating_PercentageRating.hpp +0 -105
  877. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidUpdateOptions.kt +0 -50
  878. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IosOutputType.kt +0 -32
  879. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PartialSetupPlayerOptions.kt +0 -47
  880. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/UpdateOptions.kt +0 -53
  881. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_HeartRating_ThumbsRating_StarRating_PercentageRating.kt +0 -77
  882. package/nitrogen/generated/ios/swift/AndroidUpdateOptions.swift +0 -119
  883. package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError______BrowserSourceCallbackParam.swift +0 -68
  884. package/nitrogen/generated/ios/swift/HeartRating.swift +0 -29
  885. package/nitrogen/generated/ios/swift/IosOutputType.swift +0 -76
  886. package/nitrogen/generated/ios/swift/PartialSetupPlayerOptions.swift +0 -101
  887. package/nitrogen/generated/ios/swift/PercentageRating.swift +0 -29
  888. package/nitrogen/generated/ios/swift/RatingType.swift +0 -60
  889. package/nitrogen/generated/ios/swift/RemoteSetRatingEvent.swift +0 -58
  890. package/nitrogen/generated/ios/swift/StarRating.swift +0 -29
  891. package/nitrogen/generated/ios/swift/ThumbsRating.swift +0 -29
  892. package/nitrogen/generated/ios/swift/UpdateOptions.swift +0 -143
  893. package/nitrogen/generated/ios/swift/Variant_HeartRating_ThumbsRating_StarRating_PercentageRating.swift +0 -20
  894. package/nitrogen/generated/shared/c++/AndroidUpdateOptions.hpp +0 -109
  895. package/nitrogen/generated/shared/c++/IosOutputType.hpp +0 -112
  896. package/nitrogen/generated/shared/c++/PartialSetupPlayerOptions.hpp +0 -104
  897. package/nitrogen/generated/shared/c++/RatingType.hpp +0 -96
  898. package/nitrogen/generated/shared/c++/RemoteSetRatingEvent.hpp +0 -94
  899. package/nitrogen/generated/shared/c++/UpdateOptions.hpp +0 -111
  900. package/src/features/rating.ts +0 -17
  901. package/src/types/player.ts +0 -69
  902. package/src/web/TrackPlayer/PlaylistPlayer.ts +0 -346
@@ -29,16 +29,48 @@ public final class RNABCarPlayController: NSObject {
29
29
  private var isStarted = false
30
30
  private var listenerRemovals: [() -> Void] = []
31
31
 
32
- /// Current navigation stack paths (for back navigation context)
33
- private var navigationStack: [String] = []
34
-
35
- /// Key (src ?? url) of the item currently being selected.
36
- /// Prevents duplicate taps from triggering parallel async operations.
37
- private var pendingSelectionKey: String?
38
-
39
- /// URLs currently being resolved for navigation.
40
- /// Prevents duplicate taps from pushing the same template twice.
41
- private var pendingNavigationUrls: Set<String> = []
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)
42
74
 
43
75
  /// Helper object for CPInterfaceControllerDelegate conformance
44
76
  private var interfaceDelegate: InterfaceControllerDelegate?
@@ -57,9 +89,21 @@ public final class RNABCarPlayController: NSObject {
57
89
  audioBrowser?.browserManager.config ?? BrowserConfig()
58
90
  }
59
91
 
60
- /// Checks if the given src matches the currently active (loaded) track
61
- private func isActiveTrack(src: String) -> Bool {
62
- audioBrowser?.getPlayer()?.currentTrack?.src == src
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
63
107
  }
64
108
 
65
109
  // MARK: - Initialization
@@ -74,10 +118,22 @@ public final class RNABCarPlayController: NSObject {
74
118
  self?.listItemFactory?.createListItem(for: track, handler: handler)
75
119
  ?? CPListItem(text: track.title, detailText: nil)
76
120
  }
121
+ nowPlayingManager.navigateToUrl = { [weak self] url, title in
122
+ Task { @MainActor in await self?.navigateToUrl(url, title: title) }
123
+ }
77
124
  }
78
125
 
79
126
  // MARK: - Lifecycle
80
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.
132
+ @objc
133
+ public static func setConnected(_ connected: Bool) {
134
+ HybridAudioBrowser.setCarPlayConnected(connected)
135
+ }
136
+
81
137
  @objc
82
138
  public func start() {
83
139
  guard !isStarted else { return }
@@ -90,6 +146,18 @@ public final class RNABCarPlayController: NSObject {
90
146
  interfaceDelegate = delegate
91
147
  interfaceController.delegate = delegate
92
148
 
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
+ }
156
+ }
157
+ listenerRemovals.append {
158
+ HybridAudioBrowser.instanceChangedEmitter.removeListener(instanceToken)
159
+ }
160
+
93
161
  // Show loading template while waiting
94
162
  showLoadingTemplate()
95
163
 
@@ -99,20 +167,21 @@ public final class RNABCarPlayController: NSObject {
99
167
  guard self.isStarted else { return }
100
168
  self.logger.debug("AudioBrowser and player ready, setting up CarPlay")
101
169
  self.audioBrowser = browser
170
+ self.isGated = browser.isGateActive
102
171
  self.trackSelector = TrackSelector(browserManager: browser.browserManager)
103
172
 
104
173
  // Create image loader with CarPlay display traits
105
174
  self.imageLoader = CarPlayImageLoader(
106
175
  carTraitCollection: self.interfaceController.carTraitCollection,
107
- browserManager: browser.browserManager
176
+ browserManager: browser.browserManager,
108
177
  )
109
178
 
110
179
  // Create list item factory
111
180
  let factory = CarPlayListItemFactory(
112
- isActiveTrack: { [weak self] src in self?.isActiveTrack(src: src) ?? false },
181
+ isActiveTrack: { [weak self] id, src in self?.isActiveTrack(id: id, src: src) ?? false },
113
182
  onItemSelected: { [weak self] track, completion in
114
183
  self?.handleItemSelection(track: track, completion: completion)
115
- }
184
+ },
116
185
  )
117
186
  factory.imageLoader = self.imageLoader
118
187
  self.listItemFactory = factory
@@ -134,18 +203,33 @@ public final class RNABCarPlayController: NSObject {
134
203
  logger.info("Stopping CarPlay controller")
135
204
 
136
205
  // Remove all emitter listeners
137
- for removal in listenerRemovals { removal() }
206
+ for removal in listenerRemovals {
207
+ removal()
208
+ }
138
209
  listenerRemovals.removeAll()
139
210
 
140
211
  // Clear config callback
141
212
  audioBrowser?.browserManager.onConfigChanged = nil
213
+ audioBrowser?.browserManager.onFavoritesChanged = nil
142
214
 
143
215
  nowPlayingManager.teardown()
144
216
  listItemFactory = nil
145
217
 
146
- navigationStack.removeAll()
147
- pendingSelectionKey = nil
148
- pendingNavigationUrls.removeAll()
218
+ navigatingPaths.removeAll()
219
+ loadingTemplates.removeAll()
220
+ pendingTabs = nil
221
+ }
222
+
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
+ private func restart() {
229
+ guard isStarted else { return }
230
+ logger.info("AudioBrowser instance changed — restarting CarPlay controller")
231
+ stop()
232
+ start()
149
233
  }
150
234
 
151
235
  // MARK: - Content Subscriptions
@@ -176,10 +260,35 @@ public final class RNABCarPlayController: NSObject {
176
260
  audioBrowser?.contentChangedEmitter.removeListener(contentToken)
177
261
  }
178
262
 
179
- // Subscribe to config changes (for Now Playing buttons)
263
+ // Subscribe to config changes (for Now Playing buttons and per-track
264
+ // button state — e.g. resolveAlbumUrl appearing/disappearing)
180
265
  audioBrowser.browserManager.onConfigChanged = { [weak self] _ in
181
266
  Task { @MainActor in
182
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()
183
292
  }
184
293
  }
185
294
 
@@ -193,14 +302,54 @@ public final class RNABCarPlayController: NSObject {
193
302
  audioBrowser?.favoriteChangedEmitter.removeListener(favoriteToken)
194
303
  }
195
304
 
196
- // Subscribe to external content changes (from notifyContentChanged)
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).
197
328
  let externalContentToken = audioBrowser.externalContentChangedEmitter.addListener { [weak self] path in
198
- self?.notifyContentChanged(path: path)
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
+ }
199
338
  }
200
339
  listenerRemovals.append { [weak audioBrowser] in
201
340
  audioBrowser?.externalContentChangedEmitter.removeListener(externalContentToken)
202
341
  }
203
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
+
204
353
  // Subscribe to active track changes (for playing indicator in lists)
205
354
  let activeTrackToken = audioBrowser.activeTrackChangedEmitter.addListener { [weak self] event in
206
355
  Task { @MainActor in
@@ -221,6 +370,17 @@ public final class RNABCarPlayController: NSObject {
221
370
  audioBrowser?.queueChangedEmitter.removeListener(queueToken)
222
371
  }
223
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
+
224
384
  // Subscribe to navigation errors (from browser layer)
225
385
  let navErrorToken = audioBrowser.navigationErrorEmitter.addListener { [weak self] event in
226
386
  Task { @MainActor in
@@ -237,8 +397,9 @@ public final class RNABCarPlayController: NSObject {
237
397
  private func handleNavigationError(_ event: NavigationErrorEvent) {
238
398
  guard let error = event.error else { return }
239
399
  logger.warning("Navigation error: \(error.code.stringValue) - \(error.message)")
240
- // Use current path from navigation stack, or "/" as fallback
241
- let path = navigationStack.last ?? "/"
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) } ?? "/"
242
403
  showNavigationError(error, path: path)
243
404
  }
244
405
 
@@ -248,7 +409,9 @@ public final class RNABCarPlayController: NSObject {
248
409
  private func buildInitialInterface() async {
249
410
  guard let audioBrowser else {
250
411
  logger.error("AudioBrowser not available")
251
- showErrorTemplate(message: "Audio browser not initialized")
412
+ await showRootNavigationError(
413
+ NavigationError(code: .unknownError, message: "", statusCode: nil, statusCodeSuccess: nil),
414
+ )
252
415
  return
253
416
  }
254
417
 
@@ -260,33 +423,65 @@ public final class RNABCarPlayController: NSObject {
260
423
  } else {
261
424
  // No tabs yet - query them
262
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
+ }
263
432
  do {
264
433
  let queriedTabs = try await audioBrowser.browserManager.queryTabs()
265
434
  if !queriedTabs.isEmpty {
266
435
  await showTabBar(tabs: queriedTabs)
267
436
  } else {
268
- showErrorTemplate(message: "No content available")
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
+ )
269
442
  }
270
443
  } catch {
271
444
  logger.error("Failed to query tabs: \(error.localizedDescription)")
272
- showErrorTemplate(message: "Failed to load content")
445
+ await showRootNavigationError(NavigationError.from(error))
273
446
  }
274
447
  }
275
448
  }
276
449
 
277
- /// Shows a loading template while waiting for initialization
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.
278
454
  private func showLoadingTemplate() {
279
- let template = CPListTemplate(
280
- title: nil,
281
- sections: [],
282
- )
283
- interfaceController.setRootTemplate(template, animated: false, completion: nil)
455
+ let template = makeLoadingTemplate(title: nil, path: nil)
456
+ interfaceController.safeSetRoot(template, animated: false)
284
457
  }
285
458
 
286
459
  // MARK: - Tab Bar
287
460
 
288
461
  @MainActor
289
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
+
290
485
  logger.info("Building tab bar with \(tabs.count) tabs")
291
486
 
292
487
  let maxTabs = CPTabBarTemplate.maximumTabCount
@@ -296,10 +491,14 @@ public final class RNABCarPlayController: NSObject {
296
491
  createTabTemplate(for: tab)
297
492
  }
298
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
+
299
498
  // Set the tab bar immediately so UI appears fast
300
499
  logger.info("Setting tab bar root template with \(tabTemplates.count) templates")
301
500
  let tabBar = CPTabBarTemplate(templates: tabTemplates)
302
- interfaceController.setRootTemplate(tabBar, animated: true, completion: nil)
501
+ interfaceController.safeSetRoot(tabBar, animated: true)
303
502
 
304
503
  // Load content for the first tab only - others load lazily when selected
305
504
  if let firstTemplate = tabTemplates.first, let firstTab = tabs.first, let url = firstTab.url {
@@ -307,27 +506,93 @@ public final class RNABCarPlayController: NSObject {
307
506
  }
308
507
  }
309
508
 
310
- /// Creates a tab template shell without loading content (synchronous)
311
- private func createTabTemplate(for track: Track) -> CPListTemplate {
312
- let template = CPListTemplate(
313
- title: track.title,
314
- sections: [],
315
- )
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 }
316
521
 
317
- // Set tab title explicitly (required for tab bar display)
318
- template.tabTitle = track.title
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
+ }
319
565
 
320
- // Store path for lazy loading and refresh
321
- if let url = track.url {
322
- template.userInfo = ["path": url] as [String: Any]
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)
323
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
+ }
324
583
 
325
- // Set tab image - CarPlay requires an image for proper tab display
326
- // Tab bar icons are 24pt x 24pt per CarPlay Developer Guide
327
- // https://developer.apple.com/download/files/CarPlay-Developer-Guide.pdf
328
- template.tabImage = imageLoader?.defaultTabImage()
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
329
590
 
330
- if let artwork = track.artwork, SFSymbolRenderer.isSFSymbol(artwork) {
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) {
331
596
  let (symbolName, _, _) = SFSymbolRenderer.parseArtwork(artwork)
332
597
  if let image = imageLoader?.sfSymbolImage(symbolName) {
333
598
  template.tabImage = image
@@ -342,28 +607,15 @@ public final class RNABCarPlayController: NSObject {
342
607
  }
343
608
  }
344
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
345
614
  }
346
-
347
- return template
348
615
  }
349
616
 
350
617
  // MARK: - List Templates
351
618
 
352
- @MainActor
353
- private func createListTemplate(
354
- for resolvedTrack: ResolvedTrack,
355
- path: String,
356
- ) -> CPListTemplate {
357
- let template = CPListTemplate(
358
- title: resolvedTrack.title,
359
- sections: [],
360
- )
361
- updateTemplate(template, with: resolvedTrack)
362
- template.userInfo = ["path": path] as [String: Any]
363
-
364
- return template
365
- }
366
-
367
619
  /// Finds the path associated with a template, if any
368
620
  private func getPath(from template: CPTemplate) -> String? {
369
621
  (template.userInfo as? [String: Any])?["path"] as? String
@@ -379,7 +631,6 @@ public final class RNABCarPlayController: NSObject {
379
631
  /// Configures the assistant cell ("Ask Siri to Play Audio") on a template
380
632
  /// based on the `carPlaySiriListButton` property of the resolved content.
381
633
  private func configureAssistantCell(on template: CPListTemplate, from resolvedTrack: ResolvedTrack) {
382
- guard #available(iOS 15.4, *) else { return }
383
634
  guard let position = resolvedTrack.carPlaySiriListButton else {
384
635
  template.assistantCellConfiguration = nil
385
636
  return
@@ -394,15 +645,96 @@ public final class RNABCarPlayController: NSObject {
394
645
 
395
646
  // MARK: - Content Loading
396
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.
397
653
  @MainActor
398
654
  private func loadContent(for path: String, into template: CPListTemplate) async {
399
655
  guard let audioBrowser else { return }
400
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
+
401
678
  do {
402
679
  let resolved = try await audioBrowser.browserManager.resolve(path, useCache: true)
403
- updateTemplate(template, with: resolved)
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
+ }
404
696
  } catch {
697
+ watchdog.cancel()
405
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
406
738
  }
407
739
  }
408
740
 
@@ -416,17 +748,8 @@ public final class RNABCarPlayController: NSObject {
416
748
  return
417
749
  }
418
750
 
419
- // Deduplicate: ignore if the same item is already being loaded
420
- let key = track.src ?? track.url
421
- if let key, key == pendingSelectionKey {
422
- completion()
423
- return
424
- }
425
- pendingSelectionKey = key
426
-
427
- // If this track is already loaded, resume playback and show Now Playing
428
- if let src = track.src, isActiveTrack(src: src) {
429
- pendingSelectionKey = nil
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) {
430
753
  try? audioBrowser.play()
431
754
  nowPlayingManager.showNowPlaying()
432
755
  completion()
@@ -434,78 +757,171 @@ public final class RNABCarPlayController: NSObject {
434
757
  }
435
758
 
436
759
  guard let player = audioBrowser.getPlayer(), let trackSelector else {
437
- pendingSelectionKey = nil
438
760
  completion()
439
761
  return
440
762
  }
441
763
 
442
- Task {
443
- defer { self.pendingSelectionKey = nil }
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
444
785
  let result = await trackSelector.select(track: track, player: player)
786
+ guard let self else { return }
445
787
  switch result {
446
- case .play(let intent):
788
+ case let .play(intent):
447
789
  self.executePlayback(intent, player: player)
448
- self.nowPlayingManager.showNowPlaying()
790
+ self.nowPlayingManager.showNowPlaying(popToFront: false)
449
791
  case .intercepted:
450
- self.nowPlayingManager.showNowPlaying()
451
- case .browse(let url):
452
- self.navigateToUrl(url, completion: completion)
453
- return // navigateToUrl handles its own completion
792
+ self.nowPlayingManager.showNowPlaying(popToFront: false)
793
+ case let .browse(url):
794
+ await self.navigateToUrl(url, title: track.title)
454
795
  case .none:
455
796
  break
456
797
  }
457
- completion()
458
798
  }
459
799
  }
460
800
 
461
801
  private func executePlayback(_ intent: TrackSelector.PlaybackIntent, player: TrackPlayer) {
462
802
  switch intent {
463
- case .skipTo(let index):
803
+ case let .skipTo(index):
464
804
  try? player.skipTo(index, playWhenReady: true)
465
- case .setQueue(let tracks, let startIndex, let sourcePath):
805
+ case let .setQueue(tracks, startIndex, sourcePath):
466
806
  player.setQueue(tracks, initialIndex: startIndex, playWhenReady: true, sourcePath: sourcePath)
467
- case .loadTrack(let track):
807
+ case let .loadTrack(track):
468
808
  player.load(track, playWhenReady: true)
469
809
  }
470
810
  }
471
811
 
472
- /// Navigates to a browsable URL path, showing error action sheet on failure with retry option.
473
- private func navigateToUrl(_ url: String, completion: @escaping () -> Void) {
474
- guard let audioBrowser else {
475
- completion()
476
- return
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
+ }
477
835
  }
478
-
479
- // Deduplicate: ignore if already navigating to this URL
480
- guard !pendingNavigationUrls.contains(url) else {
481
- completion()
836
+ // Avoid pushing a duplicate if the top template already shows this path.
837
+ if let top = interfaceController.topTemplate, getPath(from: top) == url {
482
838
  return
483
839
  }
484
- pendingNavigationUrls.insert(url)
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
+ }
485
858
 
486
- Task {
487
- defer { self.pendingNavigationUrls.remove(url) }
488
- do {
489
- let resolved = try await audioBrowser.browserManager.resolve(url, useCache: true)
490
-
491
- // Guard against duplicate push if top template already shows this path
492
- if let top = self.interfaceController.topTemplate,
493
- self.getPath(from: top) == url
494
- {
495
- completion()
496
- return
497
- }
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
+ }
498
880
 
499
- let listTemplate = self.createListTemplate(for: resolved, path: url)
500
- self.navigationStack.append(url)
501
- self.interfaceController.pushTemplate(listTemplate, animated: true, completion: nil)
502
- completion()
503
- } catch {
504
- logger.error("Failed to navigate to \(url): \(error.localizedDescription)")
505
- let navError = NavigationError.from(error)
506
- self.showNavigationError(navError, path: url)
507
- completion()
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
508
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)
509
925
  }
510
926
  }
511
927
 
@@ -514,12 +930,49 @@ public final class RNABCarPlayController: NSObject {
514
930
  @MainActor
515
931
  private func handleTabsChanged(_ tabs: [Track]) {
516
932
  logger.debug("Tabs changed: \(tabs.count) tabs")
517
- // Rebuild tab bar if we're at the root
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
518
956
  Task {
519
957
  await showTabBar(tabs: tabs)
520
958
  }
521
959
  }
522
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
+
523
976
  @MainActor
524
977
  private func handleContentChanged(_ content: ResolvedTrack?) {
525
978
  // This callback fires when the main browser's content changes.
@@ -532,8 +985,9 @@ public final class RNABCarPlayController: NSObject {
532
985
  private func handleActiveTrackChanged(_ event: PlaybackActiveTrackChangedEvent) {
533
986
  logger.debug("handleActiveTrackChanged: \(event.lastTrack?.src ?? "nil") → \(event.track?.src ?? "nil")")
534
987
  updatePlayingIndicators()
535
- // Update favorite button to reflect the new track's favorite state
536
- nowPlayingManager.updateFavoriteButtonState()
988
+ // Refresh per-track button state: favorite heart, Up Next availability,
989
+ // and the album line's pre-resolved destination.
990
+ nowPlayingManager.handleActiveTrackChanged()
537
991
  }
538
992
 
539
993
  /// Updates the isPlaying state on all list items based on the current active track.
@@ -556,19 +1010,31 @@ public final class RNABCarPlayController: NSObject {
556
1010
  }
557
1011
 
558
1012
  for template in templates {
1013
+ var templateChanged = false
559
1014
  for section in template.sections {
560
1015
  for item in section.items {
561
1016
  guard let listItem = item as? CPListItem,
562
- let itemSrc = listItem.carPlayItemInfo?.src
1017
+ let info = listItem.carPlayItemInfo,
1018
+ info.src != nil
563
1019
  else { continue }
564
1020
 
565
- let isPlaying = isActiveTrack(src: itemSrc)
1021
+ let isPlaying = isActiveTrack(id: info.id, src: info.src)
566
1022
  if listItem.isPlaying != isPlaying {
567
- logger.debug("Updating isPlaying for \(itemSrc): \(listItem.isPlaying) → \(isPlaying)")
1023
+ logger.debug(
1024
+ "Updating isPlaying for \(info.id ?? info.src ?? ""): \(listItem.isPlaying) → \(isPlaying)")
568
1025
  listItem.isPlaying = isPlaying
1026
+ templateChanged = true
569
1027
  }
570
1028
  }
571
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
+ }
572
1038
  }
573
1039
  }
574
1040
 
@@ -632,6 +1098,108 @@ public final class RNABCarPlayController: NSObject {
632
1098
  }
633
1099
  }
634
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
+
635
1203
  // MARK: - Error Handling
636
1204
 
637
1205
  /// Shows a navigation error using CPActionSheetTemplate.
@@ -667,7 +1235,7 @@ public final class RNABCarPlayController: NSObject {
667
1235
  private func presentErrorActionSheet(customDisplay: FormattedNavigationError) {
668
1236
  // If another template is already presented, dismiss it first
669
1237
  if interfaceController.presentedTemplate != nil {
670
- interfaceController.dismissTemplate(animated: false) { [weak self] _, _ in
1238
+ interfaceController.safeDismiss(animated: false) { [weak self] _, _ in
671
1239
  self?.showErrorActionSheet(customDisplay: customDisplay)
672
1240
  }
673
1241
  } else {
@@ -680,7 +1248,7 @@ public final class RNABCarPlayController: NSObject {
680
1248
  // OK action - dismiss the action sheet (use system-localized "OK")
681
1249
  let okTitle = Bundle(for: UIAlertController.self).localizedString(forKey: "OK", value: "OK", table: nil)
682
1250
  let ok = CPAlertAction(title: okTitle, style: .cancel) { [weak self] _ in
683
- self?.interfaceController.dismissTemplate(animated: true, completion: nil)
1251
+ self?.interfaceController.safeDismiss(animated: true)
684
1252
  }
685
1253
 
686
1254
  let actionSheet = CPActionSheetTemplate(
@@ -689,19 +1257,20 @@ public final class RNABCarPlayController: NSObject {
689
1257
  actions: [ok],
690
1258
  )
691
1259
 
692
- interfaceController.presentTemplate(actionSheet, animated: true, completion: nil)
1260
+ interfaceController.safePresent(actionSheet, animated: true)
693
1261
  }
694
1262
 
695
- /// Shows a simple error template as root (for initialization errors when no other template exists)
696
- private func showErrorTemplate(message: String) {
697
- // CPAlertTemplate cannot be set as root - use a list template instead
698
- let errorItem = CPListItem(text: message, detailText: nil)
699
- errorItem.isEnabled = false
700
- let template = CPListTemplate(
701
- title: "Error",
702
- sections: [CPListSection(items: [errorItem])],
703
- )
704
- interfaceController.setRootTemplate(template, animated: true, completion: nil)
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)
705
1274
  }
706
1275
  }
707
1276
 
@@ -717,13 +1286,30 @@ private final class InterfaceControllerDelegate: NSObject, CPInterfaceController
717
1286
  }
718
1287
 
719
1288
  func templateDidAppear(_ aTemplate: CPTemplate, animated _: Bool) {
720
- guard let listTemplate = aTemplate as? CPListTemplate else { return }
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 }
721
1297
 
722
1298
  // Update playing indicators when navigating back to a list template
723
1299
  controller?.updatePlayingIndicators()
724
1300
 
725
1301
  // Lazy load content for tabs that haven't been loaded yet
726
- controller?.loadContentIfNeeded(for: listTemplate)
1302
+ if let listTemplate = aTemplate as? CPListTemplate {
1303
+ controller?.loadContentIfNeeded(for: listTemplate)
1304
+ }
1305
+ }
1306
+
1307
+ 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()
727
1313
  }
728
1314
  }
729
1315
 
@@ -732,12 +1318,16 @@ private final class InterfaceControllerDelegate: NSObject, CPInterfaceController
732
1318
  private extension RNABCarPlayController {
733
1319
  /// Loads content for a template if it hasn't been loaded yet (lazy loading for tabs)
734
1320
  func loadContentIfNeeded(for template: CPListTemplate) {
735
- // Skip if already has content
736
- guard template.sections.isEmpty else { return }
737
-
738
1321
  // Get path from userInfo
739
1322
  guard let path = getPath(from: template) else { return }
740
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
+
741
1331
  logger.debug("Lazy loading content for tab: \(path)")
742
1332
 
743
1333
  Task {