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

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 (156) hide show
  1. package/README.md +10 -0
  2. package/android/build.gradle +20 -1
  3. package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +5 -1
  4. package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +8 -0
  5. package/android/src/main/java/com/audiobrowser/browser/InertStyleDiagnostic.kt +267 -0
  6. package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +27 -9
  7. package/android/src/main/java/com/audiobrowser/browser/StyleResolver.kt +11 -0
  8. package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +11 -1
  9. package/android/src/main/java/com/audiobrowser/model/PlayerUpdateOptions.kt +1 -1
  10. package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +17 -2
  11. package/android/src/main/java/com/audiobrowser/player/Player.kt +3 -0
  12. package/android/src/main/java/com/audiobrowser/player/ShuffleOrderLedBy.kt +30 -0
  13. package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +73 -0
  14. package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +8 -1
  15. package/android/src/test/java/com/audiobrowser/TestFixtures.kt +49 -3
  16. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +3 -2
  17. package/android/src/test/java/com/audiobrowser/browser/InertStyleDiagnosticTest.kt +277 -0
  18. package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +56 -0
  19. package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +4 -4
  20. package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +77 -10
  21. package/android/src/test/java/com/audiobrowser/player/PlaybackStateStoreTrackJsonTest.kt +2 -2
  22. package/android/src/test/java/com/audiobrowser/player/ShuffleOrderLedByTest.kt +79 -0
  23. package/android/src/test/java/com/audiobrowser/util/MediaExtrasBuilderTest.kt +8 -8
  24. package/ios/Browser/BrowserManager.swift +106 -45
  25. package/ios/Browser/InertStyleDiagnostic.swift +279 -0
  26. package/ios/Browser/JsonModels.swift +81 -10
  27. package/ios/Browser/SectionPresentation.swift +30 -0
  28. package/ios/Browser/StyleResolver.swift +11 -0
  29. package/ios/Browser/TabBarEntries.swift +7 -1
  30. package/ios/CarPlay/CarPlayController.swift +139 -35
  31. package/ios/CarPlay/CarPlayListItemFactory.swift +99 -27
  32. package/ios/Extension/UIApplication+ActiveScene.swift +27 -0
  33. package/ios/Http/HttpClient.swift +8 -2
  34. package/ios/HybridAudioBrowser.swift +54 -11
  35. package/ios/Model/NitroTypeStubs.swift +60 -0
  36. package/ios/Player/MediaIntentCriteria.swift +13 -5
  37. package/ios/Player/MediaLoader.swift +33 -9
  38. package/ios/Player/NowPlayingUpdater.swift +53 -5
  39. package/ios/Player/PlaybackCoordinator.swift +11 -4
  40. package/ios/Player/PlaybackTimer.swift +8 -2
  41. package/ios/Player/QueueManager.swift +36 -10
  42. package/ios/Player/ShuffleOrder.swift +23 -0
  43. package/ios/Player/SleepTimerHandling.swift +13 -0
  44. package/ios/RemoteCommand/RemoteCommandController.swift +53 -18
  45. package/ios/Tests/InertStyleDiagnosticTests.swift +200 -0
  46. package/ios/Tests/JsonModelsTests.swift +76 -1
  47. package/ios/Tests/QueueManagerTests.swift +72 -6
  48. package/ios/Tests/RemoteCommandControllerTests.swift +16 -0
  49. package/ios/Tests/SectionPresentationTests.swift +29 -0
  50. package/ios/Tests/StyleResolverTests.swift +60 -0
  51. package/ios/Tests/Support/TestTypes.swift +7 -0
  52. package/ios/TrackPlayer.swift +57 -34
  53. package/ios/Util/NetworkMonitor.swift +43 -26
  54. package/ios/Util/SFSymbolRenderer.swift +24 -2
  55. package/ios/Util/SVGProcessor.swift +9 -2
  56. package/lib/commonjs/features/playback/progress.js +2 -1
  57. package/lib/commonjs/features/playback/progress.js.map +1 -1
  58. package/lib/commonjs/features/playback/rate.js +8 -1
  59. package/lib/commonjs/features/playback/rate.js.map +1 -1
  60. package/lib/commonjs/web/TrackPlayer/QueueManager.js +14 -0
  61. package/lib/commonjs/web/TrackPlayer/QueueManager.js.map +1 -1
  62. package/lib/module/features/playback/progress.js +2 -1
  63. package/lib/module/features/playback/progress.js.map +1 -1
  64. package/lib/module/features/playback/rate.js +8 -1
  65. package/lib/module/features/playback/rate.js.map +1 -1
  66. package/lib/module/web/TrackPlayer/QueueManager.js +14 -0
  67. package/lib/module/web/TrackPlayer/QueueManager.js.map +1 -1
  68. package/lib/typescript/src/features/playback/progress.d.ts.map +1 -1
  69. package/lib/typescript/src/features/playback/rate.d.ts +8 -1
  70. package/lib/typescript/src/features/playback/rate.d.ts.map +1 -1
  71. package/lib/typescript/src/types/browser-nodes.d.ts +104 -2
  72. package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
  73. package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +4 -0
  74. package/nitrogen/generated/android/c++/JBrowseResult.hpp +6 -0
  75. package/nitrogen/generated/android/c++/JCardImage.hpp +58 -0
  76. package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +4 -0
  77. package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +4 -0
  78. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +4 -0
  79. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +4 -0
  80. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +4 -0
  81. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +4 -0
  82. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +4 -0
  83. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +4 -0
  84. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.hpp +6 -0
  85. package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +4 -0
  86. package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +4 -0
  87. package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +6 -0
  88. package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +4 -0
  89. package/nitrogen/generated/android/c++/JGridTile.hpp +61 -0
  90. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +12 -0
  91. package/nitrogen/generated/android/c++/JImageShape.hpp +58 -0
  92. package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +4 -0
  93. package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +6 -0
  94. package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +6 -0
  95. package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +4 -0
  96. package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +4 -0
  97. package/nitrogen/generated/android/c++/JResolvedTrack.hpp +6 -0
  98. package/nitrogen/generated/android/c++/JSection.hpp +6 -0
  99. package/nitrogen/generated/android/c++/JSectionStyle.hpp +30 -3
  100. package/nitrogen/generated/android/c++/JTrack.hpp +4 -0
  101. package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +4 -0
  102. package/nitrogen/generated/android/c++/JTrackStyle.hpp +24 -3
  103. package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +6 -0
  104. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CardImage.kt +23 -0
  105. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GridTile.kt +24 -0
  106. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageShape.kt +23 -0
  107. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SectionStyle.kt +18 -3
  108. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +15 -3
  109. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +54 -0
  110. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +9 -0
  111. package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +9 -0
  112. package/nitrogen/generated/ios/swift/CardImage.swift +40 -0
  113. package/nitrogen/generated/ios/swift/GridTile.swift +44 -0
  114. package/nitrogen/generated/ios/swift/ImageShape.swift +40 -0
  115. package/nitrogen/generated/ios/swift/SectionStyle.swift +70 -1
  116. package/nitrogen/generated/ios/swift/TrackStyle.swift +59 -1
  117. package/nitrogen/generated/shared/c++/CardImage.hpp +76 -0
  118. package/nitrogen/generated/shared/c++/GridTile.hpp +80 -0
  119. package/nitrogen/generated/shared/c++/ImageShape.hpp +76 -0
  120. package/nitrogen/generated/shared/c++/SectionStyle.hpp +32 -2
  121. package/nitrogen/generated/shared/c++/TrackStyle.hpp +25 -2
  122. package/package.json +7 -2
  123. package/src/features/playback/progress.ts +2 -1
  124. package/src/features/playback/rate.ts +8 -1
  125. package/src/style-resolution.test.ts +72 -1
  126. package/src/types/browser-nodes.ts +112 -2
  127. package/src/web/TrackPlayer/QueueManager.test.ts +29 -0
  128. package/src/web/TrackPlayer/QueueManager.ts +14 -0
  129. package/website/guide/android-auto.md +114 -0
  130. package/website/guide/android-certificates.md +409 -0
  131. package/website/guide/artwork.md +255 -0
  132. package/website/guide/audio-output.md +135 -0
  133. package/website/guide/automotive.md +86 -0
  134. package/website/guide/basic-usage.md +151 -0
  135. package/website/guide/battery.md +115 -0
  136. package/website/guide/browser.md +581 -0
  137. package/website/guide/carplay.md +620 -0
  138. package/website/guide/configuration.md +173 -0
  139. package/website/guide/equalizer.md +231 -0
  140. package/website/guide/errors.md +310 -0
  141. package/website/guide/favorites.md +154 -0
  142. package/website/guide/gate.md +143 -0
  143. package/website/guide/getting-started.md +46 -0
  144. package/website/guide/hooks.md +238 -0
  145. package/website/guide/live-streams.md +125 -0
  146. package/website/guide/metadata.md +239 -0
  147. package/website/guide/migrating-from-track-player.md +244 -0
  148. package/website/guide/native-callback-fetch.md +127 -0
  149. package/website/guide/network.md +86 -0
  150. package/website/guide/now-playing.md +175 -0
  151. package/website/guide/playback.md +328 -0
  152. package/website/guide/queue.md +300 -0
  153. package/website/guide/remote-controls.md +256 -0
  154. package/website/guide/search.md +255 -0
  155. package/website/guide/sleep-timer.md +268 -0
  156. package/website/guide/track.md +246 -0
@@ -0,0 +1,80 @@
1
+ ///
2
+ /// GridTile.hpp
3
+ /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
4
+ /// https://github.com/mrousavy/nitro
5
+ /// Copyright © Marc Rousavy @ Margelo
6
+ ///
7
+
8
+ #pragma once
9
+
10
+ #if __has_include(<NitroModules/NitroHash.hpp>)
11
+ #include <NitroModules/NitroHash.hpp>
12
+ #else
13
+ #error NitroModules cannot be found! Are you sure you installed NitroModules properly?
14
+ #endif
15
+ #if __has_include(<NitroModules/JSIConverter.hpp>)
16
+ #include <NitroModules/JSIConverter.hpp>
17
+ #else
18
+ #error NitroModules cannot be found! Are you sure you installed NitroModules properly?
19
+ #endif
20
+ #if __has_include(<NitroModules/NitroDefines.hpp>)
21
+ #include <NitroModules/NitroDefines.hpp>
22
+ #else
23
+ #error NitroModules cannot be found! Are you sure you installed NitroModules properly?
24
+ #endif
25
+
26
+ namespace margelo::nitro::audiobrowser {
27
+
28
+ /**
29
+ * An enum which can be represented as a JavaScript union (GridTile).
30
+ */
31
+ enum class GridTile {
32
+ PLAIN SWIFT_NAME(plain) = 0,
33
+ CARD SWIFT_NAME(card) = 1,
34
+ CONDENSED SWIFT_NAME(condensed) = 2,
35
+ } CLOSED_ENUM;
36
+
37
+ } // namespace margelo::nitro::audiobrowser
38
+
39
+ namespace margelo::nitro {
40
+
41
+ // C++ GridTile <> JS GridTile (union)
42
+ template <>
43
+ struct JSIConverter<margelo::nitro::audiobrowser::GridTile> final {
44
+ static inline margelo::nitro::audiobrowser::GridTile fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) {
45
+ std::string unionValue = JSIConverter<std::string>::fromJSI(runtime, arg);
46
+ switch (hashString(unionValue.c_str(), unionValue.size())) {
47
+ case hashString("plain"): return margelo::nitro::audiobrowser::GridTile::PLAIN;
48
+ case hashString("card"): return margelo::nitro::audiobrowser::GridTile::CARD;
49
+ case hashString("condensed"): return margelo::nitro::audiobrowser::GridTile::CONDENSED;
50
+ default: [[unlikely]]
51
+ throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum GridTile - invalid value!");
52
+ }
53
+ }
54
+ static inline jsi::Value toJSI(jsi::Runtime& runtime, margelo::nitro::audiobrowser::GridTile arg) {
55
+ switch (arg) {
56
+ case margelo::nitro::audiobrowser::GridTile::PLAIN: return JSIConverter<std::string>::toJSI(runtime, "plain");
57
+ case margelo::nitro::audiobrowser::GridTile::CARD: return JSIConverter<std::string>::toJSI(runtime, "card");
58
+ case margelo::nitro::audiobrowser::GridTile::CONDENSED: return JSIConverter<std::string>::toJSI(runtime, "condensed");
59
+ default: [[unlikely]]
60
+ throw std::invalid_argument("Cannot convert GridTile to JS - invalid value: "
61
+ + std::to_string(static_cast<int>(arg)) + "!");
62
+ }
63
+ }
64
+ static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) {
65
+ if (!value.isString()) {
66
+ return false;
67
+ }
68
+ std::string unionValue = JSIConverter<std::string>::fromJSI(runtime, value);
69
+ switch (hashString(unionValue.c_str(), unionValue.size())) {
70
+ case hashString("plain"):
71
+ case hashString("card"):
72
+ case hashString("condensed"):
73
+ return true;
74
+ default:
75
+ return false;
76
+ }
77
+ }
78
+ };
79
+
80
+ } // namespace margelo::nitro
@@ -0,0 +1,76 @@
1
+ ///
2
+ /// ImageShape.hpp
3
+ /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
4
+ /// https://github.com/mrousavy/nitro
5
+ /// Copyright © Marc Rousavy @ Margelo
6
+ ///
7
+
8
+ #pragma once
9
+
10
+ #if __has_include(<NitroModules/NitroHash.hpp>)
11
+ #include <NitroModules/NitroHash.hpp>
12
+ #else
13
+ #error NitroModules cannot be found! Are you sure you installed NitroModules properly?
14
+ #endif
15
+ #if __has_include(<NitroModules/JSIConverter.hpp>)
16
+ #include <NitroModules/JSIConverter.hpp>
17
+ #else
18
+ #error NitroModules cannot be found! Are you sure you installed NitroModules properly?
19
+ #endif
20
+ #if __has_include(<NitroModules/NitroDefines.hpp>)
21
+ #include <NitroModules/NitroDefines.hpp>
22
+ #else
23
+ #error NitroModules cannot be found! Are you sure you installed NitroModules properly?
24
+ #endif
25
+
26
+ namespace margelo::nitro::audiobrowser {
27
+
28
+ /**
29
+ * An enum which can be represented as a JavaScript union (ImageShape).
30
+ */
31
+ enum class ImageShape {
32
+ CIRCULAR SWIFT_NAME(circular) = 0,
33
+ ROUNDED_RECTANGLE SWIFT_NAME(roundedRectangle) = 1,
34
+ } CLOSED_ENUM;
35
+
36
+ } // namespace margelo::nitro::audiobrowser
37
+
38
+ namespace margelo::nitro {
39
+
40
+ // C++ ImageShape <> JS ImageShape (union)
41
+ template <>
42
+ struct JSIConverter<margelo::nitro::audiobrowser::ImageShape> final {
43
+ static inline margelo::nitro::audiobrowser::ImageShape fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) {
44
+ std::string unionValue = JSIConverter<std::string>::fromJSI(runtime, arg);
45
+ switch (hashString(unionValue.c_str(), unionValue.size())) {
46
+ case hashString("circular"): return margelo::nitro::audiobrowser::ImageShape::CIRCULAR;
47
+ case hashString("rounded-rectangle"): return margelo::nitro::audiobrowser::ImageShape::ROUNDED_RECTANGLE;
48
+ default: [[unlikely]]
49
+ throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum ImageShape - invalid value!");
50
+ }
51
+ }
52
+ static inline jsi::Value toJSI(jsi::Runtime& runtime, margelo::nitro::audiobrowser::ImageShape arg) {
53
+ switch (arg) {
54
+ case margelo::nitro::audiobrowser::ImageShape::CIRCULAR: return JSIConverter<std::string>::toJSI(runtime, "circular");
55
+ case margelo::nitro::audiobrowser::ImageShape::ROUNDED_RECTANGLE: return JSIConverter<std::string>::toJSI(runtime, "rounded-rectangle");
56
+ default: [[unlikely]]
57
+ throw std::invalid_argument("Cannot convert ImageShape to JS - invalid value: "
58
+ + std::to_string(static_cast<int>(arg)) + "!");
59
+ }
60
+ }
61
+ static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) {
62
+ if (!value.isString()) {
63
+ return false;
64
+ }
65
+ std::string unionValue = JSIConverter<std::string>::fromJSI(runtime, value);
66
+ switch (hashString(unionValue.c_str(), unionValue.size())) {
67
+ case hashString("circular"):
68
+ case hashString("rounded-rectangle"):
69
+ return true;
70
+ default:
71
+ return false;
72
+ }
73
+ }
74
+ };
75
+
76
+ } // namespace margelo::nitro
@@ -28,14 +28,24 @@
28
28
  #error NitroModules cannot be found! Are you sure you installed NitroModules properly?
29
29
  #endif
30
30
 
31
+ // Forward declaration of `GridTile` to properly resolve imports.
32
+ namespace margelo::nitro::audiobrowser { enum class GridTile; }
31
33
  // Forward declaration of `StyleDisplay` to properly resolve imports.
32
34
  namespace margelo::nitro::audiobrowser { enum class StyleDisplay; }
33
35
  // Forward declaration of `ArtworkRendering` to properly resolve imports.
34
36
  namespace margelo::nitro::audiobrowser { enum class ArtworkRendering; }
37
+ // Forward declaration of `ImageShape` to properly resolve imports.
38
+ namespace margelo::nitro::audiobrowser { enum class ImageShape; }
39
+ // Forward declaration of `CardImage` to properly resolve imports.
40
+ namespace margelo::nitro::audiobrowser { enum class CardImage; }
35
41
 
36
42
  #include <optional>
43
+ #include "GridTile.hpp"
37
44
  #include "StyleDisplay.hpp"
38
45
  #include "ArtworkRendering.hpp"
46
+ #include "ImageShape.hpp"
47
+ #include <string>
48
+ #include "CardImage.hpp"
39
49
 
40
50
  namespace margelo::nitro::audiobrowser {
41
51
 
@@ -45,12 +55,17 @@ namespace margelo::nitro::audiobrowser {
45
55
  struct SectionStyle final {
46
56
  public:
47
57
  std::optional<bool> gridWrap SWIFT_PRIVATE;
58
+ std::optional<GridTile> gridTile SWIFT_PRIVATE;
48
59
  std::optional<StyleDisplay> display SWIFT_PRIVATE;
49
60
  std::optional<ArtworkRendering> artworkRendering SWIFT_PRIVATE;
61
+ std::optional<ImageShape> imageShape SWIFT_PRIVATE;
62
+ std::optional<std::string> accessorySymbol SWIFT_PRIVATE;
63
+ std::optional<std::string> cardTint SWIFT_PRIVATE;
64
+ std::optional<CardImage> cardImage SWIFT_PRIVATE;
50
65
 
51
66
  public:
52
67
  SectionStyle() = default;
53
- explicit SectionStyle(std::optional<bool> gridWrap, std::optional<StyleDisplay> display, std::optional<ArtworkRendering> artworkRendering): gridWrap(gridWrap), display(display), artworkRendering(artworkRendering) {}
68
+ explicit SectionStyle(std::optional<bool> gridWrap, std::optional<GridTile> gridTile, std::optional<StyleDisplay> display, std::optional<ArtworkRendering> artworkRendering, std::optional<ImageShape> imageShape, std::optional<std::string> accessorySymbol, std::optional<std::string> cardTint, std::optional<CardImage> cardImage): gridWrap(gridWrap), gridTile(gridTile), display(display), artworkRendering(artworkRendering), imageShape(imageShape), accessorySymbol(accessorySymbol), cardTint(cardTint), cardImage(cardImage) {}
54
69
 
55
70
  public:
56
71
  friend bool operator==(const SectionStyle& lhs, const SectionStyle& rhs) = default;
@@ -67,15 +82,25 @@ namespace margelo::nitro {
67
82
  jsi::Object obj = arg.asObject(runtime);
68
83
  return margelo::nitro::audiobrowser::SectionStyle(
69
84
  JSIConverter<std::optional<bool>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "gridWrap"))),
85
+ JSIConverter<std::optional<margelo::nitro::audiobrowser::GridTile>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "gridTile"))),
70
86
  JSIConverter<std::optional<margelo::nitro::audiobrowser::StyleDisplay>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "display"))),
71
- JSIConverter<std::optional<margelo::nitro::audiobrowser::ArtworkRendering>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "artworkRendering")))
87
+ JSIConverter<std::optional<margelo::nitro::audiobrowser::ArtworkRendering>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "artworkRendering"))),
88
+ JSIConverter<std::optional<margelo::nitro::audiobrowser::ImageShape>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "imageShape"))),
89
+ JSIConverter<std::optional<std::string>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "accessorySymbol"))),
90
+ JSIConverter<std::optional<std::string>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "cardTint"))),
91
+ JSIConverter<std::optional<margelo::nitro::audiobrowser::CardImage>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "cardImage")))
72
92
  );
73
93
  }
74
94
  static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::audiobrowser::SectionStyle& arg) {
75
95
  jsi::Object obj(runtime);
76
96
  obj.setProperty(runtime, PropNameIDCache::get(runtime, "gridWrap"), JSIConverter<std::optional<bool>>::toJSI(runtime, arg.gridWrap));
97
+ obj.setProperty(runtime, PropNameIDCache::get(runtime, "gridTile"), JSIConverter<std::optional<margelo::nitro::audiobrowser::GridTile>>::toJSI(runtime, arg.gridTile));
77
98
  obj.setProperty(runtime, PropNameIDCache::get(runtime, "display"), JSIConverter<std::optional<margelo::nitro::audiobrowser::StyleDisplay>>::toJSI(runtime, arg.display));
78
99
  obj.setProperty(runtime, PropNameIDCache::get(runtime, "artworkRendering"), JSIConverter<std::optional<margelo::nitro::audiobrowser::ArtworkRendering>>::toJSI(runtime, arg.artworkRendering));
100
+ obj.setProperty(runtime, PropNameIDCache::get(runtime, "imageShape"), JSIConverter<std::optional<margelo::nitro::audiobrowser::ImageShape>>::toJSI(runtime, arg.imageShape));
101
+ obj.setProperty(runtime, PropNameIDCache::get(runtime, "accessorySymbol"), JSIConverter<std::optional<std::string>>::toJSI(runtime, arg.accessorySymbol));
102
+ obj.setProperty(runtime, PropNameIDCache::get(runtime, "cardTint"), JSIConverter<std::optional<std::string>>::toJSI(runtime, arg.cardTint));
103
+ obj.setProperty(runtime, PropNameIDCache::get(runtime, "cardImage"), JSIConverter<std::optional<margelo::nitro::audiobrowser::CardImage>>::toJSI(runtime, arg.cardImage));
79
104
  return obj;
80
105
  }
81
106
  static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) {
@@ -87,8 +112,13 @@ namespace margelo::nitro {
87
112
  return false;
88
113
  }
89
114
  if (!JSIConverter<std::optional<bool>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "gridWrap")))) return false;
115
+ if (!JSIConverter<std::optional<margelo::nitro::audiobrowser::GridTile>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "gridTile")))) return false;
90
116
  if (!JSIConverter<std::optional<margelo::nitro::audiobrowser::StyleDisplay>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "display")))) return false;
91
117
  if (!JSIConverter<std::optional<margelo::nitro::audiobrowser::ArtworkRendering>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "artworkRendering")))) return false;
118
+ if (!JSIConverter<std::optional<margelo::nitro::audiobrowser::ImageShape>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "imageShape")))) return false;
119
+ if (!JSIConverter<std::optional<std::string>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "accessorySymbol")))) return false;
120
+ if (!JSIConverter<std::optional<std::string>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "cardTint")))) return false;
121
+ if (!JSIConverter<std::optional<margelo::nitro::audiobrowser::CardImage>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "cardImage")))) return false;
92
122
  return true;
93
123
  }
94
124
  };
@@ -32,10 +32,17 @@
32
32
  namespace margelo::nitro::audiobrowser { enum class StyleDisplay; }
33
33
  // Forward declaration of `ArtworkRendering` to properly resolve imports.
34
34
  namespace margelo::nitro::audiobrowser { enum class ArtworkRendering; }
35
+ // Forward declaration of `ImageShape` to properly resolve imports.
36
+ namespace margelo::nitro::audiobrowser { enum class ImageShape; }
37
+ // Forward declaration of `CardImage` to properly resolve imports.
38
+ namespace margelo::nitro::audiobrowser { enum class CardImage; }
35
39
 
36
40
  #include "StyleDisplay.hpp"
37
41
  #include <optional>
38
42
  #include "ArtworkRendering.hpp"
43
+ #include "ImageShape.hpp"
44
+ #include <string>
45
+ #include "CardImage.hpp"
39
46
 
40
47
  namespace margelo::nitro::audiobrowser {
41
48
 
@@ -46,10 +53,14 @@ namespace margelo::nitro::audiobrowser {
46
53
  public:
47
54
  std::optional<StyleDisplay> display SWIFT_PRIVATE;
48
55
  std::optional<ArtworkRendering> artworkRendering SWIFT_PRIVATE;
56
+ std::optional<ImageShape> imageShape SWIFT_PRIVATE;
57
+ std::optional<std::string> accessorySymbol SWIFT_PRIVATE;
58
+ std::optional<std::string> cardTint SWIFT_PRIVATE;
59
+ std::optional<CardImage> cardImage SWIFT_PRIVATE;
49
60
 
50
61
  public:
51
62
  TrackStyle() = default;
52
- explicit TrackStyle(std::optional<StyleDisplay> display, std::optional<ArtworkRendering> artworkRendering): display(display), artworkRendering(artworkRendering) {}
63
+ explicit TrackStyle(std::optional<StyleDisplay> display, std::optional<ArtworkRendering> artworkRendering, std::optional<ImageShape> imageShape, std::optional<std::string> accessorySymbol, std::optional<std::string> cardTint, std::optional<CardImage> cardImage): display(display), artworkRendering(artworkRendering), imageShape(imageShape), accessorySymbol(accessorySymbol), cardTint(cardTint), cardImage(cardImage) {}
53
64
 
54
65
  public:
55
66
  friend bool operator==(const TrackStyle& lhs, const TrackStyle& rhs) = default;
@@ -66,13 +77,21 @@ namespace margelo::nitro {
66
77
  jsi::Object obj = arg.asObject(runtime);
67
78
  return margelo::nitro::audiobrowser::TrackStyle(
68
79
  JSIConverter<std::optional<margelo::nitro::audiobrowser::StyleDisplay>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "display"))),
69
- JSIConverter<std::optional<margelo::nitro::audiobrowser::ArtworkRendering>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "artworkRendering")))
80
+ JSIConverter<std::optional<margelo::nitro::audiobrowser::ArtworkRendering>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "artworkRendering"))),
81
+ JSIConverter<std::optional<margelo::nitro::audiobrowser::ImageShape>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "imageShape"))),
82
+ JSIConverter<std::optional<std::string>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "accessorySymbol"))),
83
+ JSIConverter<std::optional<std::string>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "cardTint"))),
84
+ JSIConverter<std::optional<margelo::nitro::audiobrowser::CardImage>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "cardImage")))
70
85
  );
71
86
  }
72
87
  static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::audiobrowser::TrackStyle& arg) {
73
88
  jsi::Object obj(runtime);
74
89
  obj.setProperty(runtime, PropNameIDCache::get(runtime, "display"), JSIConverter<std::optional<margelo::nitro::audiobrowser::StyleDisplay>>::toJSI(runtime, arg.display));
75
90
  obj.setProperty(runtime, PropNameIDCache::get(runtime, "artworkRendering"), JSIConverter<std::optional<margelo::nitro::audiobrowser::ArtworkRendering>>::toJSI(runtime, arg.artworkRendering));
91
+ obj.setProperty(runtime, PropNameIDCache::get(runtime, "imageShape"), JSIConverter<std::optional<margelo::nitro::audiobrowser::ImageShape>>::toJSI(runtime, arg.imageShape));
92
+ obj.setProperty(runtime, PropNameIDCache::get(runtime, "accessorySymbol"), JSIConverter<std::optional<std::string>>::toJSI(runtime, arg.accessorySymbol));
93
+ obj.setProperty(runtime, PropNameIDCache::get(runtime, "cardTint"), JSIConverter<std::optional<std::string>>::toJSI(runtime, arg.cardTint));
94
+ obj.setProperty(runtime, PropNameIDCache::get(runtime, "cardImage"), JSIConverter<std::optional<margelo::nitro::audiobrowser::CardImage>>::toJSI(runtime, arg.cardImage));
76
95
  return obj;
77
96
  }
78
97
  static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) {
@@ -85,6 +104,10 @@ namespace margelo::nitro {
85
104
  }
86
105
  if (!JSIConverter<std::optional<margelo::nitro::audiobrowser::StyleDisplay>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "display")))) return false;
87
106
  if (!JSIConverter<std::optional<margelo::nitro::audiobrowser::ArtworkRendering>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "artworkRendering")))) return false;
107
+ if (!JSIConverter<std::optional<margelo::nitro::audiobrowser::ImageShape>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "imageShape")))) return false;
108
+ if (!JSIConverter<std::optional<std::string>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "accessorySymbol")))) return false;
109
+ if (!JSIConverter<std::optional<std::string>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "cardTint")))) return false;
110
+ if (!JSIConverter<std::optional<margelo::nitro::audiobrowser::CardImage>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "cardImage")))) return false;
88
111
  return true;
89
112
  }
90
113
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-audio-browser",
3
- "version": "0.2.0-next.12",
3
+ "version": "0.2.0-next.13",
4
4
  "description": "react-native-audio-browser is a react native package built with Nitro",
5
5
  "keywords": [
6
6
  "react-native",
@@ -31,6 +31,7 @@
31
31
  "ios/**/*.mm",
32
32
  "ios/**/*.cpp",
33
33
  "ios/**/*.swift",
34
+ "website/guide",
34
35
  "*.podspec",
35
36
  "README.md"
36
37
  ],
@@ -46,23 +47,27 @@
46
47
  "scripts": {
47
48
  "android:format": "yarn workspace example-native android:format",
48
49
  "android:format:check": "yarn workspace example-native android:format:check",
50
+ "android:gradle-stop": "yarn workspace example-native android:gradle-stop",
49
51
  "android:ide": "yarn workspace example-native android:ide",
50
52
  "android:logs": "react-native log-android",
51
53
  "android:test": "yarn workspace example-native android:test",
54
+ "android:types": "yarn workspace example-native android:types",
52
55
  "build": "yarn prepare",
53
56
  "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
54
57
  "codegen": "node codegen/index.js --logLevel=\"debug\" && node post-script.cjs && node codegen/strip-struct-equality.cjs && yarn build",
55
58
  "prepare": "bob build && yarn hooks:install",
56
59
  "hooks:install": "git config core.hooksPath .githooks || exit 0",
57
60
  "example": "yarn workspace example-native",
61
+ "ci:android": "yarn android:format:check && yarn android:types && yarn android:test",
58
62
  "ci:format": "oxfmt --check --ignore-path .gitignore --ignore-path .oxfmtignore .",
59
63
  "ci:format:ios": "swiftformat --lint ios",
64
+ "ci:ios": "yarn ci:format:ios && yarn ios:test",
60
65
  "ci:lint": "oxlint --type-aware src",
61
66
  "format": "oxfmt --write --ignore-path .gitignore --ignore-path .oxfmtignore .",
62
67
  "ios:format": "swiftformat ios",
63
68
  "ios:ide": "yarn workspace example-native ios:ide",
64
69
  "ios:rebuild": "yarn workspace example-native ios:rebuild",
65
- "ios:test": "swift test",
70
+ "ios:test": "swift test --disable-sandbox",
66
71
  "lint": "oxlint --fix",
67
72
  "release": "semantic-release",
68
73
  "test": "vitest run",
@@ -118,7 +118,8 @@ export function usePolledProgress(updateInterval = 1000): Progress {
118
118
  }
119
119
 
120
120
  const stop = () => {
121
- clearTimeout(timer)
121
+ // Guarded: React Native types clearTimeout(handle: number) — no undefined.
122
+ if (timer !== undefined) clearTimeout(timer)
122
123
  timer = undefined
123
124
  unsubscribeState?.()
124
125
  unsubscribeState = undefined
@@ -14,8 +14,15 @@ export function getRate(): number {
14
14
 
15
15
  /**
16
16
  * Sets the playback rate.
17
+ *
18
+ * The rate is a speed multiplier, not a transport control — it is independent
19
+ * of play/pause, and setting it while paused never starts playback. Use
20
+ * {@link play} / {@link pause} for that.
21
+ *
17
22
  * @param rate - The playback rate to change to, where 0.5 would be half speed,
18
- * 1 would be regular speed, 2 would be double speed etc.
23
+ * 1 would be regular speed, 2 would be double speed etc. Must be greater than
24
+ * zero; platforms disagree on what a non-positive rate does (iOS ignores it,
25
+ * Android throws, web stalls the element), so treat it as unsupported.
19
26
  */
20
27
  export function setRate(rate: number): void {
21
28
  nativeBrowser.setRate(rate)
@@ -3,7 +3,12 @@ import { join } from 'node:path'
3
3
  import { describe, expect, it } from 'vitest'
4
4
 
5
5
  /**
6
- * Inheritance-completeness guard for the style declaration block (ADR 0011).
6
+ * Completeness guards for the style declaration block (ADR 0011) — the checks
7
+ * Nitro's `extends`-flattening took out of the type system, in two parts:
8
+ * every key must be *resolved* by each platform's `StyleResolver`, and every
9
+ * key must be *accounted for* by each platform's `InertStyleDiagnostic`.
10
+ *
11
+ * Part one — inheritance completeness.
7
12
  *
8
13
  * `SectionStyle extends TrackStyle` in the spec, but Nitro flattens `extends`
9
14
  * into independent structs on every platform — so nothing in any type system
@@ -137,3 +142,69 @@ for (const resolver of RESOLVERS) {
137
142
  })
138
143
  })
139
144
  }
145
+
146
+ /**
147
+ * Part two — diagnostic completeness.
148
+ *
149
+ * The block model traded compile-time invalid-combination errors for
150
+ * inertness, and the dev-mode `InertStyleDiagnostic` is the recovery. It is
151
+ * exposed to the same flattening problem as the resolvers, one level up: a
152
+ * key added to the spec has no type-system tie to the diagnostic's rule set,
153
+ * so a property could ship whose inert combinations nobody is warned about.
154
+ *
155
+ * Each platform therefore dispositions the whole vocabulary — every key is a
156
+ * `Property` with an inertness rule, `alwaysRenderable`, or `positional` —
157
+ * and this test reads those three declarations and asserts set equality with
158
+ * the spec, both directions: a new key must be dispositioned, and a key
159
+ * deleted from the spec must not linger in the rule set.
160
+ */
161
+ const DIAGNOSTICS = [
162
+ {
163
+ file: 'ios/Browser/InertStyleDiagnostic.swift',
164
+ // `case gridWrap`, up to the first member after the case list.
165
+ ruled: (source: string) =>
166
+ keysIn(
167
+ source,
168
+ /enum Property[\s\S]*?var isItemProperty/,
169
+ /^\s*case (\w+)$/gm
170
+ )
171
+ },
172
+ {
173
+ file: 'android/src/main/java/com/audiobrowser/browser/InertStyleDiagnostic.kt',
174
+ // `GRID_WRAP("gridWrap", …)`, up to the `;` that ends the entry list.
175
+ ruled: (source: string) =>
176
+ keysIn(source, /enum class Property[\s\S]*?;/, /\w+\("(\w+)"/g)
177
+ }
178
+ ]
179
+
180
+ /** The capture group of every `pattern` match within the first `region`. */
181
+ function keysIn(source: string, region: RegExp, pattern: RegExp): string[] {
182
+ const found = region.exec(source)
183
+ expect(found, `region ${region} not found`).not.toBeNull()
184
+ return [...found![0].matchAll(pattern)].flatMap((match) =>
185
+ match[1] === undefined ? [] : [match[1]]
186
+ )
187
+ }
188
+
189
+ for (const diagnostic of DIAGNOSTICS) {
190
+ describe(diagnostic.file, () => {
191
+ // Comment-free, so prose naming a key doesn't pass for dispositioning it.
192
+ const source = stripComments(
193
+ readFileSync(join(ROOT, diagnostic.file), 'utf8')
194
+ )
195
+ // The two lists are spelled `alwaysRenderable`/`positional` in Swift and
196
+ // SCREAMING_CASE in Kotlin; both hold bare string literals.
197
+ const listed = [
198
+ ...source.matchAll(
199
+ /(?:alwaysRenderable|ALWAYS_RENDERABLE|positional|POSITIONAL)\s*=[^\n]*/gi
200
+ )
201
+ ].flatMap((match) =>
202
+ [...match[0].matchAll(/"(\w+)"/g)].map((key) => key[1])
203
+ )
204
+
205
+ it('dispositions every key of the block, and only those', () => {
206
+ const dispositioned = [...diagnostic.ruled(source), ...listed]
207
+ expect(new Set(dispositioned)).toEqual(new Set(sectionKeys))
208
+ })
209
+ })
210
+ }
@@ -25,6 +25,50 @@ export type StyleDisplay = 'list' | 'grid'
25
25
  */
26
26
  export type ArtworkRendering = 'original' | 'stencil'
27
27
 
28
+ /**
29
+ * The shape a grid tile's artwork is cropped to.
30
+ *
31
+ * - `'rounded-rectangle'` (default) — the standard tile crop.
32
+ * - `'circular'` — the standard presentation for *people* (artists, hosts,
33
+ * DJs). Shape encodes the kind of item, so mixed grids legitimately
34
+ * render circular artists next to rounded albums.
35
+ *
36
+ * Named after Apple's `imageShape` parameter (a platform passthrough), and
37
+ * deliberately without a grid prefix: every tile presentation that takes a
38
+ * shape reads the same property.
39
+ */
40
+ export type ImageShape = 'circular' | 'rounded-rectangle'
41
+
42
+ /**
43
+ * The kind of tile a grid renders.
44
+ *
45
+ * - `'plain'` (default) — artwork tiles with a title.
46
+ * - `'card'` — a larger card with title/subtitle and a
47
+ * {@link TrackStyle.cardTint} color; its image mode is
48
+ * {@link TrackStyle.cardImage}. Cards have no shape.
49
+ * - `'condensed'` — a denser cell carrying title + subtitle; takes
50
+ * {@link TrackStyle.imageShape} and {@link TrackStyle.accessorySymbol}
51
+ * like a plain tile.
52
+ *
53
+ * Element families are mutually exclusive within a container, so this is
54
+ * a container property — hence the enum, and the `grid` prefix.
55
+ */
56
+ export type GridTile = 'plain' | 'card' | 'condensed'
57
+
58
+ /**
59
+ * How a card tile uses its image.
60
+ *
61
+ * - `'normal'` (default) — image above the labels;
62
+ * {@link TrackStyle.cardTint} colors a gradient behind the labels.
63
+ * - `'background'` — the image fills the card full-height with the labels
64
+ * overlaid; `cardTint` becomes the color behind them. Selects the larger
65
+ * artwork target size.
66
+ *
67
+ * An enum rather than a boolean: it names the card's image mode and
68
+ * leaves room for future modes.
69
+ */
70
+ export type CardImage = 'normal' | 'background'
71
+
28
72
  /**
29
73
  * The style declaration block a {@link Track} may carry: *inherited item
30
74
  * properties* (resolved `track ?? section ?? page ?? default`, per
@@ -82,6 +126,58 @@ export interface TrackStyle {
82
126
  * @platform carplay
83
127
  */
84
128
  artworkRendering?: ArtworkRendering
129
+
130
+ /**
131
+ * Inherited (`track ?? section ?? page`): the shape a grid tile's
132
+ * artwork is cropped to — `'circular'` for people, the default
133
+ * `'rounded-rectangle'` for everything else. Shape follows the item
134
+ * wherever it travels: a circular artist stays circular in whatever
135
+ * grid renders it next. Inert where the presentation has no shape
136
+ * (list rows, pre-26 CarPlay, Android Auto).
137
+ *
138
+ * @default 'rounded-rectangle'
139
+ *
140
+ * @platform carplay
141
+ */
142
+ imageShape?: ImageShape
143
+
144
+ /**
145
+ * Inherited (`track ?? section ?? page`): an SF Symbol name drawn as
146
+ * the item's accessory wherever the current presentation supports one —
147
+ * the trailing edge of a list row, leading the title on an iOS 26 grid
148
+ * tile (placement is the platform's choice per surface). A resolved
149
+ * symbol replaces the derived accessory (the disclosure chevron a
150
+ * browsable row gets); `'none'` clears an inherited value, restoring
151
+ * the derived behavior — inheritance has no other escape, and no SF
152
+ * Symbol is named `none`, so the sentinel is collision-free. Inert off
153
+ * CarPlay.
154
+ *
155
+ * @platform carplay
156
+ */
157
+ accessorySymbol?: string
158
+
159
+ /**
160
+ * Inherited (`track ?? section ?? page`): the card treatment's tint
161
+ * color (hex, e.g. `'#1e3a8a'`) — a gradient behind the labels, or the
162
+ * color behind them when {@link TrackStyle.cardImage} is
163
+ * `'background'`. Card-prefixed: inert unless the container renders
164
+ * `gridTile: 'card'`.
165
+ *
166
+ * @platform carplay
167
+ */
168
+ cardTint?: string
169
+
170
+ /**
171
+ * Inherited (`track ?? section ?? page`): how a card tile uses its
172
+ * image — see {@link CardImage}. Uniform via the section value,
173
+ * mixable per card. Inert unless the container renders
174
+ * `gridTile: 'card'`.
175
+ *
176
+ * @default 'normal'
177
+ *
178
+ * @platform carplay
179
+ */
180
+ cardImage?: CardImage
85
181
  }
86
182
 
87
183
  /**
@@ -113,6 +209,20 @@ export interface SectionStyle extends TrackStyle {
113
209
  * @see https://developer.apple.com/documentation/carplay/cplistimagerowitem
114
210
  */
115
211
  gridWrap?: boolean
212
+
213
+ /**
214
+ * The kind of tile this container's grid renders — see
215
+ * {@link GridTile}. A container property (element families are
216
+ * container-homogeneous), resolved by scope override
217
+ * (`section ?? page`), never per item. Inert unless
218
+ * {@link TrackStyle.display} is `'grid'`, and on CarPlay before
219
+ * iOS 26 (where the treatment drops and the layout survives).
220
+ *
221
+ * @default 'plain'
222
+ *
223
+ * @platform carplay
224
+ */
225
+ gridTile?: GridTile
116
226
  }
117
227
 
118
228
  /**
@@ -200,8 +310,8 @@ export interface Section {
200
310
  /**
201
311
  * Navigation target for the section header / "view all" surface. A
202
312
  * tile section's header tap (CarPlay) and appended "view all" link
203
- * (Android Auto) navigate here. Absent = a pure preview; the header is
204
- * not tappable.
313
+ * (Android Auto) navigate here. Absent = a self-contained section:
314
+ * the header is a plain label, not a link.
205
315
  */
206
316
  path?: string
207
317
  /** The section's tracks. */
@@ -160,6 +160,35 @@ describe('QueueManager', () => {
160
160
  expect(q.nextIndex()).toBeUndefined()
161
161
  })
162
162
 
163
+ // Filling a queue that had no current track: the order is shuffled with
164
+ // nothing to lead it, so the track that becomes current can land anywhere —
165
+ // last included, which reads as end-of-queue from the very first track.
166
+ it('leads the order with the start track when setTracks fills an empty queue', () => {
167
+ const fresh = new QueueManager()
168
+ fresh.setShuffleEnabled(true) // shuffle already on, queue still empty
169
+ fresh.setTracks(tracks(4))
170
+ fresh.currentIndex = 2 // the start index
171
+
172
+ // identity order [0,1,2,3] re-pinned on 2 → [2,1,0,3]
173
+ expect(fresh.previousIndex()).toBeUndefined()
174
+ expect(fresh.nextIndex()).toBe(1)
175
+ })
176
+
177
+ it('leads the order with the start track when insert fills an empty queue', () => {
178
+ const fresh = new QueueManager()
179
+ fresh.setShuffleEnabled(true)
180
+ const random = vi.spyOn(Math, 'random').mockReturnValue(0.999)
181
+ try {
182
+ fresh.insert(tracks(4)) // slotted in at the order's end → [0,1,2,3]
183
+ } finally {
184
+ random.mockRestore()
185
+ }
186
+ fresh.currentIndex = 3 // would sit LAST in the order without the re-pin
187
+
188
+ expect(fresh.nextIndex()).toBe(1)
189
+ expect(fresh.previousIndex()).toBeUndefined()
190
+ })
191
+
163
192
  it('keeps every track at its shuffle position across a move', () => {
164
193
  q.currentIndex = 1
165
194
  q.move(1, 3) // tracks [s0,s2,s3,s1]; order remapped to [0,3,1,2]