react-native-audio-browser 0.2.0-next.10 → 0.2.0-next.12

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 (345) hide show
  1. package/README.md +4 -4
  2. package/android/build.gradle +6 -5
  3. package/android/gradle.properties +1 -1
  4. package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +110 -25
  5. package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +254 -188
  6. package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +23 -3
  7. package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +77 -58
  8. package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +68 -26
  9. package/android/src/main/java/com/audiobrowser/browser/StyleResolver.kt +49 -0
  10. package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +9 -7
  11. package/android/src/main/java/com/audiobrowser/extension/TappedTrack.kt +20 -0
  12. package/android/src/main/java/com/audiobrowser/extension/TrackIdentity.kt +15 -0
  13. package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +120 -67
  14. package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +21 -24
  15. package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +93 -73
  16. package/android/src/main/java/com/audiobrowser/player/Player.kt +48 -30
  17. package/android/src/main/java/com/audiobrowser/player/PlayerCapabilitiesExt.kt +2 -15
  18. package/android/src/main/java/com/audiobrowser/tls/AiaCertChaser.kt +46 -5
  19. package/android/src/main/java/com/audiobrowser/tls/AiaChasingExtendedTrustManager.kt +86 -27
  20. package/android/src/main/java/com/audiobrowser/tls/AiaChasingTrustManager.kt +58 -14
  21. package/android/src/main/java/com/audiobrowser/tls/AiaTls.kt +20 -6
  22. package/android/src/main/java/com/audiobrowser/tls/CachingCertificateFetcher.kt +249 -86
  23. package/android/src/main/java/com/audiobrowser/util/BrowserPathHelper.kt +44 -40
  24. package/android/src/main/java/com/audiobrowser/util/MediaExtrasBuilder.kt +28 -23
  25. package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +35 -0
  26. package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +45 -53
  27. package/android/src/test/java/com/audiobrowser/TestFixtures.kt +69 -30
  28. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerFavoritesTest.kt +153 -0
  29. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerLayerResolutionTest.kt +6 -0
  30. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +345 -8
  31. package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +38 -0
  32. package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +1 -1
  33. package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +91 -0
  34. package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +128 -53
  35. package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +87 -0
  36. package/android/src/test/java/com/audiobrowser/extension/TappedTrackTest.kt +43 -0
  37. package/android/src/test/java/com/audiobrowser/extension/TrackIdentityTest.kt +41 -0
  38. package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +1 -2
  39. package/android/src/test/java/com/audiobrowser/player/NowPlayingUpdaterTest.kt +42 -2
  40. package/android/src/test/java/com/audiobrowser/player/PlaybackStateStoreTrackJsonTest.kt +55 -0
  41. package/android/src/test/java/com/audiobrowser/tls/AiaCertChaserTest.kt +152 -8
  42. package/android/src/test/java/com/audiobrowser/tls/AiaChasingTrustManagerTest.kt +218 -10
  43. package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherCallSiteTest.kt +322 -0
  44. package/android/src/test/java/com/audiobrowser/util/BrowserPathHelperTest.kt +41 -16
  45. package/android/src/test/java/com/audiobrowser/util/MediaExtrasBuilderTest.kt +161 -0
  46. package/android/src/test/java/com/audiobrowser/util/TrackFactoryMediaIdTest.kt +7 -7
  47. package/ios/Browser/BrowserConfig.swift +102 -5
  48. package/ios/Browser/BrowserManager+URLResolution.swift +18 -14
  49. package/ios/Browser/BrowserManager.swift +131 -143
  50. package/ios/Browser/BrowserPathHelper.swift +44 -44
  51. package/ios/Browser/JsonModels.swift +187 -106
  52. package/ios/Browser/SearchDrillIn.swift +6 -6
  53. package/ios/Browser/SectionPresentation.swift +34 -0
  54. package/ios/Browser/SectionScope.swift +58 -24
  55. package/ios/Browser/StyleResolver.swift +47 -0
  56. package/ios/Browser/TabBarEntries.swift +2 -2
  57. package/ios/Browser/TrackIdTemplate.swift +37 -0
  58. package/ios/CarPlay/CarPlayArtworkResolver.swift +8 -7
  59. package/ios/CarPlay/CarPlayController.swift +105 -67
  60. package/ios/CarPlay/CarPlayImageLoader.swift +38 -18
  61. package/ios/CarPlay/CarPlayListItemFactory.swift +217 -84
  62. package/ios/CarPlay/CarPlayNowPlayingManager.swift +15 -12
  63. package/ios/Extension/Capability+RemoteCommand.swift +1 -1
  64. package/ios/Extension/PlayerCapabilities+Favorite.swift +1 -15
  65. package/ios/Extension/ResolvedTrack+Copying.swift +11 -15
  66. package/ios/Extension/Section+Copying.swift +31 -0
  67. package/ios/Extension/Track+Copying.swift +6 -12
  68. package/ios/Extension/Track+Identity.swift +18 -0
  69. package/ios/HybridAudioBrowser.swift +113 -35
  70. package/ios/Model/NitroTypeStubs.swift +64 -23
  71. package/ios/Model/PlayerUpdateOptions.swift +7 -0
  72. package/ios/Player/NowPlayingUpdater.swift +1 -1
  73. package/ios/Player/PlaybackCoordinator.swift +2 -2
  74. package/ios/Player/RetryManager.swift +4 -3
  75. package/ios/Tests/BrowserPathHelperTests.swift +50 -20
  76. package/ios/Tests/CarPlayArtworkResolverTests.swift +24 -41
  77. package/ios/Tests/JsonModelsTests.swift +75 -1
  78. package/ios/Tests/PlaybackStateStoreTests.swift +4 -3
  79. package/ios/Tests/QueueManagerTests.swift +4 -4
  80. package/ios/Tests/ResolvedTrackCopyingTests.swift +37 -35
  81. package/ios/Tests/SearchDrillInTests.swift +16 -16
  82. package/ios/Tests/SectionPresentationTests.swift +61 -0
  83. package/ios/Tests/SectionScopeTests.swift +87 -55
  84. package/ios/Tests/StyleResolverTests.swift +67 -0
  85. package/ios/Tests/TabBarEntriesTests.swift +18 -18
  86. package/ios/Tests/TrackIdTemplateTests.swift +67 -0
  87. package/ios/Tests/TrackIdentityTests.swift +20 -0
  88. package/ios/Tests/TrackSelectorTests.swift +168 -33
  89. package/ios/TrackSelector.swift +45 -30
  90. package/ios/Util/ArtworkImageFetcher.swift +16 -1
  91. package/lib/commonjs/AudioBrowser.js +11 -0
  92. package/lib/commonjs/AudioBrowser.js.map +1 -1
  93. package/lib/commonjs/features/browser-config.js +11 -8
  94. package/lib/commonjs/features/browser-config.js.map +1 -1
  95. package/lib/commonjs/features/browser.js +2 -2
  96. package/lib/commonjs/features/favorites.js +13 -10
  97. package/lib/commonjs/features/favorites.js.map +1 -1
  98. package/lib/commonjs/features/playback/controls.js +4 -2
  99. package/lib/commonjs/features/playback/controls.js.map +1 -1
  100. package/lib/commonjs/features/playback/state.js +2 -3
  101. package/lib/commonjs/features/playback/state.js.map +1 -1
  102. package/lib/commonjs/features/player/options.js.map +1 -1
  103. package/lib/commonjs/features/player/setup.js +3 -1
  104. package/lib/commonjs/features/player/setup.js.map +1 -1
  105. package/lib/commonjs/features/player/validateOptions.js +2 -0
  106. package/lib/commonjs/features/player/validateOptions.js.map +1 -1
  107. package/lib/commonjs/features/queue/activeTrack.js +14 -3
  108. package/lib/commonjs/features/queue/activeTrack.js.map +1 -1
  109. package/lib/commonjs/features/queue/queue.js +11 -4
  110. package/lib/commonjs/features/queue/queue.js.map +1 -1
  111. package/lib/commonjs/utils/LazyNativeEmitter.js +21 -10
  112. package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
  113. package/lib/commonjs/utils/NativeUpdatedValue.js +31 -13
  114. package/lib/commonjs/utils/NativeUpdatedValue.js.map +1 -1
  115. package/lib/commonjs/utils/getTrackIdentity.js +24 -0
  116. package/lib/commonjs/utils/getTrackIdentity.js.map +1 -0
  117. package/lib/commonjs/utils/useNativeValueRefreshedBy.js +33 -0
  118. package/lib/commonjs/utils/useNativeValueRefreshedBy.js.map +1 -0
  119. package/lib/commonjs/web/NativeAudioBrowser.js +32 -24
  120. package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
  121. package/lib/commonjs/web/browser/BrowserManager.js +144 -87
  122. package/lib/commonjs/web/browser/BrowserManager.js.map +1 -1
  123. package/lib/commonjs/web/browser/FavoriteManager.js +30 -21
  124. package/lib/commonjs/web/browser/FavoriteManager.js.map +1 -1
  125. package/lib/commonjs/web/browser/assertBrowsePageShape.js +2 -2
  126. package/lib/commonjs/web/browser/assertBrowsePageShape.js.map +1 -1
  127. package/lib/commonjs/web/browser/classifyTrackNavigation.js +9 -9
  128. package/lib/commonjs/web/browser/classifyTrackNavigation.js.map +1 -1
  129. package/lib/commonjs/web/browser/sections.js +89 -0
  130. package/lib/commonjs/web/browser/sections.js.map +1 -0
  131. package/lib/commonjs/web/player/NowPlayingManager.js +4 -3
  132. package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
  133. package/lib/commonjs/web/util/BrowserPathHelper.js +56 -11
  134. package/lib/commonjs/web/util/BrowserPathHelper.js.map +1 -1
  135. package/lib/module/AudioBrowser.js +1 -0
  136. package/lib/module/AudioBrowser.js.map +1 -1
  137. package/lib/module/features/browser-config.js +11 -8
  138. package/lib/module/features/browser-config.js.map +1 -1
  139. package/lib/module/features/browser.js +2 -2
  140. package/lib/module/features/favorites.js +13 -10
  141. package/lib/module/features/favorites.js.map +1 -1
  142. package/lib/module/features/playback/controls.js +4 -2
  143. package/lib/module/features/playback/controls.js.map +1 -1
  144. package/lib/module/features/playback/state.js +2 -3
  145. package/lib/module/features/playback/state.js.map +1 -1
  146. package/lib/module/features/player/options.js.map +1 -1
  147. package/lib/module/features/player/setup.js +3 -1
  148. package/lib/module/features/player/setup.js.map +1 -1
  149. package/lib/module/features/player/validateOptions.js +2 -0
  150. package/lib/module/features/player/validateOptions.js.map +1 -1
  151. package/lib/module/features/queue/activeTrack.js +14 -3
  152. package/lib/module/features/queue/activeTrack.js.map +1 -1
  153. package/lib/module/features/queue/queue.js +11 -4
  154. package/lib/module/features/queue/queue.js.map +1 -1
  155. package/lib/module/utils/LazyNativeEmitter.js +21 -10
  156. package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
  157. package/lib/module/utils/NativeUpdatedValue.js +31 -13
  158. package/lib/module/utils/NativeUpdatedValue.js.map +1 -1
  159. package/lib/module/utils/getTrackIdentity.js +20 -0
  160. package/lib/module/utils/getTrackIdentity.js.map +1 -0
  161. package/lib/module/utils/useNativeValueRefreshedBy.js +29 -0
  162. package/lib/module/utils/useNativeValueRefreshedBy.js.map +1 -0
  163. package/lib/module/web/NativeAudioBrowser.js +32 -24
  164. package/lib/module/web/NativeAudioBrowser.js.map +1 -1
  165. package/lib/module/web/browser/BrowserManager.js +144 -87
  166. package/lib/module/web/browser/BrowserManager.js.map +1 -1
  167. package/lib/module/web/browser/FavoriteManager.js +31 -21
  168. package/lib/module/web/browser/FavoriteManager.js.map +1 -1
  169. package/lib/module/web/browser/assertBrowsePageShape.js +2 -2
  170. package/lib/module/web/browser/assertBrowsePageShape.js.map +1 -1
  171. package/lib/module/web/browser/classifyTrackNavigation.js +9 -9
  172. package/lib/module/web/browser/classifyTrackNavigation.js.map +1 -1
  173. package/lib/module/web/browser/sections.js +83 -0
  174. package/lib/module/web/browser/sections.js.map +1 -0
  175. package/lib/module/web/player/NowPlayingManager.js +4 -3
  176. package/lib/module/web/player/NowPlayingManager.js.map +1 -1
  177. package/lib/module/web/util/BrowserPathHelper.js +56 -11
  178. package/lib/module/web/util/BrowserPathHelper.js.map +1 -1
  179. package/lib/typescript/src/AudioBrowser.d.ts +1 -0
  180. package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
  181. package/lib/typescript/src/features/browser-config.d.ts.map +1 -1
  182. package/lib/typescript/src/features/browser.d.ts +2 -2
  183. package/lib/typescript/src/features/favorites.d.ts +13 -10
  184. package/lib/typescript/src/features/favorites.d.ts.map +1 -1
  185. package/lib/typescript/src/features/playback/controls.d.ts +4 -2
  186. package/lib/typescript/src/features/playback/controls.d.ts.map +1 -1
  187. package/lib/typescript/src/features/playback/state.d.ts +2 -3
  188. package/lib/typescript/src/features/playback/state.d.ts.map +1 -1
  189. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts +1 -1
  190. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts.map +1 -1
  191. package/lib/typescript/src/features/player/options.d.ts +21 -19
  192. package/lib/typescript/src/features/player/options.d.ts.map +1 -1
  193. package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
  194. package/lib/typescript/src/features/player/validateOptions.d.ts +1 -8
  195. package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -1
  196. package/lib/typescript/src/features/queue/activeTrack.d.ts +8 -1
  197. package/lib/typescript/src/features/queue/activeTrack.d.ts.map +1 -1
  198. package/lib/typescript/src/features/queue/queue.d.ts +5 -2
  199. package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
  200. package/lib/typescript/src/types/browser-native.d.ts +2 -2
  201. package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
  202. package/lib/typescript/src/types/browser-nodes.d.ts +260 -127
  203. package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
  204. package/lib/typescript/src/types/browser.d.ts +33 -53
  205. package/lib/typescript/src/types/browser.d.ts.map +1 -1
  206. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +16 -17
  207. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
  208. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts +24 -19
  209. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts.map +1 -1
  210. package/lib/typescript/src/utils/getTrackIdentity.d.ts +18 -0
  211. package/lib/typescript/src/utils/getTrackIdentity.d.ts.map +1 -0
  212. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts +21 -0
  213. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts.map +1 -0
  214. package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +0 -1
  215. package/nitrogen/generated/android/c++/{JFavoritesMatchMode.hpp → JArtworkRendering.hpp} +15 -15
  216. package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +4 -3
  217. package/nitrogen/generated/android/c++/JBrowseResult.hpp +10 -4
  218. package/nitrogen/generated/android/c++/JCarPlayPlayingIndicatorLocation.hpp +58 -0
  219. package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +4 -3
  220. package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +4 -3
  221. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +4 -3
  222. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +4 -3
  223. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +4 -3
  224. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +4 -3
  225. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +4 -2
  226. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +4 -2
  227. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.hpp +10 -4
  228. package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +4 -3
  229. package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +2 -5
  230. package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +4 -3
  231. package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +9 -3
  232. package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +4 -2
  233. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +21 -14
  234. package/nitrogen/generated/android/c++/JIOSOptions.hpp +9 -3
  235. package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +4 -3
  236. package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +16 -10
  237. package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +8 -2
  238. package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +6 -7
  239. package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +2 -5
  240. package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +9 -3
  241. package/nitrogen/generated/android/c++/JOptions.hpp +2 -5
  242. package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +4 -3
  243. package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +5 -11
  244. package/nitrogen/generated/android/c++/JResolvedTrack.hpp +49 -51
  245. package/nitrogen/generated/android/c++/JSection.hpp +114 -0
  246. package/nitrogen/generated/android/c++/JSectionStyle.hpp +69 -0
  247. package/nitrogen/generated/android/c++/JStyleDisplay.hpp +58 -0
  248. package/nitrogen/generated/android/c++/JTrack.hpp +19 -48
  249. package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +4 -2
  250. package/nitrogen/generated/android/c++/JTrackStyle.hpp +25 -18
  251. package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +10 -4
  252. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoritesMatchMode.kt → ArtworkRendering.kt} +5 -5
  253. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CarPlayPlayingIndicatorLocation.kt +23 -0
  254. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IOSOptions.kt +6 -3
  255. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +5 -5
  256. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroIOSUpdateOptions.kt +6 -3
  257. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayerCapabilities.kt +2 -2
  258. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ResolvedTrack.kt +12 -18
  259. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Section.kt +50 -0
  260. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoriteConfig.kt → SectionStyle.kt} +12 -6
  261. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/StyleDisplay.kt +23 -0
  262. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +6 -15
  263. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +23 -5
  264. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +104 -82
  265. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +16 -10
  266. package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +16 -10
  267. package/nitrogen/generated/ios/swift/ArtworkRendering.swift +40 -0
  268. package/nitrogen/generated/ios/swift/CarPlayPlayingIndicatorLocation.swift +40 -0
  269. package/nitrogen/generated/ios/swift/IOSOptions.swift +7 -2
  270. package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +5 -5
  271. package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +12 -1
  272. package/nitrogen/generated/ios/swift/PlayerCapabilities.swift +8 -27
  273. package/nitrogen/generated/ios/swift/ResolvedTrack.swift +54 -83
  274. package/nitrogen/generated/ios/swift/Section.swift +100 -0
  275. package/nitrogen/generated/ios/swift/SectionStyle.swift +64 -0
  276. package/nitrogen/generated/ios/swift/StyleDisplay.swift +40 -0
  277. package/nitrogen/generated/ios/swift/Track.swift +20 -73
  278. package/nitrogen/generated/ios/swift/TrackStyle.swift +28 -22
  279. package/nitrogen/generated/shared/c++/{FavoritesMatchMode.hpp → ArtworkRendering.hpp} +17 -17
  280. package/nitrogen/generated/shared/c++/CarPlayPlayingIndicatorLocation.hpp +76 -0
  281. package/nitrogen/generated/shared/c++/IOSOptions.hpp +9 -2
  282. package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +6 -6
  283. package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +9 -2
  284. package/nitrogen/generated/shared/c++/PlayerCapabilities.hpp +6 -9
  285. package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +30 -38
  286. package/nitrogen/generated/shared/c++/Section.hpp +106 -0
  287. package/nitrogen/generated/shared/c++/SectionStyle.hpp +96 -0
  288. package/nitrogen/generated/shared/c++/StyleDisplay.hpp +76 -0
  289. package/nitrogen/generated/shared/c++/Track.hpp +15 -31
  290. package/nitrogen/generated/shared/c++/TrackStyle.hpp +50 -34
  291. package/package.json +3 -2
  292. package/src/AudioBrowser.ts +1 -0
  293. package/src/features/browser-config.test.ts +21 -6
  294. package/src/features/browser-config.ts +11 -8
  295. package/src/features/browser.test.ts +1 -1
  296. package/src/features/browser.ts +2 -2
  297. package/src/features/favorites.ts +13 -10
  298. package/src/features/hook-wiring.test.ts +63 -15
  299. package/src/features/playback/controls.ts +4 -2
  300. package/src/features/playback/state.ts +2 -3
  301. package/src/features/playback/trackPlaybackTime.ts +1 -1
  302. package/src/features/player/options.ts +27 -19
  303. package/src/features/player/setup.test.ts +12 -3
  304. package/src/features/player/setup.ts +3 -1
  305. package/src/features/player/validateOptions.ts +2 -0
  306. package/src/features/queue/activeTrack.ts +14 -3
  307. package/src/features/queue/queue.ts +11 -4
  308. package/src/nitro-stub-drift.test.ts +277 -0
  309. package/src/public-surface.test.ts +40 -0
  310. package/src/specs/audio-browser.nitro.ts +4 -3
  311. package/src/style-resolution.test.ts +139 -0
  312. package/src/types/browser-native.ts +2 -2
  313. package/src/types/browser-nodes.ts +268 -136
  314. package/src/types/browser.ts +34 -55
  315. package/src/utils/LazyNativeEmitter.ts +20 -10
  316. package/src/utils/NativeUpdatedValue.ts +29 -13
  317. package/src/utils/getTrackIdentity.ts +20 -0
  318. package/src/utils/useNativeValueRefreshedBy.ts +38 -0
  319. package/src/web/NativeAudioBrowser.ts +37 -22
  320. package/src/web/browser/BrowserManager.ts +176 -95
  321. package/src/web/browser/FavoriteManager.test.ts +84 -0
  322. package/src/web/browser/FavoriteManager.ts +28 -23
  323. package/src/web/browser/assertBrowsePageShape.test.ts +1 -1
  324. package/src/web/browser/assertBrowsePageShape.ts +5 -3
  325. package/src/web/browser/classifyTrackNavigation.test.ts +10 -8
  326. package/src/web/browser/classifyTrackNavigation.ts +9 -9
  327. package/src/web/browser/parseSearchResponse.test.ts +8 -2
  328. package/src/web/browser/sections.test.ts +91 -0
  329. package/src/web/browser/sections.ts +72 -0
  330. package/src/web/player/NowPlayingManager.ts +4 -3
  331. package/src/web/util/BrowserPathHelper.test.ts +52 -0
  332. package/src/web/util/BrowserPathHelper.ts +61 -13
  333. package/ios/CarPlay/ImageRowItem+CarPlay.swift +0 -19
  334. package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +0 -58
  335. package/nitrogen/generated/android/c++/JImageRowItem.hpp +0 -105
  336. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.cpp +0 -26
  337. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.hpp +0 -71
  338. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageRowItem.kt +0 -68
  339. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_FavoriteConfig.kt +0 -53
  340. package/nitrogen/generated/ios/swift/FavoriteConfig.swift +0 -29
  341. package/nitrogen/generated/ios/swift/FavoritesMatchMode.swift +0 -40
  342. package/nitrogen/generated/ios/swift/ImageRowItem.swift +0 -195
  343. package/nitrogen/generated/ios/swift/Variant_Bool_FavoriteConfig.swift +0 -18
  344. package/nitrogen/generated/shared/c++/FavoriteConfig.hpp +0 -84
  345. package/nitrogen/generated/shared/c++/ImageRowItem.hpp +0 -129
@@ -7,8 +7,9 @@ import os.log
7
7
 
8
8
  /// Typed metadata stored on CPListItems, replacing stringly-typed userInfo dictionaries.
9
9
  struct CarPlayItemInfo {
10
- let id: String?
11
- let src: String?
10
+ /// The track's identity (`id` when non-blank, else `src`) — the comparison
11
+ /// key for the now-playing indicator repaint sweep.
12
+ let identity: String?
12
13
  }
13
14
 
14
15
  extension CPListItem {
@@ -29,11 +30,14 @@ final class CarPlayListItemFactory {
29
30
  private let logger = Logger(subsystem: "com.audiobrowser", category: "CarPlayListItemFactory")
30
31
 
31
32
  var imageLoader: CarPlayImageLoader?
32
- private let isActiveTrack: (_ id: String?, _ src: String?) -> Bool
33
+ /// Pushed by the controller (at creation and when options change), like
34
+ /// `imageLoader` — the factory never reads options itself.
35
+ var playingIndicatorLocation: CPListItemPlayingIndicatorLocation = .leading
36
+ private let isActiveTrack: (_ identity: String?) -> Bool
33
37
  private let onItemSelected: (Track, @escaping () -> Void) -> Void
34
38
 
35
39
  init(
36
- isActiveTrack: @escaping (_ id: String?, _ src: String?) -> Bool,
40
+ isActiveTrack: @escaping (_ identity: String?) -> Bool,
37
41
  onItemSelected: @escaping (Track, @escaping () -> Void) -> Void,
38
42
  ) {
39
43
  self.isActiveTrack = isActiveTrack
@@ -42,55 +46,69 @@ final class CarPlayListItemFactory {
42
46
 
43
47
  // MARK: - Sections
44
48
 
49
+ /// Whether this OS has CarPlay's wrapping tile container (the iOS 26
50
+ /// element API) — the capability `SectionPresentation` degrades against.
51
+ private static var supportsWrappingGrid: Bool {
52
+ if #available(iOS 26.0, *) { return true }
53
+ return false
54
+ }
55
+
56
+ /// Maps the page's sections 1:1 to CPListSections (ADR 0010), respecting
57
+ /// CarPlay's section and total-item budgets. Style blocks declare the
58
+ /// requested presentation (resolved `section ?? page` here — ADR 0011);
59
+ /// this renders CarPlay's nearest supported form (`SectionPresentation`):
60
+ /// - list (default): a titled/headerless section of list rows.
61
+ /// - single-line grid (`gridWrap: false`): a headerless section holding
62
+ /// one single-line image-row item whose text is the section title — the
63
+ /// tiles that fit render, the rest truncate (the platform doesn't
64
+ /// report the fit).
65
+ /// - wrapping grid: a wrapping, titled tile grid on iOS 26+; a plain list
66
+ /// before that.
45
67
  func createSections(from resolvedTrack: ResolvedTrack) -> [CPListSection] {
46
- guard let children = resolvedTrack.children else {
68
+ guard let sections = resolvedTrack.normalizedSections else {
47
69
  return []
48
70
  }
49
71
 
50
72
  let maxSections = CPListTemplate.maximumSectionCount
51
73
  let maxTotalItems = CPListTemplate.maximumItemCount
52
74
 
53
- // Contiguous runs sharing a groupTitle form a section (the same semantics
54
- // Android Auto's GROUP_TITLE hint has), so sections — titled or not —
55
- // render in exact source (server) order. Ungrouped runs become headerless
56
- // sections in place rather than being hoisted to the top.
57
- var runs: [(title: String?, tracks: [Track])] = []
58
- for track in children {
59
- if let last = runs.indices.last, runs[last].title == track.groupTitle {
60
- runs[last].tracks.append(track)
61
- } else {
62
- runs.append((track.groupTitle, [track]))
63
- }
64
- }
65
-
66
- // Create sections (respecting both section and total item limits)
67
- var sections: [CPListSection] = []
75
+ var listSections: [CPListSection] = []
68
76
  var totalItemCount = 0
69
77
 
70
- for run in runs {
71
- guard sections.count < maxSections else { break }
78
+ for section in sections {
79
+ guard listSections.count < maxSections else { break }
72
80
  guard totalItemCount < maxTotalItems else { break }
73
-
74
- let availableSlots = maxTotalItems - totalItemCount
75
- let items: [CPListTemplateItem] = run.tracks.prefix(availableSlots).map { track in
76
- // Empty image rows fall through to a plain list item: zero-image
77
- // CPListImageRowItem init is undocumented territory, and a header-only
78
- // row is a dead end regardless (a "popular" row with no entries yet).
79
- if let imageRow = track.imageRow, !imageRow.isEmpty {
80
- return createImageRowItem(for: track)
81
+ // An empty section is a dead end regardless of style (a "popular"
82
+ // section with no entries yet) — skip it rather than render a header
83
+ // with nothing under it.
84
+ guard !section.children.isEmpty else { continue }
85
+
86
+ let style = StyleResolver.sectionStyle(section: section.style, page: resolvedTrack.style)
87
+ let presentation = SectionPresentation(for: style, supportsWrappingGrid: Self.supportsWrappingGrid)
88
+ switch presentation {
89
+ case .list:
90
+ let availableSlots = maxTotalItems - totalItemCount
91
+ let items: [CPListTemplateItem] = section.children.prefix(availableSlots).map {
92
+ createListItem(for: $0, style: StyleResolver.trackStyle(track: $0.style, section: style))
81
93
  }
82
- return createListItem(for: track)
83
- }
84
- guard !items.isEmpty else { continue }
85
- if let title = run.title {
86
- sections.append(CPListSection(items: items, header: title, sectionIndexTitle: nil))
87
- } else {
88
- sections.append(CPListSection(items: items))
94
+ if let title = section.title {
95
+ listSections.append(CPListSection(items: items, header: title, sectionIndexTitle: nil))
96
+ } else {
97
+ listSections.append(CPListSection(items: items))
98
+ }
99
+ totalItemCount += items.count
100
+ case .singleLineRow, .wrappingGrid:
101
+ // Tile presentations render as one image-row item inside a
102
+ // headerless CPListSection, with the section title as the item's
103
+ // text — a headed section would render the title twice.
104
+ guard let item = createImageRowItem(for: section, style: style, presentation: presentation)
105
+ else { continue }
106
+ listSections.append(CPListSection(items: [item]))
107
+ totalItemCount += 1
89
108
  }
90
- totalItemCount += items.count
91
109
  }
92
110
 
93
- return sections
111
+ return listSections
94
112
  }
95
113
 
96
114
  // MARK: - List Item
@@ -98,9 +116,14 @@ final class CarPlayListItemFactory {
98
116
  /// Creates a CPListItem for a track with common setup (typed userInfo, artwork, isPlaying).
99
117
  /// - Parameters:
100
118
  /// - track: The track to create the item for
119
+ /// - style: The track's resolved style block (`track ?? section ?? page`,
120
+ /// via `StyleResolver`). nil = the track's own declaration — the one
121
+ /// genuinely section-less caller (the Up Next queue list) has no
122
+ /// inheritance to resolve.
101
123
  /// - handler: Optional custom handler. If nil, uses default browse/play handling.
102
124
  func createListItem(
103
125
  for track: Track,
126
+ style: TrackStyle? = nil,
104
127
  handler: ((CPSelectableListItem, @escaping () -> Void) -> Void)? = nil,
105
128
  ) -> CPListItem {
106
129
  let item = CPListItem(
@@ -109,46 +132,59 @@ final class CarPlayListItemFactory {
109
132
  )
110
133
 
111
134
  // Store typed info for updatePlayingIndicators()
112
- item.setCarPlayItemInfo(CarPlayItemInfo(id: track.id, src: track.src))
135
+ item.setCarPlayItemInfo(CarPlayItemInfo(identity: track.identity))
113
136
 
114
137
  // Set accessory type based on whether track is browsable or playable
115
138
  if let src = track.src {
116
139
  // Playable track - check if it's currently playing
117
140
  item.accessoryType = .none
118
- // Leading (the system default, set explicitly to make the choice
119
- // deliberate): the indicator draws in the artwork slot. Note the
141
+ // Consumer-configurable via `ios.carPlayPlayingIndicatorLocation`
142
+ // (default leading: the indicator draws in the artwork slot). Note the
120
143
  // indicator's rendering is owned by the phone's CarPlay service, and on
121
144
  // some iOS/CarPlay-Simulator combinations it isn't drawn at all — for
122
145
  // ANY third-party app. Verify on a real head unit before assuming a
123
146
  // logic bug here.
124
- item.playingIndicatorLocation = .leading
125
- item.isPlaying = isActiveTrack(track.id, src)
147
+ item.playingIndicatorLocation = playingIndicatorLocation
148
+ item.isPlaying = isActiveTrack(track.identity)
126
149
  if item.isPlaying {
127
150
  logger.debug("Setting isPlaying=true for: \(track.title) (src: \(src))")
128
151
  }
129
- } else if track.url != nil {
152
+ } else if track.path != nil {
130
153
  // Browsable only - show disclosure indicator
131
154
  item.accessoryType = .disclosureIndicator
132
155
  }
133
156
 
157
+ // An unavailable track grays out and goes inert — list rows are the
158
+ // surface that CAN draw the fact, so it shows rather than hides
159
+ // (Track.disabled's rendering ladder).
160
+ if track.disabled == true {
161
+ item.isEnabled = false
162
+ }
163
+
134
164
  // Load artwork with size context for proper CDN optimization
135
165
  if track.artwork != nil || track.artworkSource != nil {
136
166
  // Set empty placeholder to reserve space while loading
137
- item.setImage(imageLoader?.placeholderImage)
138
- imageLoader?.loadArtwork(for: track, size: CPListItem.maximumImageSize) { [weak item] image in
167
+ item.setImage(imageLoader?.placeholderImage(size: CPListItem.maximumImageSize))
168
+ imageLoader?.loadArtwork(for: track, style: style, size: CPListItem.maximumImageSize) { [weak item] image in
139
169
  Task { @MainActor in
140
170
  item?.setImage(image)
141
171
  }
142
172
  }
143
173
  }
144
174
 
145
- // Set selection handler
146
- if let handler {
147
- item.handler = handler
148
- } else {
149
- item.handler = { [onItemSelected] _, completion in
150
- onItemSelected(track, completion)
175
+ // Set selection handler. The disabled guard wraps BOTH handler paths —
176
+ // custom handlers (Up Next rows skipping the queue, say) included: belt
177
+ // over the isEnabled braces, so an unavailable track stays inert even if
178
+ // a surface delivers the tap.
179
+ let selection = handler ?? { [onItemSelected] _, completion in
180
+ onItemSelected(track, completion)
181
+ }
182
+ item.handler = { item, completion in
183
+ guard track.disabled != true else {
184
+ completion()
185
+ return
151
186
  }
187
+ selection(item, completion)
152
188
  }
153
189
 
154
190
  return item
@@ -156,58 +192,141 @@ final class CarPlayListItemFactory {
156
192
 
157
193
  // MARK: - Image Row Item
158
194
 
159
- /// Creates a CPListImageRowItem for a track that has an imageRow.
160
- /// Renders as a horizontal row of tappable artwork thumbnails with a header title.
161
- func createImageRowItem(for track: Track) -> CPListImageRowItem {
162
- guard let imageRowItems = track.imageRow else {
163
- fatalError("createImageRowItem called without imageRow")
195
+ /// Target size for images inside an image row. The iOS 26 SDK deprecates
196
+ /// `CPListImageRowItem.maximumImageSize` in favor of the element-based API,
197
+ /// so read whichever the running OS considers canonical.
198
+ private static var rowImageSize: CGSize {
199
+ if #available(iOS 26.0, *) {
200
+ return CPListImageRowItemElement.maximumImageSize
164
201
  }
202
+ return CPListImageRowItem.maximumImageSize
203
+ }
165
204
 
166
- // CPListImageRowItem requires images at init — start with placeholders
167
- let maxImages = CPMaximumNumberOfGridImages
168
- let visibleItems = Array(imageRowItems.prefix(maxImages))
169
- let placeholders = visibleItems.map { _ in imageLoader?.placeholderImage ?? UIImage() }
170
- let titles = visibleItems.map(\.title)
205
+ /// Creates the image-row item rendering a tile-presented section: a single
206
+ /// truncating line (`.singleLineRow`) or a wrapping multi-line grid
207
+ /// (`.wrappingGrid` — constructed on iOS 26+ only, by `SectionPresentation`).
208
+ ///
209
+ /// Returns nil when nothing is drawable — every child hidden by the
210
+ /// disabled ladder (pre-26 only; the element APIs gray instead).
211
+ private func createImageRowItem(
212
+ for section: Section, style: SectionStyle, presentation: SectionPresentation,
213
+ ) -> CPListImageRowItem? {
214
+ let singleLine = presentation == .singleLineRow
215
+ // Disabled ladder: iOS 26 elements can draw unavailability (grayed +
216
+ // inert, below); the legacy image row can't, so there a disabled track
217
+ // hides — never a normal-looking dead tile.
218
+ let visibleChildren: [Track] = if #available(iOS 26.0, *) {
219
+ section.children
220
+ } else {
221
+ section.children.filter { $0.disabled != true }
222
+ }
223
+ guard !visibleChildren.isEmpty else { return nil }
224
+ // A single line shows at most CPMaximumNumberOfGridImages; the wrapping
225
+ // grid takes every child (queue scope is declared, never rendered — the
226
+ // cap here only limits what's drawn, and only where the platform does).
227
+ let tracks = singleLine
228
+ ? Array(visibleChildren.prefix(CPMaximumNumberOfGridImages))
229
+ : visibleChildren
230
+ let placeholder = { [imageLoader] in
231
+ imageLoader?.placeholderImage(size: Self.rowImageSize) ?? UIImage()
232
+ }
171
233
 
172
- // Use imageTitles variant on iOS 17.4+ to show titles below each thumbnail
173
- let item = if #available(iOS 17.4, *) {
174
- CPListImageRowItem(text: track.title, images: placeholders, imageTitles: titles)
234
+ let item: CPListImageRowItem
235
+ // Applies an async-loaded artwork image to a tile on the element-based
236
+ // API (the elements are captured per availability branch — their type
237
+ // doesn't exist before iOS 26). nil = the legacy gridImages path.
238
+ // @MainActor makes the closure Sendable for the artwork completion hop.
239
+ var applyImage: (@MainActor (Int, UIImage) -> Void)?
240
+ if #available(iOS 26.0, *) {
241
+ if singleLine {
242
+ // Row elements are the only tile element with a subtitle slot — the
243
+ // imageGridElements branch below has no equivalent, by SDK design.
244
+ let rowElements = tracks.map { track in
245
+ let element = CPListImageRowItemRowElement(
246
+ image: placeholder(), title: track.title, subtitle: track.subtitle,
247
+ )
248
+ if track.disabled == true {
249
+ element.isEnabled = false
250
+ }
251
+ return element
252
+ }
253
+ item = CPListImageRowItem(text: section.title, elements: rowElements, allowsMultipleLines: false)
254
+ applyImage = { index, image in
255
+ guard index < rowElements.count else { return }
256
+ rowElements[index].image = image
257
+ }
258
+ } else {
259
+ // imageGridElements, never the title-less gridElements: an
260
+ // artwork-less track must keep its name on screen (ADR 0010).
261
+ let gridElements = tracks.map { track in
262
+ let element = CPListImageRowItemImageGridElement(
263
+ image: placeholder(), imageShape: .roundedRectangle, title: track.title,
264
+ accessorySymbolName: nil,
265
+ )
266
+ if track.disabled == true {
267
+ element.isEnabled = false
268
+ }
269
+ return element
270
+ }
271
+ item = CPListImageRowItem(text: section.title, imageGridElements: gridElements, allowsMultipleLines: true)
272
+ applyImage = { index, image in
273
+ guard index < gridElements.count else { return }
274
+ gridElements[index].image = image
275
+ }
276
+ }
175
277
  } else {
176
- CPListImageRowItem(text: track.title, images: placeholders)
278
+ let placeholders = tracks.map { _ in placeholder() }
279
+ // Use imageTitles variant on iOS 17.4+ to show titles below each thumbnail
280
+ if #available(iOS 17.4, *) {
281
+ item = CPListImageRowItem(text: section.title ?? "", images: placeholders, imageTitles: tracks.map(\.title))
282
+ } else {
283
+ item = CPListImageRowItem(text: section.title ?? "", images: placeholders)
284
+ }
177
285
  }
178
286
 
179
- // Handler for row header tap → navigate to track.url if present. A row
180
- // without a url (pure preview) has nothing to open, so its header tap is
181
- // a no-op rather than a selection that can't resolve.
287
+ // Header tap → navigate to section.path ("view all"). A path-less
288
+ // section is a pure preview — its header tap is a no-op rather than a
289
+ // selection that can't resolve.
182
290
  item.handler = { [onItemSelected] _, completion in
183
- guard track.url != nil else {
291
+ guard let path = section.path else {
184
292
  completion()
185
293
  return
186
294
  }
187
- onItemSelected(track, completion)
295
+ onItemSelected(Self.navigationTrack(path: path, title: section.title), completion)
188
296
  }
189
297
 
190
- // Handler for individual image taps
298
+ // Per-tile taps select the child track directly. A disabled tile is
299
+ // grayed and inert (26+ elements) — the guard keeps it inert even if
300
+ // the surface delivers the tap anyway.
191
301
  item.listImageRowHandler = { [onItemSelected] _, index, completion in
192
- guard index < visibleItems.count else {
302
+ guard index < tracks.count, tracks[index].disabled != true else {
193
303
  completion()
194
304
  return
195
305
  }
196
- let tappedItem = visibleItems[index]
197
- onItemSelected(tappedItem.toTrack(), completion)
306
+ onItemSelected(tracks[index], completion)
198
307
  }
199
308
 
200
- // Load artwork for each visible image row item asynchronously
201
- for (index, imageRowItem) in visibleItems.enumerated() {
202
- guard imageRowItem.artwork != nil || imageRowItem.artworkSource != nil else { continue }
309
+ // Load artwork for each rendered tile asynchronously
310
+ for (index, track) in tracks.enumerated() {
311
+ guard track.artwork != nil || track.artworkSource != nil else { continue }
203
312
 
204
- imageLoader?.loadArtwork(for: imageRowItem.toTrack(), size: CPListImageRowItem.maximumImageSize) { [weak item] image in
313
+ imageLoader?.loadArtwork(
314
+ for: track,
315
+ style: StyleResolver.trackStyle(track: track.style, section: style),
316
+ size: Self.rowImageSize,
317
+ ) { [weak item] image in
205
318
  Task { @MainActor in
206
319
  guard let item, let image else { return }
207
- var images = item.gridImages
208
- if index < images.count {
209
- images[index] = image
210
- item.update(images)
320
+ if let applyImage {
321
+ // The element-based API exposes a settable `image`; assigning it
322
+ // updates the element the row observes.
323
+ applyImage(index, image)
324
+ } else {
325
+ var images = item.gridImages
326
+ if index < images.count {
327
+ images[index] = image
328
+ item.update(images)
329
+ }
211
330
  }
212
331
  }
213
332
  }
@@ -215,4 +334,18 @@ final class CarPlayListItemFactory {
215
334
 
216
335
  return item
217
336
  }
337
+
338
+ /// A synthetic browsable Track for section-header navigation — the
339
+ /// section's "view all" surface has a path and a title, but no Track.
340
+ /// Style-less by design: CarPlay resolves presentation from the page it
341
+ /// navigates to. (Android's counterpart projects the section's block —
342
+ /// the promise only exists to feed Android Auto's parent-level hint.)
343
+ private static func navigationTrack(path: String, title: String?) -> Track {
344
+ Track(
345
+ id: nil, path: path, src: nil, artwork: nil, artworkSource: nil,
346
+ request: nil, title: title ?? "", subtitle: nil,
347
+ artist: nil, albumPath: nil, album: nil, description: nil, genre: nil,
348
+ duration: nil, style: nil, disabled: nil, favorited: nil, live: nil,
349
+ )
350
+ }
218
351
  }
@@ -16,8 +16,8 @@ final class CarPlayNowPlayingManager {
16
16
 
17
17
  private let interfaceController: CPInterfaceController
18
18
  var listItemFactory: (Track, ((CPSelectableListItem, @escaping () -> Void) -> Void)?) -> CPListItem
19
- /// Pushes a browse destination (wired to the controller's navigateToUrl).
20
- var navigateToUrl: ((_ url: String, _ title: String) -> Void)?
19
+ /// Pushes a browse destination (wired to the controller's navigateToPath).
20
+ var navigateToPath: ((_ path: String, _ title: String) -> Void)?
21
21
 
22
22
  private weak var audioBrowser: HybridAudioBrowser?
23
23
  private var nowPlayingObserver: NowPlayingObserver?
@@ -46,11 +46,11 @@ final class CarPlayNowPlayingManager {
46
46
  private var builtRow: BuiltButtonRow?
47
47
 
48
48
  /// Browse path the album line navigates to for the active track —
49
- /// `track.albumUrl`, or the pre-resolved `resolveAlbumUrl` result. Resolved
49
+ /// `track.albumPath`, or the pre-resolved `resolveAlbumPath` result. Resolved
50
50
  /// at track-change time (not at tap) so the album line only surfaces when a
51
51
  /// destination actually exists.
52
52
  private var albumArtistDestination: String?
53
- /// Drops a stale async `resolveAlbumUrl` result when the track changed while
53
+ /// Drops a stale async `resolveAlbumPath` result when the track changed while
54
54
  /// the resolver was in flight (latest-update-wins).
55
55
  private var albumArtistGeneration: UInt = 0
56
56
  /// The just-tapped track whose load is still in flight. CarPlay reads
@@ -214,7 +214,7 @@ final class CarPlayNowPlayingManager {
214
214
 
215
215
  /// Re-resolves the album line's destination for the active track and
216
216
  /// enables the (tappable) album/artist button only when one exists:
217
- /// `track.albumUrl`, or the app's `resolveAlbumUrl` result.
217
+ /// `track.albumPath`, or the app's `resolveAlbumPath` result.
218
218
  /// Marks `track` as the just-tapped (still loading) track and derives the
219
219
  /// album line's destination from it. Must run before the Now Playing
220
220
  /// template is pushed — CarPlay reads `isAlbumArtistButtonEnabled` at display
@@ -242,12 +242,12 @@ final class CarPlayNowPlayingManager {
242
242
  setAlbumArtistDestination(nil)
243
243
  return
244
244
  }
245
- logger.info("AlbumArtist: track=\(track.title) albumUrl=\(track.albumUrl ?? "nil") resolver=\(self.config.resolveAlbumUrl != nil)")
246
- if let albumUrl = track.albumUrl {
247
- setAlbumArtistDestination(albumUrl)
245
+ logger.info("AlbumArtist: track=\(track.title) albumPath=\(track.albumPath ?? "nil") resolver=\(self.config.resolveAlbumPath != nil)")
246
+ if let albumPath = track.albumPath {
247
+ setAlbumArtistDestination(albumPath)
248
248
  return
249
249
  }
250
- guard let resolver = config.resolveAlbumUrl else {
250
+ guard let resolver = config.resolveAlbumPath else {
251
251
  setAlbumArtistDestination(nil)
252
252
  return
253
253
  }
@@ -256,7 +256,7 @@ final class CarPlayNowPlayingManager {
256
256
  do {
257
257
  path = try await resolver(track).await()
258
258
  } catch {
259
- self?.logger.error("resolveAlbumUrl failed: \(error.localizedDescription)")
259
+ self?.logger.error("resolveAlbumPath failed: \(error.localizedDescription)")
260
260
  path = nil
261
261
  }
262
262
  guard let self, self.albumArtistGeneration == generation else { return }
@@ -295,7 +295,10 @@ final class CarPlayNowPlayingManager {
295
295
  /// Returns the appropriate image for the favorite button based on state
296
296
  private func favoriteButtonImage(isFavorited: Bool) -> UIImage {
297
297
  let symbolName = isFavorited ? "heart.fill" : "heart"
298
- guard let image = UIImage(systemName: symbolName)?.resized(to: CPNowPlayingButtonMaximumImageSize) else {
298
+ guard let image = UIImage(systemName: symbolName)?.resized(
299
+ to: CPNowPlayingButtonMaximumImageSize,
300
+ scale: interfaceController.carTraitCollection.displayScale,
301
+ ) else {
299
302
  return UIImage()
300
303
  }
301
304
  return image
@@ -360,7 +363,7 @@ final class CarPlayNowPlayingManager {
360
363
  // the pushed destination (artist may be unset, or the live song).
361
364
  let title = track?.album ?? track?.artist ?? track?.title ?? ""
362
365
  logger.info("Album/Artist button tapped, navigating to \(destination)")
363
- navigateToUrl?(destination, title)
366
+ navigateToPath?(destination, title)
364
367
  }
365
368
 
366
369
  private func updateNowPlayingUpNextButton() {
@@ -54,7 +54,7 @@ extension PlayerCapabilities {
54
54
  // Apple silently removed. On the iOS 16+ target it can never be invoked: CarPlay
55
55
  // uses its own CPNowPlayingButton heart, and Siri "I like this" routes through
56
56
  // INUpdateMediaAffinityIntent. The `favorite` capability still drives the
57
- // favorites-match row indicators via setFavoriteMatch. See issues #67 / #71.
57
+ // favorite row indicators via setFavoriteEnabled. See issues #67 / #71.
58
58
 
59
59
  // Mode controls
60
60
  if shuffleMode != false {
@@ -1,20 +1,6 @@
1
1
  import Foundation
2
2
 
3
3
  extension PlayerCapabilities {
4
- /// Resolved favorite match mode, or `nil` when favoriting is disabled.
5
- ///
6
- /// `false`/unset → `nil`; `true` → `.exact`; `{ match }` → that match.
7
- var favoriteMatch: FavoritesMatchMode? {
8
- switch favorite {
9
- case .none:
10
- nil
11
- case let .some(.first(enabled)):
12
- enabled ? .exact : nil
13
- case let .some(.second(config)):
14
- config.match
15
- }
16
- }
17
-
18
4
  /// Whether the favorite/like control is enabled.
19
- var favoriteEnabled: Bool { favoriteMatch != nil }
5
+ var favoriteEnabled: Bool { favorite ?? false }
20
6
  }
@@ -10,9 +10,11 @@ extension ResolvedTrack {
10
10
  /// - `.some(nil)` → set field to nil
11
11
  /// - `.some(value)` → set field to value
12
12
  ///
13
- /// `url` and `title` use single-optional since they are non-optional on ResolvedTrack.
13
+ /// `path` and `title` use single-optional since they are non-optional on ResolvedTrack.
14
14
  func copying(
15
- url: String? = nil,
15
+ path: String? = nil,
16
+ style: SectionStyle?? = nil,
17
+ sections: [Section]?? = nil,
16
18
  children: [Track]?? = nil,
17
19
  carPlaySiriListButton: CarPlaySiriListButtonPosition?? = nil,
18
20
  id: String?? = nil,
@@ -20,24 +22,22 @@ extension ResolvedTrack {
20
22
  artwork: Variant_String_ArtworkVariants?? = nil,
21
23
  artworkSource: ImageSource?? = nil,
22
24
  request: TrackRequest?? = nil,
23
- artworkCarPlayTinted: Bool?? = nil,
24
25
  title: String? = nil,
25
26
  subtitle: String?? = nil,
26
27
  artist: String?? = nil,
27
- albumUrl: String?? = nil,
28
+ albumPath: String?? = nil,
28
29
  album: String?? = nil,
29
30
  description: String?? = nil,
30
31
  genre: String?? = nil,
31
32
  duration: Double?? = nil,
32
- style: TrackStyle?? = nil,
33
- childrenStyle: TrackStyle?? = nil,
33
+ disabled: Bool?? = nil,
34
34
  favorited: Bool?? = nil,
35
- groupTitle: String?? = nil,
36
35
  live: Bool?? = nil,
37
- imageRow: [ImageRowItem]?? = nil,
38
36
  ) -> ResolvedTrack {
39
37
  ResolvedTrack(
40
- url: url ?? self.url,
38
+ path: path ?? self.path,
39
+ style: style ?? self.style,
40
+ sections: sections ?? self.sections,
41
41
  children: children ?? self.children,
42
42
  carPlaySiriListButton: carPlaySiriListButton ?? self.carPlaySiriListButton,
43
43
  id: id ?? self.id,
@@ -45,21 +45,17 @@ extension ResolvedTrack {
45
45
  artwork: artwork ?? self.artwork,
46
46
  artworkSource: artworkSource ?? self.artworkSource,
47
47
  request: request ?? self.request,
48
- artworkCarPlayTinted: artworkCarPlayTinted ?? self.artworkCarPlayTinted,
49
48
  title: title ?? self.title,
50
49
  subtitle: subtitle ?? self.subtitle,
51
50
  artist: artist ?? self.artist,
52
- albumUrl: albumUrl ?? self.albumUrl,
51
+ albumPath: albumPath ?? self.albumPath,
53
52
  album: album ?? self.album,
54
53
  description: description ?? self.description,
55
54
  genre: genre ?? self.genre,
56
55
  duration: duration ?? self.duration,
57
- style: style ?? self.style,
58
- childrenStyle: childrenStyle ?? self.childrenStyle,
56
+ disabled: disabled ?? self.disabled,
59
57
  favorited: favorited ?? self.favorited,
60
- groupTitle: groupTitle ?? self.groupTitle,
61
58
  live: live ?? self.live,
62
- imageRow: imageRow ?? self.imageRow,
63
59
  )
64
60
  }
65
61
  }
@@ -0,0 +1,31 @@
1
+ #if canImport(NitroModules)
2
+ import NitroModules
3
+ #endif
4
+
5
+ extension Section {
6
+ /// Returns a copy of this Section with only the specified fields changed.
7
+ ///
8
+ /// Same double-optional (`T??`) convention as the other copying helpers;
9
+ /// `children` is single-optional since it is non-optional on Section.
10
+ func copying(
11
+ title: String?? = nil,
12
+ subtitle: String?? = nil,
13
+ style: SectionStyle?? = nil,
14
+ path: String?? = nil,
15
+ children: [Track]? = nil,
16
+ ) -> Section {
17
+ Section(
18
+ title: title ?? self.title,
19
+ subtitle: subtitle ?? self.subtitle,
20
+ style: style ?? self.style,
21
+ path: path ?? self.path,
22
+ children: children ?? self.children,
23
+ )
24
+ }
25
+
26
+ /// One untitled section — the canonical wrap of a flat track list
27
+ /// (`children` authoring sugar, search results — ADR 0010).
28
+ static func untitled(_ children: [Track]) -> Section {
29
+ Section(title: nil, subtitle: nil, style: nil, path: nil, children: children)
30
+ }
31
+ }