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.
- package/README.md +10 -0
- package/android/build.gradle +20 -1
- package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +5 -1
- package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +8 -0
- package/android/src/main/java/com/audiobrowser/browser/InertStyleDiagnostic.kt +267 -0
- package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +27 -9
- 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/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 +8 -1
- package/android/src/test/java/com/audiobrowser/TestFixtures.kt +49 -3
- 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 +8 -8
- package/ios/Browser/BrowserManager.swift +106 -45
- package/ios/Browser/InertStyleDiagnostic.swift +279 -0
- package/ios/Browser/JsonModels.swift +81 -10
- 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/UIApplication+ActiveScene.swift +27 -0
- package/ios/Http/HttpClient.swift +8 -2
- package/ios/HybridAudioBrowser.swift +54 -11
- package/ios/Model/NitroTypeStubs.swift +60 -0
- 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/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/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/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/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-nodes.d.ts +104 -2
- package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
- 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__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 +12 -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 +6 -0
- 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 +6 -0
- 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/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/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.hpp +54 -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/GridTile.swift +44 -0
- package/nitrogen/generated/ios/swift/ImageShape.swift +40 -0
- 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++/SectionStyle.hpp +32 -2
- package/nitrogen/generated/shared/c++/TrackStyle.hpp +25 -2
- package/package.json +7 -2
- 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-nodes.ts +112 -2
- 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
|
@@ -120,6 +120,41 @@ struct NextTests {
|
|
|
120
120
|
#expect(q.shuffleOrder.shuffled == order)
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
/// A queue reorder permutes positions, not identities, so it must leave the
|
|
124
|
+
/// shuffled playback order alone. Re-inserting at a random shuffle position
|
|
125
|
+
/// would silently reshuffle under the listener.
|
|
126
|
+
@Test func shuffle_moveKeepsThePlaybackOrder() {
|
|
127
|
+
for target in 0 ..< 5 {
|
|
128
|
+
let q = QueueManager()
|
|
129
|
+
q.setQueue(tracks("a", "b", "c", "d", "e"))
|
|
130
|
+
q.shuffleEnabled = true
|
|
131
|
+
let before = q.shuffleOrder.shuffled.map { q.tracks[$0].id }
|
|
132
|
+
|
|
133
|
+
_ = try? q.move(fromIndex: 0, toIndex: target)
|
|
134
|
+
|
|
135
|
+
#expect(q.shuffleOrder.shuffled.map { q.tracks[$0].id } == before)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/// The same hazard as `shuffle_startTrackLeadsTheOrder`, reached by moving the
|
|
140
|
+
/// *playing* track: a random re-insert can drop it last in the shuffle order,
|
|
141
|
+
/// which reads as `isLastInPlaybackOrder` and ends the queue at the next track
|
|
142
|
+
/// boundary with tracks still unplayed.
|
|
143
|
+
@Test func shuffle_movingCurrentTrackDoesNotEndTheQueue() {
|
|
144
|
+
for target in 1 ..< 6 {
|
|
145
|
+
let q = QueueManager()
|
|
146
|
+
q.setQueue(tracks("a", "b", "c", "d", "e", "f"), initialIndex: 0)
|
|
147
|
+
q.shuffleEnabled = true // pins "a" first in the order
|
|
148
|
+
#expect(!q.isLastInPlaybackOrder)
|
|
149
|
+
|
|
150
|
+
_ = try? q.move(fromIndex: 0, toIndex: target)
|
|
151
|
+
|
|
152
|
+
#expect(q.currentTrack?.id == "a")
|
|
153
|
+
#expect(!q.isLastInPlaybackOrder)
|
|
154
|
+
#expect(q.nextTracks.count == 5)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
123
158
|
@Test func shuffle_visitsAllTracks() {
|
|
124
159
|
let q = QueueManager()
|
|
125
160
|
q.setQueue(tracks("a", "b", "c", "d"))
|
|
@@ -382,6 +417,25 @@ struct AddTests {
|
|
|
382
417
|
#expect(q.tracks[2].id == "c")
|
|
383
418
|
#expect(q.tracks[3].id == "d")
|
|
384
419
|
}
|
|
420
|
+
|
|
421
|
+
/// Filling an empty queue must pin the start track to the head of the shuffle
|
|
422
|
+
/// order, exactly as `setQueue` does — `insert(at:count:)` places new indices
|
|
423
|
+
/// at random positions, so without the pin the start track can land last and
|
|
424
|
+
/// the queue reports end-of-queue as soon as it finishes. `shuffleEnabled`'s
|
|
425
|
+
/// didSet only re-pins on the off→on edge, so shuffle-already-on gets nothing
|
|
426
|
+
/// from it.
|
|
427
|
+
@Test func toEmptyQueue_withShuffleAlreadyOn_startTrackLeadsTheOrder() {
|
|
428
|
+
for start in 0 ..< 6 {
|
|
429
|
+
let q = QueueManager()
|
|
430
|
+
q.shuffleEnabled = true
|
|
431
|
+
q.add(tracks("a", "b", "c", "d", "e", "f"), initialIndex: start)
|
|
432
|
+
|
|
433
|
+
#expect(q.shuffleOrder.isFirst(start))
|
|
434
|
+
#expect(!q.isLastInPlaybackOrder)
|
|
435
|
+
#expect(q.nextTracks.count == 5)
|
|
436
|
+
#expect(q.previousTracks.isEmpty)
|
|
437
|
+
}
|
|
438
|
+
}
|
|
385
439
|
}
|
|
386
440
|
|
|
387
441
|
// MARK: - Mutations: addAt()
|
|
@@ -414,6 +468,18 @@ struct AddAtTests {
|
|
|
414
468
|
#expect(q.currentIndex == 0)
|
|
415
469
|
}
|
|
416
470
|
|
|
471
|
+
/// Same pinning requirement as `add()` into an empty queue — see its twin.
|
|
472
|
+
@Test func toEmptyQueue_withShuffleAlreadyOn_startTrackLeadsTheOrder() throws {
|
|
473
|
+
let q = QueueManager()
|
|
474
|
+
q.shuffleEnabled = true
|
|
475
|
+
try q.addAt(tracks("a", "b", "c", "d", "e", "f"), at: 0)
|
|
476
|
+
|
|
477
|
+
#expect(q.shuffleOrder.isFirst(0))
|
|
478
|
+
#expect(!q.isLastInPlaybackOrder)
|
|
479
|
+
#expect(q.nextTracks.count == 5)
|
|
480
|
+
#expect(q.previousTracks.isEmpty)
|
|
481
|
+
}
|
|
482
|
+
|
|
417
483
|
@Test func invalidIndex_throws() {
|
|
418
484
|
let q = QueueManager()
|
|
419
485
|
q.setQueue(tracks("a"))
|
|
@@ -691,10 +757,10 @@ struct ClearTests {
|
|
|
691
757
|
@Suite("replace & removeUpcomingTracks")
|
|
692
758
|
@MainActor
|
|
693
759
|
struct OtherMutationTests {
|
|
694
|
-
@Test func replace_updatesTrackAtIndex() {
|
|
760
|
+
@Test func replace_updatesTrackAtIndex() throws {
|
|
695
761
|
let q = QueueManager()
|
|
696
762
|
q.setQueue(tracks("a", "b", "c"))
|
|
697
|
-
q.replace(1, Track(id: "B"))
|
|
763
|
+
try q.replace(1, Track(id: "B"))
|
|
698
764
|
#expect(q.tracks[1].id == "B")
|
|
699
765
|
}
|
|
700
766
|
|
|
@@ -738,10 +804,10 @@ struct OtherMutationTests {
|
|
|
738
804
|
#expect(q.tracks[0].id == "a")
|
|
739
805
|
}
|
|
740
806
|
|
|
741
|
-
@Test func replace_currentTrack_reflectsInCurrentTrack() {
|
|
807
|
+
@Test func replace_currentTrack_reflectsInCurrentTrack() throws {
|
|
742
808
|
let q = QueueManager()
|
|
743
809
|
q.setQueue(tracks("a", "b", "c"), initialIndex: 1)
|
|
744
|
-
q.replace(1, Track(id: "B"))
|
|
810
|
+
try q.replace(1, Track(id: "B"))
|
|
745
811
|
#expect(q.currentTrack?.id == "B")
|
|
746
812
|
#expect(q.currentIndex == 1)
|
|
747
813
|
}
|
|
@@ -926,12 +992,12 @@ struct DelegateTests {
|
|
|
926
992
|
#expect(spy.calls.count == 1)
|
|
927
993
|
}
|
|
928
994
|
|
|
929
|
-
@Test func queueDidChangeTracks_firesOnReplace() {
|
|
995
|
+
@Test func queueDidChangeTracks_firesOnReplace() throws {
|
|
930
996
|
let q = QueueManager()
|
|
931
997
|
q.setQueue(tracks("a", "b"))
|
|
932
998
|
let spy = DelegateSpy()
|
|
933
999
|
q.delegate = spy
|
|
934
|
-
q.replace(0, Track(id: "A"))
|
|
1000
|
+
try q.replace(0, Track(id: "A"))
|
|
935
1001
|
#expect(spy.calls.count == 1)
|
|
936
1002
|
}
|
|
937
1003
|
|
|
@@ -136,6 +136,22 @@ struct RemoteCommandControllerTests {
|
|
|
136
136
|
#expect(center.previousTrackCommand.isEnabled)
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
/// The handler properties must not capture the controller strongly. Assigning a
|
|
140
|
+
/// bare method reference (`= handlePlayCommandDefault`) curries to
|
|
141
|
+
/// `{ [self] in self.method($0) }`, and storing that back on self is a permanent
|
|
142
|
+
/// cycle — one leaked controller per player instance, across every JS reload.
|
|
143
|
+
@Test func doesNotRetainItselfThroughHandlers() {
|
|
144
|
+
weak var weakController: RemoteCommandController?
|
|
145
|
+
autoreleasepool {
|
|
146
|
+
let (controller, _) = makeController()
|
|
147
|
+
// Forces every lazy handler to initialize, which is what closes the cycle.
|
|
148
|
+
controller.enable(commands: RemoteCommand.all())
|
|
149
|
+
controller.disableAll()
|
|
150
|
+
weakController = controller
|
|
151
|
+
}
|
|
152
|
+
#expect(weakController == nil)
|
|
153
|
+
}
|
|
154
|
+
|
|
139
155
|
@Test func shuffleAndRepeatStateSurviveCommandCenterSwitch() {
|
|
140
156
|
let (controller, _) = makeController()
|
|
141
157
|
let center = MPRemoteCommandCenter.shared()
|
|
@@ -58,4 +58,33 @@ struct SectionPresentationTests {
|
|
|
58
58
|
SectionPresentation(for: style(display: nil, gridWrap: false), supportsWrappingGrid: true)
|
|
59
59
|
== .list)
|
|
60
60
|
}
|
|
61
|
+
|
|
62
|
+
@Test func tileFamilyFollowsGridTileWithThePlainSplit() {
|
|
63
|
+
// Card/condensed follow the declaration in either wrap mode; the plain
|
|
64
|
+
// default splits on the wrap: single-line keeps row elements (the
|
|
65
|
+
// subtitle slot), wrapping renders the image grid.
|
|
66
|
+
#expect(
|
|
67
|
+
SectionPresentation.tileFamily(for: SectionStyle(gridTile: .card), singleLine: true)
|
|
68
|
+
== .cardElements)
|
|
69
|
+
#expect(
|
|
70
|
+
SectionPresentation.tileFamily(for: SectionStyle(gridTile: .condensed), singleLine: false)
|
|
71
|
+
== .condensedElements)
|
|
72
|
+
#expect(SectionPresentation.tileFamily(for: nil, singleLine: true) == .rowElements)
|
|
73
|
+
#expect(SectionPresentation.tileFamily(for: nil, singleLine: false) == .imageGridElements)
|
|
74
|
+
#expect(
|
|
75
|
+
SectionPresentation.tileFamily(for: SectionStyle(gridTile: .plain), singleLine: true)
|
|
76
|
+
== .rowElements)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@Test func effectiveAccessorySymbol_noneAndAbsenceDrawNothing() {
|
|
80
|
+
// A declared symbol draws; 'none' (the inheritance escape) and absence
|
|
81
|
+
// both fall back to the derived accessory.
|
|
82
|
+
#expect(
|
|
83
|
+
SectionPresentation.effectiveAccessorySymbol(TrackStyle(accessorySymbol: "lock.fill"))
|
|
84
|
+
== "lock.fill")
|
|
85
|
+
#expect(
|
|
86
|
+
SectionPresentation.effectiveAccessorySymbol(TrackStyle(accessorySymbol: "none")) == nil)
|
|
87
|
+
#expect(SectionPresentation.effectiveAccessorySymbol(TrackStyle()) == nil)
|
|
88
|
+
#expect(SectionPresentation.effectiveAccessorySymbol(nil) == nil)
|
|
89
|
+
}
|
|
61
90
|
}
|
|
@@ -33,6 +33,21 @@ struct SectionStyleResolutionTests {
|
|
|
33
33
|
#expect(resolved.display == nil)
|
|
34
34
|
#expect(resolved.artworkRendering == nil)
|
|
35
35
|
#expect(resolved.gridWrap == nil)
|
|
36
|
+
#expect(resolved.gridTile == nil)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@Test func gridTileResolvesByScopeOverride() {
|
|
40
|
+
// A page-wide card treatment; one section opts back to plain tiles.
|
|
41
|
+
let inherited = StyleResolver.sectionStyle(
|
|
42
|
+
section: nil,
|
|
43
|
+
page: SectionStyle(gridTile: .card),
|
|
44
|
+
)
|
|
45
|
+
#expect(inherited.gridTile == .card)
|
|
46
|
+
let overridden = StyleResolver.sectionStyle(
|
|
47
|
+
section: SectionStyle(gridTile: .plain),
|
|
48
|
+
page: SectionStyle(gridTile: .card),
|
|
49
|
+
)
|
|
50
|
+
#expect(overridden.gridTile == .plain)
|
|
36
51
|
}
|
|
37
52
|
}
|
|
38
53
|
|
|
@@ -64,4 +79,49 @@ struct TrackStyleResolutionTests {
|
|
|
64
79
|
)
|
|
65
80
|
#expect(resolved.display == nil)
|
|
66
81
|
}
|
|
82
|
+
|
|
83
|
+
@Test func imageShapeInheritsAndOverridesPerItem() {
|
|
84
|
+
// Section-wide circular (an artists shelf); one album overrides.
|
|
85
|
+
let inherited = StyleResolver.trackStyle(
|
|
86
|
+
track: nil,
|
|
87
|
+
section: SectionStyle(imageShape: .circular),
|
|
88
|
+
)
|
|
89
|
+
#expect(inherited.imageShape == .circular)
|
|
90
|
+
let overridden = StyleResolver.trackStyle(
|
|
91
|
+
track: TrackStyle(imageShape: .roundedRectangle),
|
|
92
|
+
section: SectionStyle(imageShape: .circular),
|
|
93
|
+
)
|
|
94
|
+
#expect(overridden.imageShape == .roundedRectangle)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@Test func accessorySymbolInheritsAndNoneResolvesAsAValue() {
|
|
98
|
+
let inherited = StyleResolver.trackStyle(
|
|
99
|
+
track: nil,
|
|
100
|
+
section: SectionStyle(accessorySymbol: "lock.fill"),
|
|
101
|
+
)
|
|
102
|
+
#expect(inherited.accessorySymbol == "lock.fill")
|
|
103
|
+
// 'none' is the inheritance escape — it must survive resolution intact
|
|
104
|
+
// (the renderer, not the resolver, maps it to "no accessory").
|
|
105
|
+
let escaped = StyleResolver.trackStyle(
|
|
106
|
+
track: TrackStyle(accessorySymbol: "none"),
|
|
107
|
+
section: SectionStyle(accessorySymbol: "lock.fill"),
|
|
108
|
+
)
|
|
109
|
+
#expect(escaped.accessorySymbol == "none")
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@Test func cardPropertiesInheritAndOverridePerItem() {
|
|
113
|
+
// Section-wide tint/mode; one card overrides its image mode.
|
|
114
|
+
let inherited = StyleResolver.trackStyle(
|
|
115
|
+
track: nil,
|
|
116
|
+
section: SectionStyle(accessorySymbol: nil, cardTint: "#1e3a8a", cardImage: .background),
|
|
117
|
+
)
|
|
118
|
+
#expect(inherited.cardTint == "#1e3a8a")
|
|
119
|
+
#expect(inherited.cardImage == .background)
|
|
120
|
+
let overridden = StyleResolver.trackStyle(
|
|
121
|
+
track: TrackStyle(cardImage: .normal),
|
|
122
|
+
section: SectionStyle(cardTint: "#1e3a8a", cardImage: .background),
|
|
123
|
+
)
|
|
124
|
+
#expect(overridden.cardImage == .normal)
|
|
125
|
+
#expect(overridden.cardTint == "#1e3a8a")
|
|
126
|
+
}
|
|
67
127
|
}
|
|
@@ -185,6 +185,7 @@ final class MockSleepTimerHandling: SleepTimerHandling {
|
|
|
185
185
|
var onComplete: (() -> Void)?
|
|
186
186
|
var onFadeStart: ((_ duration: TimeInterval) -> Void)?
|
|
187
187
|
var onFadeCancel: (() -> Void)?
|
|
188
|
+
var onChanged: ((SleepTimerState) -> Void)?
|
|
188
189
|
var trackChangedCallCount = 0
|
|
189
190
|
var trackPlayedToEndCallCount = 0
|
|
190
191
|
var clearCallCount = 0
|
|
@@ -196,6 +197,12 @@ final class MockSleepTimerHandling: SleepTimerHandling {
|
|
|
196
197
|
|
|
197
198
|
func onTrackChanged() { trackChangedCallCount += 1 }
|
|
198
199
|
func onTrackPlayedToEnd() { trackPlayedToEndCallCount += 1 }
|
|
200
|
+
|
|
201
|
+
// Consumer-facing surface — unused by the coordinator tests, present only to
|
|
202
|
+
// satisfy the protocol. Add recorders alongside the tests that need them.
|
|
203
|
+
func get() -> SleepTimerState? { nil }
|
|
204
|
+
func set(seconds _: TimeInterval, fadeDuration _: TimeInterval?) {}
|
|
205
|
+
func setToEndOfTrack() {}
|
|
199
206
|
}
|
|
200
207
|
|
|
201
208
|
// MARK: - Mock RetryHandling
|
package/ios/TrackPlayer.swift
CHANGED
|
@@ -136,7 +136,7 @@ class TrackPlayer {
|
|
|
136
136
|
var previousTracks: [Track] { coordinator.previousTracks }
|
|
137
137
|
var isLastInPlaybackOrder: Bool { coordinator.isLastInPlaybackOrder }
|
|
138
138
|
var playbackActive: Bool { coordinator.playbackActive }
|
|
139
|
-
var sleepTimerManager:
|
|
139
|
+
var sleepTimerManager: any SleepTimerHandling { coordinator.sleepTimerManager }
|
|
140
140
|
var queue: QueueManager { coordinator.queue }
|
|
141
141
|
var loadSeekCoordinator: LoadSeekCoordinator { coordinator.loadSeekCoordinator }
|
|
142
142
|
var playingStateManager: PlayingStateManager { coordinator.playingStateManager }
|
|
@@ -195,12 +195,14 @@ class TrackPlayer {
|
|
|
195
195
|
return seconds.isNaN ? 0 : seconds
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
/// The current item's duration, or 0 when it has none (a live stream).
|
|
199
|
+
///
|
|
200
|
+
/// Don't reintroduce `item.asset.duration`: it is deprecated as of the iOS 16
|
|
201
|
+
/// minimum and blocks the main actor fetching a remote asset's duration. Read
|
|
202
|
+
/// on every progress tick.
|
|
198
203
|
var duration: Double {
|
|
199
204
|
guard let item = avPlayer.currentItem else { return 0.0 }
|
|
200
205
|
|
|
201
|
-
if !item.asset.duration.seconds.isNaN {
|
|
202
|
-
return item.asset.duration.seconds
|
|
203
|
-
}
|
|
204
206
|
if !item.duration.seconds.isNaN {
|
|
205
207
|
return item.duration.seconds
|
|
206
208
|
}
|
|
@@ -252,8 +254,22 @@ class TrackPlayer {
|
|
|
252
254
|
var rate: Float {
|
|
253
255
|
get { coordinator.rate }
|
|
254
256
|
set {
|
|
257
|
+
// A non-positive rate leaves the player unable to play at all — and would
|
|
258
|
+
// poison the next play() through `defaultRate` below. Rejected here, at
|
|
259
|
+
// the one setter every source funnels through (JS, remote command, CarPlay
|
|
260
|
+
// rate cycling, a restored snapshot), rather than at each of them.
|
|
261
|
+
guard newValue > 0 else { return }
|
|
255
262
|
coordinator.rate = newValue
|
|
256
|
-
|
|
263
|
+
// Assigning a nonzero `AVPlayer.rate` IS `play()` — so setting the speed
|
|
264
|
+
// while paused would start audible playback behind the user's back, with
|
|
265
|
+
// `playWhenReady` still false (the lock screen would read "paused" over
|
|
266
|
+
// sound). On Android and web the speed control is independent of the play
|
|
267
|
+
// intent; this keeps iOS the same.
|
|
268
|
+
avPlayer.defaultRate = newValue
|
|
269
|
+
if playWhenReady { avPlayer.rate = newValue }
|
|
270
|
+
// Same reason as the repeat/shuffle edges below — a speed changed while
|
|
271
|
+
// paused would otherwise not reach the snapshot the cold resume restores.
|
|
272
|
+
persistPlaybackState()
|
|
257
273
|
}
|
|
258
274
|
}
|
|
259
275
|
|
|
@@ -312,17 +328,6 @@ class TrackPlayer {
|
|
|
312
328
|
coordinator.load(track, playWhenReady: playWhenReady)
|
|
313
329
|
}
|
|
314
330
|
|
|
315
|
-
func togglePlaying() {
|
|
316
|
-
switch avPlayer.timeControlStatus {
|
|
317
|
-
case .playing, .waitingToPlayAtSpecifiedRate:
|
|
318
|
-
pause()
|
|
319
|
-
case .paused:
|
|
320
|
-
play()
|
|
321
|
-
@unknown default:
|
|
322
|
-
fatalError("Unknown AVPlayer.timeControlStatus")
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
331
|
func play() { coordinator.play() }
|
|
327
332
|
func pause() { coordinator.pause() }
|
|
328
333
|
func togglePlayback() { coordinator.togglePlayback() }
|
|
@@ -456,13 +461,17 @@ class TrackPlayer {
|
|
|
456
461
|
)
|
|
457
462
|
}
|
|
458
463
|
|
|
464
|
+
/// Empties the queue and the published now-playing info. Unlinking the
|
|
465
|
+
/// session belongs in `destroy()`: `avPlayer` survives a clear, and only
|
|
466
|
+
/// `setupAVPlayer()` re-links.
|
|
459
467
|
func clear() {
|
|
460
468
|
coordinator.clear()
|
|
461
|
-
nowPlayingInfoController.unlinkPlayer()
|
|
462
469
|
}
|
|
463
470
|
|
|
464
471
|
func destroy() {
|
|
465
472
|
clear()
|
|
473
|
+
// The player is going away for good — now the session must go with it.
|
|
474
|
+
nowPlayingInfoController.unlinkPlayer()
|
|
466
475
|
// An AVPlayer retains its time observers: one left registered is still attached when the
|
|
467
476
|
// player is released, and outlives this object.
|
|
468
477
|
playerTimeObserver.unregisterForBoundaryTimeEvents()
|
|
@@ -550,10 +559,12 @@ class TrackPlayer {
|
|
|
550
559
|
|
|
551
560
|
nowPlayingInfoController.linkPlayer(avPlayer)
|
|
552
561
|
|
|
562
|
+
// Stage the rate unconditionally, so a freshly recreated player carries it
|
|
563
|
+
// before anything plays — `rate`'s setter holds the same invariant, and on
|
|
564
|
+
// the playWhenReady branch below `defaultRate` was previously left at 1.0.
|
|
565
|
+
avPlayer.defaultRate = rate
|
|
553
566
|
if playWhenReady {
|
|
554
567
|
startPlayback()
|
|
555
|
-
} else {
|
|
556
|
-
avPlayer.defaultRate = rate
|
|
557
568
|
}
|
|
558
569
|
}
|
|
559
570
|
|
|
@@ -755,13 +766,28 @@ extension TrackPlayer {
|
|
|
755
766
|
PersistedPlaybackState(
|
|
756
767
|
track: JsonTrack(from: track),
|
|
757
768
|
positionMs: positionMs,
|
|
758
|
-
repeatMode: repeatMode.
|
|
769
|
+
repeatMode: repeatMode.stringValue,
|
|
759
770
|
shuffleEnabled: shuffleEnabled,
|
|
760
771
|
playbackSpeed: rate,
|
|
761
772
|
),
|
|
762
773
|
)
|
|
763
774
|
}
|
|
764
775
|
|
|
776
|
+
/// Applies the player settings a snapshot carries — the inverse of
|
|
777
|
+
/// `persistPlaybackState()`, and the twin of the Kotlin store's `restore()`.
|
|
778
|
+
///
|
|
779
|
+
/// Call BEFORE building the queue: `setQueue` rebuilds the shuffle order
|
|
780
|
+
/// pinned to the starting track, so shuffle has to be set for that order to
|
|
781
|
+
/// be the one playback uses (and the mode events then precede the
|
|
782
|
+
/// queue-changed event, as on Android). An unrecognized repeat mode restores
|
|
783
|
+
/// as `.off` rather than failing the whole resume over one field; a
|
|
784
|
+
/// non-positive speed is rejected by `rate`'s own setter.
|
|
785
|
+
func restorePlaybackSettings(from state: PersistedPlaybackState) {
|
|
786
|
+
repeatMode = RepeatMode(fromString: state.repeatMode) ?? .off
|
|
787
|
+
shuffleEnabled = state.shuffleEnabled
|
|
788
|
+
rate = state.playbackSpeed
|
|
789
|
+
}
|
|
790
|
+
|
|
765
791
|
/// Start a 5 s repeating save while playback is active. A previous task is
|
|
766
792
|
/// cancelled first so there is never more than one running at a time.
|
|
767
793
|
private func startPeriodicSave() {
|
|
@@ -781,17 +807,6 @@ extension TrackPlayer {
|
|
|
781
807
|
}
|
|
782
808
|
}
|
|
783
809
|
|
|
784
|
-
private extension RepeatMode {
|
|
785
|
-
/// String representation stored in `PersistedPlaybackState.repeatMode`.
|
|
786
|
-
var persistedString: String {
|
|
787
|
-
switch self {
|
|
788
|
-
case .off: "off"
|
|
789
|
-
case .track: "track"
|
|
790
|
-
case .queue: "queue"
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
|
|
795
810
|
// MARK: - PlaybackEffectHandler
|
|
796
811
|
|
|
797
812
|
extension TrackPlayer: PlaybackEffectHandler {
|
|
@@ -857,15 +872,23 @@ extension TrackPlayer: PlaybackEffectHandler {
|
|
|
857
872
|
}
|
|
858
873
|
|
|
859
874
|
func clearNowPlaying() {
|
|
860
|
-
|
|
875
|
+
nowPlayingUpdater.clearNowPlaying()
|
|
861
876
|
}
|
|
862
877
|
|
|
878
|
+
// Both persist: a snapshot is otherwise only written on track load, on pause,
|
|
879
|
+
// and every 5s while playing, so a mode toggled while paused was lost — the
|
|
880
|
+
// cold-start resume would restore the value from before the toggle. Android
|
|
881
|
+
// persists on every setting change (`PlayerListener.onRepeatModeChanged` /
|
|
882
|
+
// `onShuffleModeEnabledChanged`); these are the same two edges, and the
|
|
883
|
+
// coordinator only calls them on a real change.
|
|
863
884
|
func updateRemoteRepeatMode(_ mode: RepeatMode) {
|
|
864
885
|
remoteCommandController.updateRepeatMode(mode)
|
|
886
|
+
persistPlaybackState()
|
|
865
887
|
}
|
|
866
888
|
|
|
867
889
|
func updateRemoteShuffleMode(_ enabled: Bool) {
|
|
868
890
|
remoteCommandController.updateShuffleMode(enabled)
|
|
891
|
+
persistPlaybackState()
|
|
869
892
|
}
|
|
870
893
|
|
|
871
894
|
func updateSkipAvailability(canNext: Bool, canPrevious: Bool) {
|
|
@@ -884,8 +907,8 @@ extension TrackPlayer: TrackSelectionPlayer {}
|
|
|
884
907
|
// MARK: - Queue Methods (thin forwarders)
|
|
885
908
|
|
|
886
909
|
extension TrackPlayer {
|
|
887
|
-
func replace(_ index: Int, _ track: Track) {
|
|
888
|
-
coordinator.replace(index, track)
|
|
910
|
+
func replace(_ index: Int, _ track: Track) throws {
|
|
911
|
+
try coordinator.replace(index, track)
|
|
889
912
|
}
|
|
890
913
|
|
|
891
914
|
func setQueue(
|
|
@@ -11,13 +11,36 @@ final class NetworkMonitor: @unchecked Sendable {
|
|
|
11
11
|
private let monitor: NWPathMonitor
|
|
12
12
|
private let queue = DispatchQueue(label: "com.audiobrowser.networkmonitor")
|
|
13
13
|
|
|
14
|
-
///
|
|
15
|
-
private
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
/// Guards `_isOnline` only. Never held across `onChanged` — see `apply`.
|
|
15
|
+
private let lock = NSLock()
|
|
16
|
+
private var _isOnline = false
|
|
17
|
+
|
|
18
|
+
/// Current network connectivity state. Readable from any thread: the value is
|
|
19
|
+
/// lock-protected rather than confined, so callers on the JS thread (Nitro's
|
|
20
|
+
/// `getOnline`) don't have to block on a main-queue hop to read a Bool.
|
|
21
|
+
///
|
|
22
|
+
/// The broadcast is main-confined even though the value isn't — `onChanged`
|
|
23
|
+
/// fans out to the JS bridge, a now-playing re-render and a player reload, so
|
|
24
|
+
/// it must not run under the lock (reentrancy) or off the main actor.
|
|
25
|
+
var isOnline: Bool {
|
|
26
|
+
lock.lock()
|
|
27
|
+
defer { lock.unlock() }
|
|
28
|
+
return _isOnline
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// Stores a new status and, if it actually changed, broadcasts on main —
|
|
32
|
+
/// outside the lock.
|
|
33
|
+
private func apply(_ newStatus: Bool) {
|
|
34
|
+
lock.lock()
|
|
35
|
+
let changed = _isOnline != newStatus
|
|
36
|
+
let oldValue = _isOnline
|
|
37
|
+
_isOnline = newStatus
|
|
38
|
+
lock.unlock()
|
|
39
|
+
|
|
40
|
+
guard changed else { return }
|
|
41
|
+
logger.notice("Network status changed: \(oldValue) -> \(newStatus)")
|
|
42
|
+
DispatchQueue.main.async { [weak self] in
|
|
43
|
+
self?.onChanged?(newStatus)
|
|
21
44
|
}
|
|
22
45
|
}
|
|
23
46
|
|
|
@@ -31,29 +54,23 @@ final class NetworkMonitor: @unchecked Sendable {
|
|
|
31
54
|
init() {
|
|
32
55
|
monitor = NWPathMonitor()
|
|
33
56
|
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
// Start monitoring
|
|
43
|
-
monitor.start(queue: queue)
|
|
44
|
-
|
|
45
|
-
// Read initial state after starting (currentPath is now valid)
|
|
57
|
+
// Seed BEFORE `start`. `start` publishes `self` to the monitor queue, so any
|
|
58
|
+
// write after it races the handler's locked write — and would clobber a real
|
|
59
|
+
// update with a `currentPath` the monitor hasn't finished evaluating. Nothing
|
|
60
|
+
// recovers from that: the handler has already fired, `onChanged` isn't wired
|
|
61
|
+
// until setupPlayer, and the stale value then drives `getOnline()`, the JS
|
|
62
|
+
// event, and RetryManager's offline parking until the next genuine path
|
|
63
|
+
// change. Seeding first is a best-effort guess; the handler fires shortly
|
|
64
|
+
// after `start` and is the authoritative last writer.
|
|
46
65
|
let initialStatus = monitor.currentPath.status == .satisfied
|
|
47
66
|
logger.notice("NetworkMonitor initialized, initial isOnline=\(initialStatus)")
|
|
48
|
-
|
|
49
|
-
}
|
|
67
|
+
_isOnline = initialStatus
|
|
50
68
|
|
|
51
|
-
|
|
69
|
+
monitor.pathUpdateHandler = { [weak self] path in
|
|
70
|
+
self?.apply(path.status == .satisfied)
|
|
71
|
+
}
|
|
52
72
|
|
|
53
|
-
|
|
54
|
-
/// - Returns: true if device is online, false otherwise
|
|
55
|
-
func getOnline() -> Bool {
|
|
56
|
-
isOnline
|
|
73
|
+
monitor.start(queue: queue)
|
|
57
74
|
}
|
|
58
75
|
|
|
59
76
|
/// Stops monitoring and cleans up resources.
|
|
@@ -11,13 +11,21 @@ final class SFSymbolRenderer: @unchecked Sendable {
|
|
|
11
11
|
@MainActor static let shared = SFSymbolRenderer()
|
|
12
12
|
static let defaultCanvasSize = CGSize(width: 200, height: 200)
|
|
13
13
|
|
|
14
|
+
/// Bumped whenever the rendering below changes (symbol point-size factor,
|
|
15
|
+
/// centering, tinting, background fill). It is part of the cache filename
|
|
16
|
+
/// because `.cachesDirectory` survives app updates and the existence check is
|
|
17
|
+
/// the only cache validation — without a version key, every device that had
|
|
18
|
+
/// rendered a symbol once would keep serving the OLD pixels forever after a
|
|
19
|
+
/// render change.
|
|
20
|
+
private static let renderVersion = 1
|
|
21
|
+
|
|
14
22
|
private let logger = Logger(subsystem: "com.audiobrowser", category: "SFSymbolRenderer")
|
|
15
23
|
private let scale: CGFloat
|
|
16
24
|
private let cacheDirectory: URL
|
|
17
25
|
|
|
18
26
|
@MainActor
|
|
19
27
|
private init() {
|
|
20
|
-
scale =
|
|
28
|
+
scale = appDisplayScale
|
|
21
29
|
|
|
22
30
|
let caches = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
|
23
31
|
cacheDirectory = caches.appendingPathComponent("sf-symbols", isDirectory: true)
|
|
@@ -45,7 +53,8 @@ final class SFSymbolRenderer: @unchecked Sendable {
|
|
|
45
53
|
let safeArtwork = artworkValue.unicodeScalars
|
|
46
54
|
.filter { CharacterSet.alphanumerics.contains($0) || $0 == "_" }
|
|
47
55
|
.map(String.init).joined()
|
|
48
|
-
let fileName =
|
|
56
|
+
let fileName =
|
|
57
|
+
"\(safeArtwork)_\(Int(canvasSize.width))x\(Int(canvasSize.height))_\(Int(scale))x_v\(Self.renderVersion)"
|
|
49
58
|
let fileURL = cacheDirectory.appendingPathComponent("\(fileName).png")
|
|
50
59
|
|
|
51
60
|
if FileManager.default.fileExists(atPath: fileURL.path) {
|
|
@@ -139,4 +148,17 @@ extension UIColor {
|
|
|
139
148
|
self.init(white: 0, alpha: 1)
|
|
140
149
|
}
|
|
141
150
|
}
|
|
151
|
+
|
|
152
|
+
/// Failable form for *declared* colors (`style.cardTint`): anything but a
|
|
153
|
+
/// 3- or 6-digit hex value is nil, so an unparseable declaration degrades
|
|
154
|
+
/// to "no declaration" (the system default) instead of rendering black —
|
|
155
|
+
/// matching how unknown enum values decode. `init(hex:)` keeps its lenient
|
|
156
|
+
/// black fallback for the `sf:` artwork params it has always served.
|
|
157
|
+
convenience init?(declaredHex hex: String) {
|
|
158
|
+
let digits = hex.trimmingCharacters(in: CharacterSet(charactersIn: "#"))
|
|
159
|
+
guard [3, 6].contains(digits.count),
|
|
160
|
+
digits.allSatisfy(\.isHexDigit)
|
|
161
|
+
else { return nil }
|
|
162
|
+
self.init(hex: hex)
|
|
163
|
+
}
|
|
142
164
|
}
|
|
@@ -4,7 +4,14 @@ import UIKit
|
|
|
4
4
|
|
|
5
5
|
/// Kingfisher processor that renders SVG data to UIImage using SwiftDraw.
|
|
6
6
|
struct SVGProcessor: ImageProcessor {
|
|
7
|
-
|
|
7
|
+
/// Kingfisher keys its cache on `identifier` alone (`ImageCache.computedKey`),
|
|
8
|
+
/// and `.scaleFactor` never enters that key — so the render parameters have to,
|
|
9
|
+
/// or two different rasterizations of the same URL would collide. Today only
|
|
10
|
+
/// CarPlay rasterizes SVGs (one screen, one scale), so this is insurance
|
|
11
|
+
/// against a second call site rather than a live fix.
|
|
12
|
+
var identifier: String {
|
|
13
|
+
"com.audiobrowser.svgprocessor(\(size?.width ?? -1)x\(size?.height ?? -1)@\(scale))"
|
|
14
|
+
}
|
|
8
15
|
|
|
9
16
|
/// Target size for rendering (in points). If nil, uses SVG's intrinsic size.
|
|
10
17
|
let size: CGSize?
|
|
@@ -15,7 +22,7 @@ struct SVGProcessor: ImageProcessor {
|
|
|
15
22
|
@MainActor
|
|
16
23
|
init(size: CGSize? = nil, scale: CGFloat? = nil) {
|
|
17
24
|
self.size = size
|
|
18
|
-
self.scale = scale ??
|
|
25
|
+
self.scale = scale ?? appDisplayScale
|
|
19
26
|
}
|
|
20
27
|
|
|
21
28
|
func process(item: ImageProcessItem, options _: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
|
|
@@ -101,7 +101,8 @@ function usePolledProgress(updateInterval = 1000) {
|
|
|
101
101
|
poll();
|
|
102
102
|
};
|
|
103
103
|
const stop = () => {
|
|
104
|
-
clearTimeout(
|
|
104
|
+
// Guarded: React Native types clearTimeout(handle: number) — no undefined.
|
|
105
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
105
106
|
timer = undefined;
|
|
106
107
|
unsubscribeState?.();
|
|
107
108
|
unsubscribeState = undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["_react","require","_reactNative","_native","_NativeUpdatedValue","_useNativeUpdatedValue","_state","getProgress","nativeBrowser","onProgressUpdated","exports","NativeUpdatedValue","emitterize","cb","onPlaybackProgressUpdated","useProgress","useNativeUpdatedValue","usePolledProgress","updateInterval","state","setState","useState","position","duration","buffered","useEffect","timer","unsubscribeState","update","currentState","poll","setTimeout","start","undefined","onPlaybackChanged","addListener","stop","clearTimeout","appStateSub","AppState","addEventListener","next","remove"],"sourceRoot":"../../../../src","sources":["features/playback/progress.ts"],"mappings":";;;;;;;;;AAAA,IAAAA,MAAA,GAAAC,OAAA;AACA,IAAAC,YAAA,GAAAD,OAAA;AACA,IAAAE,OAAA,GAAAF,OAAA;AACA,IAAAG,mBAAA,GAAAH,OAAA;AACA,IAAAI,sBAAA,GAAAJ,OAAA;AACA,IAAAK,MAAA,GAAAL,OAAA;AAEA;;AAiBA;AACA;AACA;;AAMA;;AAEA;AACA;AACA;AACA;AACA;AACO,SAASM,WAAWA,CAAA,EAAa;EACtC,OAAOC,qBAAa,CAACD,WAAW,CAAC,CAAC;AACpC;;AAEA;;AAEA;AACA;AACA;AACA;AACA;AACO,MAAME,iBAAiB,GAAAC,OAAA,CAAAD,iBAAA,GAC5BE,sCAAkB,CAACC,UAAU,CAC1BC,EAAE,IAAML,qBAAa,CAACM,yBAAyB,GAAGD,EACrD,CAAC;;AAEH;;AAEA;AACA;AACA;AACA;AACA;AACA;AACO,SAASE,WAAWA,CAAA,EAAa;EACtC,OAAO,IAAAC,4CAAqB,EAACT,WAAW,EAAEE,iBAAiB,CAAC;AAC9D;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASQ,iBAAiBA,CAACC,cAAc,GAAG,IAAI,EAAY;EACjE,MAAM,CAACC,KAAK,EAAEC,QAAQ,CAAC,GAAG,IAAAC,eAAQ,EAAW;IAC3CC,QAAQ,EAAE,CAAC;IACXC,QAAQ,EAAE,CAAC;IACXC,QAAQ,EAAE;EACZ,CAAC,CAAC;EAEF,IAAAC,gBAAS,EAAC,MAAM;IACd,IAAIC,KAAgD;IACpD,IAAIC,gBAA0C;IAE9C,MAAMC,MAAM,GAAGA,CAAA,KAAM;MACnB,IAAI;QACF,MAAM;UAAEN,QAAQ;UAAEC,QAAQ;UAAEC;QAAS,CAAC,GAAGjB,WAAW,CAAC,CAAC;QAEtDa,QAAQ,CAAES,YAAY,IACpBP,QAAQ,KAAKO,YAAY,CAACP,QAAQ,IAClCC,QAAQ,KAAKM,YAAY,CAACN,QAAQ,IAClCC,QAAQ,KAAKK,YAAY,CAACL,QAAQ,GAC9BK,YAAY,GACZ;UAAEP,QAAQ;UAAEC,QAAQ;UAAEC;QAAS,CACrC,CAAC;MACH,CAAC,CAAC,MAAM;QACN;MAAA;IAEJ,CAAC;IAED,MAAMM,IAAI,GAAGA,CAAA,KAAM;MACjBF,MAAM,CAAC,CAAC;MACRF,KAAK,GAAGK,UAAU,CAACD,IAAI,EAAEZ,cAAc,CAAC;IAC1C,CAAC;;IAED;IACA;IACA;IACA,MAAMc,KAAK,GAAGA,CAAA,KAAM;MAClB,IAAIN,KAAK,KAAKO,SAAS,EAAE;MACzBN,gBAAgB,GAAGO,wBAAiB,CAACC,WAAW,CAACP,MAAM,CAAC;MACxDE,IAAI,CAAC,CAAC;IACR,CAAC;IAED,MAAMM,IAAI,GAAGA,CAAA,KAAM;
|
|
1
|
+
{"version":3,"names":["_react","require","_reactNative","_native","_NativeUpdatedValue","_useNativeUpdatedValue","_state","getProgress","nativeBrowser","onProgressUpdated","exports","NativeUpdatedValue","emitterize","cb","onPlaybackProgressUpdated","useProgress","useNativeUpdatedValue","usePolledProgress","updateInterval","state","setState","useState","position","duration","buffered","useEffect","timer","unsubscribeState","update","currentState","poll","setTimeout","start","undefined","onPlaybackChanged","addListener","stop","clearTimeout","appStateSub","AppState","addEventListener","next","remove"],"sourceRoot":"../../../../src","sources":["features/playback/progress.ts"],"mappings":"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","ignoreList":[]}
|
|
@@ -20,8 +20,15 @@ function getRate() {
|
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Sets the playback rate.
|
|
23
|
+
*
|
|
24
|
+
* The rate is a speed multiplier, not a transport control — it is independent
|
|
25
|
+
* of play/pause, and setting it while paused never starts playback. Use
|
|
26
|
+
* {@link play} / {@link pause} for that.
|
|
27
|
+
*
|
|
23
28
|
* @param rate - The playback rate to change to, where 0.5 would be half speed,
|
|
24
|
-
* 1 would be regular speed, 2 would be double speed etc.
|
|
29
|
+
* 1 would be regular speed, 2 would be double speed etc. Must be greater than
|
|
30
|
+
* zero; platforms disagree on what a non-positive rate does (iOS ignores it,
|
|
31
|
+
* Android throws, web stalls the element), so treat it as unsupported.
|
|
25
32
|
*/
|
|
26
33
|
function setRate(rate) {
|
|
27
34
|
_native.nativeBrowser.setRate(rate);
|