react-native-audio-browser 0.2.0-next.12 → 0.2.0-next.14
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.
- package/README.md +10 -0
- package/android/build.gradle +20 -1
- package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +8 -1
- package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +52 -0
- package/android/src/main/java/com/audiobrowser/browser/InertStyleDiagnostic.kt +267 -0
- package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +27 -11
- package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +1 -1
- package/android/src/main/java/com/audiobrowser/browser/StyleResolver.kt +11 -0
- package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +11 -1
- package/android/src/main/java/com/audiobrowser/model/PlayerUpdateOptions.kt +1 -1
- package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +21 -22
- package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +17 -2
- package/android/src/main/java/com/audiobrowser/player/Player.kt +3 -0
- package/android/src/main/java/com/audiobrowser/player/ShuffleOrderLedBy.kt +30 -0
- package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +73 -0
- package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +16 -6
- package/android/src/main/res/values/strings.xml +37 -2
- package/android/src/test/java/com/audiobrowser/TestFixtures.kt +50 -5
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerLayerResolutionTest.kt +9 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +3 -2
- package/android/src/test/java/com/audiobrowser/browser/InertStyleDiagnosticTest.kt +277 -0
- package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +56 -0
- package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +4 -4
- package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +77 -10
- package/android/src/test/java/com/audiobrowser/player/PlaybackStateStoreTrackJsonTest.kt +2 -2
- package/android/src/test/java/com/audiobrowser/player/ShuffleOrderLedByTest.kt +79 -0
- package/android/src/test/java/com/audiobrowser/util/MediaExtrasBuilderTest.kt +10 -9
- package/ios/Browser/BrowserManager.swift +106 -45
- package/ios/Browser/InertStyleDiagnostic.swift +279 -0
- package/ios/Browser/JsonModels.swift +81 -15
- package/ios/Browser/SectionPresentation.swift +30 -0
- package/ios/Browser/StyleResolver.swift +11 -0
- package/ios/Browser/TabBarEntries.swift +7 -1
- package/ios/CarPlay/CarPlayController.swift +139 -35
- package/ios/CarPlay/CarPlayListItemFactory.swift +99 -27
- package/ios/Extension/Section+Copying.swift +1 -3
- package/ios/Extension/UIApplication+ActiveScene.swift +27 -0
- package/ios/Http/HttpClient.swift +8 -2
- package/ios/HybridAudioBrowser.swift +56 -12
- package/ios/Model/NitroTypeStubs.swift +60 -1
- package/ios/Player/MediaIntentCriteria.swift +13 -5
- package/ios/Player/MediaLoader.swift +33 -9
- package/ios/Player/NowPlayingUpdater.swift +53 -5
- package/ios/Player/PlaybackCoordinator.swift +11 -4
- package/ios/Player/PlaybackTimer.swift +8 -2
- package/ios/Player/QueueManager.swift +36 -10
- package/ios/Player/ShuffleOrder.swift +23 -0
- package/ios/Player/SleepTimerHandling.swift +13 -0
- package/ios/RemoteCommand/RemoteCommandController.swift +53 -18
- package/ios/Tests/InertStyleDiagnosticTests.swift +200 -0
- package/ios/Tests/JsonModelsTests.swift +76 -1
- package/ios/Tests/QueueManagerTests.swift +72 -6
- package/ios/Tests/RemoteCommandControllerTests.swift +16 -0
- package/ios/Tests/ResolvedTrackCopyingTests.swift +2 -2
- package/ios/Tests/SectionPresentationTests.swift +29 -0
- package/ios/Tests/StyleResolverTests.swift +60 -0
- package/ios/Tests/Support/TestTypes.swift +7 -0
- package/ios/TrackPlayer.swift +57 -34
- package/ios/Util/NetworkMonitor.swift +43 -26
- package/ios/Util/SFSymbolRenderer.swift +24 -2
- package/ios/Util/SVGProcessor.swift +9 -2
- package/lib/commonjs/features/browser-config.js +1 -0
- package/lib/commonjs/features/browser-config.js.map +1 -1
- package/lib/commonjs/features/playback/progress.js +2 -1
- package/lib/commonjs/features/playback/progress.js.map +1 -1
- package/lib/commonjs/features/playback/rate.js +8 -1
- package/lib/commonjs/features/playback/rate.js.map +1 -1
- package/lib/commonjs/web/TrackPlayer/QueueManager.js +14 -0
- package/lib/commonjs/web/TrackPlayer/QueueManager.js.map +1 -1
- package/lib/module/features/browser-config.js +1 -0
- package/lib/module/features/browser-config.js.map +1 -1
- package/lib/module/features/playback/progress.js +2 -1
- package/lib/module/features/playback/progress.js.map +1 -1
- package/lib/module/features/playback/rate.js +8 -1
- package/lib/module/features/playback/rate.js.map +1 -1
- package/lib/module/web/TrackPlayer/QueueManager.js +14 -0
- package/lib/module/web/TrackPlayer/QueueManager.js.map +1 -1
- package/lib/typescript/src/features/browser-config.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/progress.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/rate.d.ts +8 -1
- package/lib/typescript/src/features/playback/rate.d.ts.map +1 -1
- package/lib/typescript/src/types/browser-native.d.ts +2 -1
- package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
- package/lib/typescript/src/types/browser-nodes.d.ts +116 -9
- package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
- package/lib/typescript/src/types/browser.d.ts +36 -0
- package/lib/typescript/src/types/browser.d.ts.map +1 -1
- package/nitrogen/generated/android/AudioBrowserOnLoad.cpp +2 -0
- package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +4 -0
- package/nitrogen/generated/android/c++/JBrowseResult.hpp +6 -0
- package/nitrogen/generated/android/c++/JCardImage.hpp +58 -0
- package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +4 -0
- package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +4 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +4 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +4 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +4 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +4 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +4 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +4 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__string__.hpp +102 -0
- 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
- package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +4 -0
- package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +4 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +6 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +4 -0
- package/nitrogen/generated/android/c++/JGridTile.hpp +61 -0
- package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +13 -0
- package/nitrogen/generated/android/c++/JImageShape.hpp +58 -0
- package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +4 -0
- package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +20 -1
- package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +6 -0
- package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +4 -0
- package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +4 -0
- package/nitrogen/generated/android/c++/JResolvedTrack.hpp +6 -0
- package/nitrogen/generated/android/c++/JSection.hpp +7 -5
- package/nitrogen/generated/android/c++/JSectionStyle.hpp +30 -3
- package/nitrogen/generated/android/c++/JTrack.hpp +4 -0
- package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +4 -0
- package/nitrogen/generated/android/c++/JTrackStyle.hpp +24 -3
- package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +6 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CardImage.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__string__.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GridTile.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageShape.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +7 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Section.kt +2 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SectionStyle.kt +18 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +15 -3
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.cpp +9 -0
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +104 -0
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +9 -0
- package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +9 -0
- package/nitrogen/generated/ios/swift/CardImage.swift +40 -0
- package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__string__.swift +54 -0
- package/nitrogen/generated/ios/swift/GridTile.swift +44 -0
- package/nitrogen/generated/ios/swift/ImageShape.swift +40 -0
- package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +48 -1
- package/nitrogen/generated/ios/swift/Section.swift +1 -19
- package/nitrogen/generated/ios/swift/SectionStyle.swift +70 -1
- package/nitrogen/generated/ios/swift/TrackStyle.swift +59 -1
- package/nitrogen/generated/shared/c++/CardImage.hpp +76 -0
- package/nitrogen/generated/shared/c++/GridTile.hpp +80 -0
- package/nitrogen/generated/shared/c++/ImageShape.hpp +76 -0
- package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +6 -2
- package/nitrogen/generated/shared/c++/Section.hpp +1 -5
- package/nitrogen/generated/shared/c++/SectionStyle.hpp +32 -2
- package/nitrogen/generated/shared/c++/TrackStyle.hpp +25 -2
- package/package.json +7 -2
- package/src/features/browser-config.ts +1 -0
- package/src/features/playback/progress.ts +2 -1
- package/src/features/playback/rate.ts +8 -1
- package/src/style-resolution.test.ts +72 -1
- package/src/types/browser-native.ts +5 -1
- package/src/types/browser-nodes.ts +124 -9
- package/src/types/browser.ts +38 -0
- package/src/web/TrackPlayer/QueueManager.test.ts +29 -0
- package/src/web/TrackPlayer/QueueManager.ts +14 -0
- package/website/guide/android-auto.md +114 -0
- package/website/guide/android-certificates.md +409 -0
- package/website/guide/artwork.md +255 -0
- package/website/guide/audio-output.md +135 -0
- package/website/guide/automotive.md +86 -0
- package/website/guide/basic-usage.md +151 -0
- package/website/guide/battery.md +115 -0
- package/website/guide/browser.md +581 -0
- package/website/guide/carplay.md +620 -0
- package/website/guide/configuration.md +173 -0
- package/website/guide/equalizer.md +231 -0
- package/website/guide/errors.md +310 -0
- package/website/guide/favorites.md +154 -0
- package/website/guide/gate.md +143 -0
- package/website/guide/getting-started.md +46 -0
- package/website/guide/hooks.md +238 -0
- package/website/guide/live-streams.md +125 -0
- package/website/guide/metadata.md +239 -0
- package/website/guide/migrating-from-track-player.md +244 -0
- package/website/guide/native-callback-fetch.md +127 -0
- package/website/guide/network.md +86 -0
- package/website/guide/now-playing.md +175 -0
- package/website/guide/playback.md +328 -0
- package/website/guide/queue.md +300 -0
- package/website/guide/remote-controls.md +256 -0
- package/website/guide/search.md +255 -0
- package/website/guide/sleep-timer.md +268 -0
- package/website/guide/track.md +246 -0
|
@@ -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.
|
|
3
|
+
"version": "0.2.0-next.14",
|
|
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",
|
|
@@ -289,6 +289,7 @@ export function toNativeConfig(
|
|
|
289
289
|
singleTrack: config.singleTrack,
|
|
290
290
|
handleTrackLoad: config.handleTrackLoad,
|
|
291
291
|
androidControllerOfflineError: config.androidControllerOfflineError,
|
|
292
|
+
viewMoreTitle: config.viewMoreTitle,
|
|
292
293
|
carPlayLoadingTitle: config.carPlayLoadingTitle,
|
|
293
294
|
resolveAlbumPath: config.resolveAlbumPath,
|
|
294
295
|
formatNavigationError: config.formatNavigationError
|
|
@@ -118,7 +118,8 @@ export function usePolledProgress(updateInterval = 1000): Progress {
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
const stop = () => {
|
|
121
|
-
clearTimeout(
|
|
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
|
-
*
|
|
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
|
+
}
|
|
@@ -14,7 +14,8 @@ import type {
|
|
|
14
14
|
HandleTrackLoadCallback,
|
|
15
15
|
MediaRequestConfig,
|
|
16
16
|
SearchSourceCallback,
|
|
17
|
-
TransformableRequestConfig
|
|
17
|
+
TransformableRequestConfig,
|
|
18
|
+
ViewMoreTitleCallback
|
|
18
19
|
} from './browser'
|
|
19
20
|
import type { ResolvedTrack } from './browser-nodes'
|
|
20
21
|
|
|
@@ -73,6 +74,9 @@ export interface NativeBrowserConfiguration {
|
|
|
73
74
|
singleTrack?: boolean
|
|
74
75
|
handleTrackLoad?: HandleTrackLoadCallback
|
|
75
76
|
androidControllerOfflineError?: boolean
|
|
77
|
+
// Title of the "view more" row appended to a pathed section where the header
|
|
78
|
+
// can't be tapped (Android Auto). Absent = native's "View more" default.
|
|
79
|
+
viewMoreTitle?: ViewMoreTitleCallback
|
|
76
80
|
|
|
77
81
|
// CarPlay options
|
|
78
82
|
carPlayLoadingTitle?: string
|
|
@@ -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
|
/**
|
|
@@ -183,11 +293,6 @@ export type TrackArtwork = string | ArtworkVariants
|
|
|
183
293
|
export interface Section {
|
|
184
294
|
/** Header text. Absent = headerless group. */
|
|
185
295
|
title?: string
|
|
186
|
-
/**
|
|
187
|
-
* Secondary line for the section's navigation surface — e.g. the label
|
|
188
|
-
* of the "view all" link a tile section gets on Android Auto.
|
|
189
|
-
*/
|
|
190
|
-
subtitle?: string
|
|
191
296
|
/**
|
|
192
297
|
* Presentation, separated from content. Each surface renders the
|
|
193
298
|
* declared layout's nearest supported form; on CarPlay
|
|
@@ -198,10 +303,20 @@ export interface Section {
|
|
|
198
303
|
*/
|
|
199
304
|
style?: SectionStyle
|
|
200
305
|
/**
|
|
201
|
-
* Navigation target for the section
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
306
|
+
* Navigation target for the section's "view all" affordance — the
|
|
307
|
+
* fuller page this section is a teaser for. Absent = a self-contained
|
|
308
|
+
* section: the header is a plain label, not a link.
|
|
309
|
+
*
|
|
310
|
+
* The surface picks the form (ADR 0010), and each carries the copy
|
|
311
|
+
* itself — a `path` never adds words to the section:
|
|
312
|
+
*
|
|
313
|
+
* - **CarPlay** — a tile section's row draws a disclosure chevron and
|
|
314
|
+
* the whole row navigates. A list section renders no affordance:
|
|
315
|
+
* CarPlay has no "see all" idiom for one, and an invented row would
|
|
316
|
+
* spend an item from the template's cap.
|
|
317
|
+
* - **Android Auto** — no header is tappable at any display, so the
|
|
318
|
+
* section gets an appended browsable row, titled by the global
|
|
319
|
+
* `viewMoreTitle` browser option ("View more" by default).
|
|
205
320
|
*/
|
|
206
321
|
path?: string
|
|
207
322
|
/** The section's tracks. */
|
package/src/types/browser.ts
CHANGED
|
@@ -931,6 +931,41 @@ export type BrowserConfiguration = {
|
|
|
931
931
|
*/
|
|
932
932
|
androidControllerOfflineError?: boolean
|
|
933
933
|
|
|
934
|
+
/**
|
|
935
|
+
* Titles the "view more" row appended to a `Section` that carries a
|
|
936
|
+
* `Section.path` on surfaces whose section header cannot be tapped —
|
|
937
|
+
* Android Auto, today the only one.
|
|
938
|
+
*
|
|
939
|
+
* Leave it unset and the row is titled "View more" — the library's in-code
|
|
940
|
+
* default, the same arrangement `formatNavigationError` has. Set this to
|
|
941
|
+
* localize it: it is re-resolved once per content generation, so
|
|
942
|
+
* `invalidateAllContent()` after an in-app language switch picks up the new
|
|
943
|
+
* copy without reconfiguring the browser. That is why it is a callback rather
|
|
944
|
+
* than a plain string — an app whose locale is its own, not the system's,
|
|
945
|
+
* cannot express that with a value frozen at configure time.
|
|
946
|
+
*
|
|
947
|
+
* One string for every section by design — the row is a fallback for a
|
|
948
|
+
* surface that cannot draw a link in its header, so per-section wording would
|
|
949
|
+
* be ceremony most consumers never see rendered.
|
|
950
|
+
*
|
|
951
|
+
* For wording that varies by section — "View all 42 albums", "See every
|
|
952
|
+
* episode" — author the link yourself instead: append an ordinary browsable
|
|
953
|
+
* {@link Track} to the section's `children`, titled however you like, and
|
|
954
|
+
* leave `Section.path` unset so this row doesn't also appear. Since the
|
|
955
|
+
* row is Android-only, that branch is an ordinary platform check on the
|
|
956
|
+
* content you emit; CarPlay never sees the extra row.
|
|
957
|
+
*
|
|
958
|
+
* @default 'View more'
|
|
959
|
+
*
|
|
960
|
+
* @example
|
|
961
|
+
* ```typescript
|
|
962
|
+
* viewMoreTitle: () => t('View more')
|
|
963
|
+
* ```
|
|
964
|
+
*
|
|
965
|
+
* @platform android
|
|
966
|
+
*/
|
|
967
|
+
viewMoreTitle?: ViewMoreTitleCallback
|
|
968
|
+
|
|
934
969
|
/**
|
|
935
970
|
* Title shown (as the list's centered empty state) on CarPlay screens whose
|
|
936
971
|
* content is still loading — browse destinations while they resolve, and the
|
|
@@ -1093,3 +1128,6 @@ export type FormatNavigationErrorCallback = (
|
|
|
1093
1128
|
* or `undefined` to do nothing. See `resolveAlbumPath`.
|
|
1094
1129
|
*/
|
|
1095
1130
|
export type ResolveAlbumPathCallback = (track: Track) => string | undefined
|
|
1131
|
+
|
|
1132
|
+
/** Supplies the title of the appended "view more" row. See `viewMoreTitle`. */
|
|
1133
|
+
export type ViewMoreTitleCallback = () => string
|
|
@@ -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]
|
|
@@ -63,8 +63,22 @@ export class QueueManager {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
set currentIndex(index: number | undefined) {
|
|
66
|
+
const hadNoCurrent = this._currentIndex === undefined
|
|
66
67
|
this._lastIndex = this._currentIndex
|
|
67
68
|
this._currentIndex = index
|
|
69
|
+
|
|
70
|
+
// A queue with no current track has an order that was shuffled with nothing
|
|
71
|
+
// to lead it — `setTracks` clears the index before regenerating, and
|
|
72
|
+
// `insert` drops new indices at random positions — so the track that then
|
|
73
|
+
// becomes current can sit anywhere, including last. That reads as
|
|
74
|
+
// end-of-queue immediately: `nextIndex` is undefined from the first track,
|
|
75
|
+
// so auto-advance fires onQueueEnded after one track. Re-pin at the moment
|
|
76
|
+
// the queue gains a current track. Safe as a setter hook here because the
|
|
77
|
+
// tracks are always in place before the index is assigned (both `setTracks`
|
|
78
|
+
// and `insert` run first).
|
|
79
|
+
if (hadNoCurrent && index !== undefined && this._shuffleEnabled) {
|
|
80
|
+
this.regenerateShuffleOrder()
|
|
81
|
+
}
|
|
68
82
|
}
|
|
69
83
|
|
|
70
84
|
get lastIndex(): number | undefined {
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: 'Put your browse tree and playback on the Android Auto car screen: the automotive app descriptor, the manifest entries the library merges in, and "Hey Google, play…" voice search.'
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Android Auto
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
Android Auto shows your [browse tree](/guide/basic-usage) on the car screen and plays through the same player as your app. Two systems drive it:
|
|
10
|
+
|
|
11
|
+
1. **Browse tree** — Android Auto reads your content from a `MediaBrowserService`, which mirrors your browser routes. The library ships this service.
|
|
12
|
+
2. **Voice search** — "Hey Google, play…" arrives as a `MEDIA_PLAY_FROM_SEARCH` intent, which the library routes to your `search` source.
|
|
13
|
+
|
|
14
|
+
Unlike CarPlay — which needs entitlements and scene delegates — Android Auto needs almost no app-side code. The library's own `AndroidManifest.xml` declares the media-browser service, the media session, the voice-search intent filter, and the foreground-service permissions, and those **merge into your app automatically**. You only have to declare your app as an automotive media app.
|
|
15
|
+
|
|
16
|
+
## Setup
|
|
17
|
+
|
|
18
|
+
### 1. Declare the automotive app descriptor
|
|
19
|
+
|
|
20
|
+
Add this `<meta-data>` inside the `<application>` tag of your app's `AndroidManifest.xml`:
|
|
21
|
+
|
|
22
|
+
```xml
|
|
23
|
+
<meta-data
|
|
24
|
+
android:name="com.google.android.gms.car.application"
|
|
25
|
+
android:resource="@xml/automotive_app_desc" />
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### 2. Add the descriptor resource
|
|
29
|
+
|
|
30
|
+
Create `android/app/src/main/res/xml/automotive_app_desc.xml`:
|
|
31
|
+
|
|
32
|
+
```xml
|
|
33
|
+
<automotiveApp>
|
|
34
|
+
<uses name="media"/>
|
|
35
|
+
</automotiveApp>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
That's the whole setup. The `MediaBrowserService`, media session, `MEDIA_PLAY_FROM_SEARCH` intent filter, headless service for background playback, and `FOREGROUND_SERVICE` / `FOREGROUND_SERVICE_MEDIA_PLAYBACK` permissions all come from the library's merged manifest — you don't declare them yourself.
|
|
39
|
+
|
|
40
|
+
## Voice search
|
|
41
|
+
|
|
42
|
+
Google Assistant requests ("play some jazz", "play the radishes") arrive as a `MEDIA_PLAY_FROM_SEARCH` intent. The library normalizes it into the same structured `SearchParams` your in-app and Siri searches use and calls your `search` source — no extra manifest setup, since the library declares the intent filter.
|
|
43
|
+
|
|
44
|
+
Android sends a subset of what iOS does: the query plus music focuses (genre / artist / album), always with `reference: 'unknown'`. See [Search](/guide/search) for how to resolve these requests and the cross-platform differences.
|
|
45
|
+
|
|
46
|
+
## Browse display
|
|
47
|
+
|
|
48
|
+
Android Auto reads a page's layout from the **parent item** that opens it — before the page resolves. So the layout is declared in two places: on the parent item (a tab, or a row on another page) and on the page itself.
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
// The parent item — a tab, or a row on another page. Android Auto
|
|
52
|
+
// reads the layout from here, before the page resolves.
|
|
53
|
+
{
|
|
54
|
+
title: 'Stations',
|
|
55
|
+
path: '/browse/stations',
|
|
56
|
+
style: { display: 'grid' }, // layout of the page this opens ('list' is the default)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// The page it opens — the ResolvedTrack served for '/browse/stations':
|
|
60
|
+
{
|
|
61
|
+
path: '/browse/stations',
|
|
62
|
+
title: 'Stations',
|
|
63
|
+
style: { display: 'grid' }, // the page-wide declaration
|
|
64
|
+
children: [ /* the tiles */ ]
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- **`style.display` on the parent item** — `'list'` (rows) or `'grid'` (tiles). Below the root this is the only layout signal Android Auto honors, and it is only sent when you declare it — leave it off and the page renders as a list. In dev mode the library warns when an all-grid page is opened from a parent item with no declared `display`. (A section's "view all" page needs no declaration of its own: it reuses the section's `style`.)
|
|
69
|
+
- **Sections** — declare `sections` on the page to group its tracks under headers; each [`Section`](/api/types/browser-nodes/#section)'s `title` renders as a header above its children. A section that carries a `path` also gets a browsable "view more" row appended after its children, navigating there. No header is tappable on Android Auto at any `display`, so the row is the only affordance the section's `path` has here; its title comes from the [`viewMoreTitle`](/api/types/browser/#viewmoretitle) browser option, defaulting to `"View more"` — one wording for every section. For per-section copy (`"View all 42 albums"`), leave `path` unset and append your own browsable `Track` to `children` instead, titled however you like; this page's content is Android-only anyway, so CarPlay never sees it.
|
|
70
|
+
|
|
71
|
+
::: warning AAOS lays out the whole page from the parent item's `display`
|
|
72
|
+
On AOSP-based Android Automotive units — including mainstream cars' OEM skins — the parent item's `display` is the **only** below-root layout signal honored: the whole page renders as list or grid, uniformly. Per-section `display` differences (a single-line grid above a list, say) render on CarPlay and projected Android Auto, but an AAOS unit shows the page in the single declared layout. There is no way to describe a mixed page there — pick the layout that suits its dominant section, or split the content across pages. `gridWrap: false` also has no single-line container there: the grid wraps, showing more, never less.
|
|
73
|
+
:::
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
sections: [
|
|
77
|
+
{
|
|
78
|
+
title: 'Live now',
|
|
79
|
+
children: [
|
|
80
|
+
{ title: 'Morning Show', src: '…' },
|
|
81
|
+
{ title: 'Afternoon Drive', src: '…' }
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
{ title: 'Up next', children: [{ title: 'Late Night Jazz', src: '…' }] }
|
|
85
|
+
]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Now Playing buttons
|
|
89
|
+
|
|
90
|
+
The buttons on the Now Playing screen come from the same
|
|
91
|
+
[`remoteButtonLayout`](/guide/remote-controls#button-layout-android) that drives the
|
|
92
|
+
notification — there's no car-specific option. Two behaviors are particular to
|
|
93
|
+
the car, though:
|
|
94
|
+
|
|
95
|
+
- **The head unit has the last word.** Slots are a preference. A unit with a
|
|
96
|
+
spare position may promote the first `overflow` entry onto the main row, so
|
|
97
|
+
a button you placed in overflow can still appear beside play/pause.
|
|
98
|
+
- **Fewer buttons, cleaner row.** If you want just
|
|
99
|
+
`jump-back │ play/pause │ jump-forward`, leave the rest out of the layout
|
|
100
|
+
entirely — they keep working from the steering wheel and headset regardless.
|
|
101
|
+
|
|
102
|
+
## Testing
|
|
103
|
+
|
|
104
|
+
Test in a real car, or in Google's **Desktop Head Unit (DHU)** emulator:
|
|
105
|
+
|
|
106
|
+
1. Install the **Android Auto Desktop Head Unit emulator** from the SDK Manager (_SDK Tools → Android Auto Desktop Head Unit emulator_).
|
|
107
|
+
2. In the phone's **Android Auto** settings, enable Developer Mode (tap the version 10×), then **"Start head unit server"**.
|
|
108
|
+
3. Connect the phone over USB and run the DHU.
|
|
109
|
+
|
|
110
|
+
See Google's [Test Android Auto apps with DHU](https://developer.android.com/training/cars/testing/dhu) for the full walkthrough. Your app appears under the media apps — browse the tree, exercise voice search, and verify artwork, grid/list styles, and section headers.
|
|
111
|
+
|
|
112
|
+
## Reference
|
|
113
|
+
|
|
114
|
+
See the [example app](https://github.com/radio-garden/react-native-audio-browser/tree/main/apps/example-native/android) for a complete working manifest and descriptor.
|