@rntp/player 5.0.0-beta.2 → 5.0.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/RNTPPlayer.podspec +1 -0
  2. package/android/src/main/java/com/doublesymmetry/trackplayer/TrackPlayerModule.kt +60 -0
  3. package/android/src/main/java/com/doublesymmetry/trackplayer/TrackPlayerPlaybackService.kt +156 -0
  4. package/android/src/main/java/com/doublesymmetry/trackplayer/models/BrowseTree.kt +51 -20
  5. package/android/src/main/java/com/doublesymmetry/trackplayer/models/EmitEventType.kt +11 -0
  6. package/ios/CarPlay/BrowseTreeStore.swift +40 -0
  7. package/ios/CarPlay/RNTPCarPlaySceneDelegate.swift +283 -0
  8. package/ios/TrackPlayer.swift +135 -1
  9. package/ios/TrackPlayerBridge.mm +9 -0
  10. package/ios/models/EmitEvent.swift +10 -0
  11. package/lib/commonjs/NativeTrackPlayer.js.map +1 -1
  12. package/lib/commonjs/audio.js +76 -7
  13. package/lib/commonjs/audio.js.map +1 -1
  14. package/lib/commonjs/events/SleepTimerTriggered.js +2 -0
  15. package/lib/commonjs/events/SleepTimerTriggered.js.map +1 -0
  16. package/lib/commonjs/events/index.js +13 -0
  17. package/lib/commonjs/events/index.js.map +1 -1
  18. package/lib/module/NativeTrackPlayer.js.map +1 -1
  19. package/lib/module/audio.js +72 -7
  20. package/lib/module/audio.js.map +1 -1
  21. package/lib/module/events/SleepTimerTriggered.js +2 -0
  22. package/lib/module/events/SleepTimerTriggered.js.map +1 -0
  23. package/lib/module/events/index.js +2 -0
  24. package/lib/module/events/index.js.map +1 -1
  25. package/lib/typescript/src/NativeTrackPlayer.d.ts +4 -0
  26. package/lib/typescript/src/NativeTrackPlayer.d.ts.map +1 -1
  27. package/lib/typescript/src/audio.d.ts +48 -5
  28. package/lib/typescript/src/audio.d.ts.map +1 -1
  29. package/lib/typescript/src/events/SleepTimerTriggered.d.ts +5 -0
  30. package/lib/typescript/src/events/SleepTimerTriggered.d.ts.map +1 -0
  31. package/lib/typescript/src/events/index.d.ts +5 -1
  32. package/lib/typescript/src/events/index.d.ts.map +1 -1
  33. package/lib/typescript/src/interfaces/BrowseTree.d.ts +35 -5
  34. package/lib/typescript/src/interfaces/BrowseTree.d.ts.map +1 -1
  35. package/package.json +1 -1
  36. package/src/NativeTrackPlayer.ts +6 -0
  37. package/src/audio.ts +84 -8
  38. package/src/events/SleepTimerTriggered.ts +4 -0
  39. package/src/events/index.ts +4 -0
  40. package/src/interfaces/BrowseTree.ts +40 -5
@@ -16,6 +16,7 @@ Pod::Spec.new do |s|
16
16
  s.source_files = "ios/**/*.{h,m,mm,swift}"
17
17
  s.exclude_files = "ios/tests/**", "ios/Package.swift"
18
18
  s.private_header_files = "ios/**/*.h"
19
+ s.frameworks = "CarPlay"
19
20
 
20
21
  install_modules_dependencies(s)
21
22
  end
@@ -502,6 +502,66 @@ class TrackPlayerModule internal constructor(private val context: ReactApplicati
502
502
 
503
503
  // endregion
504
504
 
505
+ // region Sleep Timer
506
+
507
+ @ReactMethod
508
+ override fun sleepAfterTime(seconds: Double, fadeOutSeconds: Double) {
509
+ controller.run { mc ->
510
+ val args = Bundle().apply {
511
+ putDouble("seconds", seconds)
512
+ putDouble("fadeOutSeconds", fadeOutSeconds)
513
+ }
514
+ mc?.sendCustomCommand(
515
+ SessionCommand(TrackPlayerPlaybackService.COMMAND_SLEEP_AFTER_TIME, Bundle.EMPTY),
516
+ args
517
+ )
518
+ }
519
+ }
520
+
521
+ @ReactMethod
522
+ override fun sleepAfterMediaItemAtIndex(index: Double) {
523
+ controller.run { mc ->
524
+ val args = Bundle().apply { putInt("index", index.toInt()) }
525
+ mc?.sendCustomCommand(
526
+ SessionCommand(TrackPlayerPlaybackService.COMMAND_SLEEP_AFTER_MEDIA_ITEM, Bundle.EMPTY),
527
+ args
528
+ )
529
+ }
530
+ }
531
+
532
+ @ReactMethod(isBlockingSynchronousMethod = true)
533
+ override fun getSleepTimer(): WritableMap? {
534
+ val prefs = context.getSharedPreferences(PLAYER_PREFS_NAME, android.content.Context.MODE_PRIVATE)
535
+ val jsonStr = prefs.getString(TrackPlayerPlaybackService.SLEEP_TIMER_STATE_KEY, null) ?: return null
536
+ return try {
537
+ val json = JSONObject(jsonStr)
538
+ val type = json.getString("type")
539
+ val map = Arguments.createMap()
540
+ map.putString("type", type)
541
+ if (type == "time") {
542
+ map.putDouble("remainingSeconds", json.getDouble("remainingSeconds"))
543
+ map.putDouble("fadeOutSeconds", json.getDouble("fadeOutSeconds"))
544
+ } else {
545
+ map.putInt("index", json.getInt("index"))
546
+ }
547
+ map
548
+ } catch (e: Exception) {
549
+ null
550
+ }
551
+ }
552
+
553
+ @ReactMethod
554
+ override fun cancelSleepTimer() {
555
+ controller.run { mc ->
556
+ mc?.sendCustomCommand(
557
+ SessionCommand(TrackPlayerPlaybackService.COMMAND_CANCEL_SLEEP_TIMER, Bundle.EMPTY),
558
+ Bundle.EMPTY
559
+ )
560
+ }
561
+ }
562
+
563
+ // endregion
564
+
505
565
  // region Destroy
506
566
 
507
567
  @ReactMethod
@@ -56,6 +56,7 @@ import com.doublesymmetry.trackplayer.models.WakeMode
56
56
  import android.os.Handler
57
57
  import android.os.Looper
58
58
  import com.doublesymmetry.trackplayer.models.PlaybackProgressUpdatedEvent
59
+ import com.doublesymmetry.trackplayer.models.SleepTimerTriggeredEvent
59
60
  import org.json.JSONObject
60
61
 
61
62
  class TrackPlayerPlaybackService: MediaLibraryService(), MediaLibraryService.MediaLibrarySession.Callback {
@@ -68,6 +69,14 @@ class TrackPlayerPlaybackService: MediaLibraryService(), MediaLibraryService.Med
68
69
  private var progressSyncTimer: java.util.Timer? = null
69
70
  private var progressSyncExecutor: java.util.concurrent.ExecutorService? = null
70
71
 
72
+ private var sleepTimerType: String? = null // "time" or "mediaItem"
73
+ private var sleepTimerRemainingSeconds: Double = 0.0
74
+ private var sleepTimerFadeOutSeconds: Double = 0.0
75
+ private var sleepTimerTargetIndex: Int? = null
76
+ private var sleepTimer: java.util.Timer? = null
77
+ private var sleepTimerPreFadeVolume: Float? = null
78
+ private var sleepTimerPreviousIndex: Int? = null
79
+
71
80
  @OptIn(UnstableApi::class)
72
81
  override fun onCreate() {
73
82
  super.onCreate()
@@ -136,6 +145,26 @@ class TrackPlayerPlaybackService: MediaLibraryService(), MediaLibraryService.Med
136
145
  override fun onIsPlayingChanged(isPlaying: Boolean) {
137
146
  if (isPlaying) startProgressSyncTimer() else stopProgressSyncTimer(fireFinalTick = true)
138
147
  }
148
+
149
+ override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
150
+ val currentIndex = activePlayer?.currentMediaItemIndex ?: 0
151
+ if (sleepTimerType == "mediaItem") {
152
+ val targetIndex = sleepTimerTargetIndex
153
+ if (targetIndex != null && sleepTimerPreviousIndex == targetIndex && currentIndex != targetIndex) {
154
+ this@TrackPlayerPlaybackService.emitEvent(SleepTimerTriggeredEvent("mediaItem"))
155
+ cancelSleepTimerInternal(restoreVolume = false)
156
+ // Defer pause to next run loop — calling during transition gets overridden
157
+ Handler(Looper.getMainLooper()).post { activePlayer?.pause() }
158
+ }
159
+ }
160
+ sleepTimerPreviousIndex = currentIndex
161
+ }
162
+
163
+ override fun onTimelineChanged(timeline: androidx.media3.common.Timeline, reason: Int) {
164
+ if (sleepTimerType == "mediaItem" && (activePlayer?.mediaItemCount ?: 0) == 0) {
165
+ cancelSleepTimerInternal(restoreVolume = false)
166
+ }
167
+ }
139
168
  })
140
169
  val player = createForwardingPlayer(activePlayer, config)
141
170
 
@@ -275,6 +304,7 @@ class TrackPlayerPlaybackService: MediaLibraryService(), MediaLibraryService.Med
275
304
  simpleCache?.release()
276
305
  simpleCache = null
277
306
  sharedCache = null
307
+ cancelSleepTimerInternal(restoreVolume = false)
278
308
  stopProgressSyncTimer(fireFinalTick = false)
279
309
  progressSyncExecutor?.shutdown()
280
310
  progressSyncExecutor = null
@@ -355,6 +385,37 @@ class TrackPlayerPlaybackService: MediaLibraryService(), MediaLibraryService.Med
355
385
  activePlayer?.seekToPreviousMediaItem()
356
386
  return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
357
387
  }
388
+ COMMAND_SLEEP_AFTER_TIME -> {
389
+ val seconds = args.getDouble("seconds")
390
+ val fadeOutSeconds = args.getDouble("fadeOutSeconds")
391
+ cancelSleepTimerInternal(restoreVolume = true)
392
+ sleepTimerType = "time"
393
+ sleepTimerRemainingSeconds = seconds
394
+ sleepTimerFadeOutSeconds = fadeOutSeconds.coerceAtMost(seconds)
395
+ persistSleepTimerState()
396
+
397
+ if (seconds <= 0) {
398
+ activePlayer?.pause()
399
+ this@TrackPlayerPlaybackService.emitEvent(SleepTimerTriggeredEvent("time"))
400
+ cancelSleepTimerInternal(restoreVolume = true)
401
+ } else {
402
+ startSleepCountdownTimer()
403
+ }
404
+ return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
405
+ }
406
+ COMMAND_SLEEP_AFTER_MEDIA_ITEM -> {
407
+ val index = args.getInt("index")
408
+ cancelSleepTimerInternal(restoreVolume = true)
409
+ sleepTimerType = "mediaItem"
410
+ sleepTimerTargetIndex = index
411
+ sleepTimerPreviousIndex = activePlayer?.currentMediaItemIndex
412
+ persistSleepTimerState()
413
+ return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
414
+ }
415
+ COMMAND_CANCEL_SLEEP_TIMER -> {
416
+ cancelSleepTimerInternal(restoreVolume = true)
417
+ return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
418
+ }
358
419
  }
359
420
  return super.onCustomCommand(session, controller, customCommand, args)
360
421
  }
@@ -404,12 +465,20 @@ class TrackPlayerPlaybackService: MediaLibraryService(), MediaLibraryService.Med
404
465
  return Futures.immediateFuture(LibraryResult.ofItemList(ImmutableList.copyOf(categoryItems), params))
405
466
  }
406
467
 
468
+ // Check if it's a category
407
469
  val category = browseTree.findCategory(parentId)
408
470
  if (category != null) {
409
471
  val mediaItems = category.items.map { it.toMediaItem() }
410
472
  return Futures.immediateFuture(LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), params))
411
473
  }
412
474
 
475
+ // Check if it's a browsable item with children
476
+ val browseItem = browseTree.findItem(parentId)
477
+ if (browseItem != null && browseItem.children != null) {
478
+ val mediaItems = browseItem.children.map { it.toMediaItem() }
479
+ return Futures.immediateFuture(LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), params))
480
+ }
481
+
413
482
  return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE))
414
483
  }
415
484
 
@@ -475,6 +544,9 @@ class TrackPlayerPlaybackService: MediaLibraryService(), MediaLibraryService.Med
475
544
  .add(SessionCommand(COMMAND_SEEK_TO_NEXT, Bundle.EMPTY))
476
545
  .add(SessionCommand(COMMAND_SEEK_TO_PREVIOUS, Bundle.EMPTY))
477
546
  .add(SessionCommand(COMMAND_UPDATE_PROGRESS_SYNC_HEADERS, Bundle.EMPTY))
547
+ .add(SessionCommand(COMMAND_SLEEP_AFTER_TIME, Bundle.EMPTY))
548
+ .add(SessionCommand(COMMAND_SLEEP_AFTER_MEDIA_ITEM, Bundle.EMPTY))
549
+ .add(SessionCommand(COMMAND_CANCEL_SLEEP_TIMER, Bundle.EMPTY))
478
550
  .build()
479
551
 
480
552
  // endregion
@@ -554,6 +626,86 @@ class TrackPlayerPlaybackService: MediaLibraryService(), MediaLibraryService.Med
554
626
 
555
627
  // endregion
556
628
 
629
+ // region Sleep Timer
630
+
631
+ private fun cancelSleepTimerInternal(restoreVolume: Boolean) {
632
+ sleepTimer?.cancel()
633
+ sleepTimer = null
634
+ if (restoreVolume) {
635
+ sleepTimerPreFadeVolume?.let { activePlayer?.volume = it }
636
+ }
637
+ sleepTimerPreFadeVolume = null
638
+ sleepTimerType = null
639
+ sleepTimerRemainingSeconds = 0.0
640
+ sleepTimerFadeOutSeconds = 0.0
641
+ sleepTimerTargetIndex = null
642
+ sleepTimerPreviousIndex = null
643
+ persistSleepTimerState()
644
+ }
645
+
646
+ private fun onSleepTimerTick() {
647
+ sleepTimerRemainingSeconds -= 1.0
648
+ persistSleepTimerState()
649
+
650
+ // Handle fade-out (before zero-check so final tick sets volume to 0)
651
+ if (sleepTimerFadeOutSeconds > 0 && sleepTimerRemainingSeconds < sleepTimerFadeOutSeconds) {
652
+ if (sleepTimerPreFadeVolume == null) {
653
+ sleepTimerPreFadeVolume = activePlayer?.volume ?: 1f
654
+ }
655
+ val progress = maxOf(0.0, sleepTimerRemainingSeconds / sleepTimerFadeOutSeconds).toFloat()
656
+ activePlayer?.volume = (sleepTimerPreFadeVolume ?: 1f) * progress
657
+ }
658
+
659
+ if (sleepTimerRemainingSeconds <= 0) {
660
+ sleepTimerRemainingSeconds = 0.0
661
+ activePlayer?.pause()
662
+ this@TrackPlayerPlaybackService.emitEvent(SleepTimerTriggeredEvent("time"))
663
+ // Restore volume after pausing so next playback isn't muted
664
+ cancelSleepTimerInternal(restoreVolume = true)
665
+ return
666
+ }
667
+ }
668
+
669
+ private fun startSleepCountdownTimer() {
670
+ sleepTimer?.cancel()
671
+ val handler = Handler(Looper.getMainLooper())
672
+ sleepTimer = java.util.Timer().apply {
673
+ scheduleAtFixedRate(object : java.util.TimerTask() {
674
+ override fun run() {
675
+ handler.post { onSleepTimerTick() }
676
+ }
677
+ }, 1000L, 1000L)
678
+ }
679
+ }
680
+
681
+ private fun pauseSleepCountdownTimer() {
682
+ sleepTimer?.cancel()
683
+ sleepTimer = null
684
+ }
685
+
686
+ private fun persistSleepTimerState() {
687
+ val prefs = getSharedPreferences(TrackPlayerModule.PLAYER_PREFS_NAME, MODE_PRIVATE)
688
+ val editor = prefs.edit()
689
+ val type = sleepTimerType
690
+ if (type == null) {
691
+ editor.remove(SLEEP_TIMER_STATE_KEY)
692
+ } else {
693
+ val json = JSONObject().apply {
694
+ put("type", type)
695
+ if (type == "time") {
696
+ put("remainingSeconds", sleepTimerRemainingSeconds)
697
+ put("fadeOutSeconds", sleepTimerFadeOutSeconds)
698
+ } else {
699
+ put("index", sleepTimerTargetIndex ?: 0)
700
+ }
701
+ }
702
+ editor.putString(SLEEP_TIMER_STATE_KEY, json.toString())
703
+ }
704
+ editor.apply()
705
+ }
706
+
707
+ // endregion
708
+
557
709
  // region Progress Sync
558
710
 
559
711
  private fun startProgressSyncTimer() {
@@ -651,6 +803,10 @@ class TrackPlayerPlaybackService: MediaLibraryService(), MediaLibraryService.Med
651
803
  const val COMMAND_SEEK_TO_PREVIOUS = "trackplayer.seek_to_previous"
652
804
  const val COMMAND_UPDATE_PROGRESS_SYNC_HEADERS = "trackplayer.update_progress_sync_headers"
653
805
  const val PROGRESS_SYNC_SAVED_KEY = "progress_sync_saved"
806
+ const val COMMAND_SLEEP_AFTER_TIME = "trackplayer.sleep_after_time"
807
+ const val COMMAND_SLEEP_AFTER_MEDIA_ITEM = "trackplayer.sleep_after_media_item"
808
+ const val COMMAND_CANCEL_SLEEP_TIMER = "trackplayer.cancel_sleep_timer"
809
+ const val SLEEP_TIMER_STATE_KEY = "sleep_timer_state"
654
810
 
655
811
  @Volatile
656
812
  var sharedCache: SimpleCache? = null
@@ -26,14 +26,18 @@ data class BrowseCategory(
26
26
  @Serializable
27
27
  data class BrowseMediaItem(
28
28
  val mediaId: String,
29
- val url: String,
29
+ val url: String? = null,
30
30
  val title: String? = null,
31
31
  val artist: String? = null,
32
32
  val albumTitle: String? = null,
33
33
  val artworkUrl: String? = null,
34
34
  val duration: Double? = null,
35
35
  val isLive: Boolean? = null,
36
+ val children: List<BrowseMediaItem>? = null,
36
37
  ) {
38
+ val isPlayable: Boolean get() = url != null
39
+ val isBrowsable: Boolean get() = children != null && url == null
40
+
37
41
  fun toMediaItem(): MediaItem {
38
42
  val extras = Bundle()
39
43
  duration?.let { extras.putDouble("duration", it) }
@@ -44,16 +48,20 @@ data class BrowseMediaItem(
44
48
  .setArtist(artist)
45
49
  .setAlbumTitle(albumTitle)
46
50
  .setArtworkUri(artworkUrl?.let { Uri.parse(it) })
47
- .setIsBrowsable(false)
48
- .setIsPlayable(true)
51
+ .setIsBrowsable(isBrowsable)
52
+ .setIsPlayable(isPlayable)
53
+ .setMediaType(if (isBrowsable) MediaMetadata.MEDIA_TYPE_FOLDER_MIXED else MediaMetadata.MEDIA_TYPE_MUSIC)
49
54
  .setExtras(extras)
50
55
  .build()
51
56
 
52
57
  val builder = MediaItem.Builder()
53
58
  .setMediaId(mediaId)
54
- .setUri(url)
55
59
  .setMediaMetadata(metadata)
56
60
 
61
+ if (url != null) {
62
+ builder.setUri(url)
63
+ }
64
+
57
65
  if (isLive == true) {
58
66
  builder.setLiveConfiguration(MediaItem.LiveConfiguration.Builder().build())
59
67
  }
@@ -75,7 +83,7 @@ data class BrowseTree(
75
83
  categories.find { it.mediaId == mediaId }
76
84
 
77
85
  fun findItem(mediaId: String): BrowseMediaItem? =
78
- categories.flatMap { it.items }.find { it.mediaId == mediaId }
86
+ categories.flatMap { findItemRecursive(it.items, mediaId) }.firstOrNull()
79
87
 
80
88
  companion object {
81
89
  private const val BROWSE_TREE_KEY = "browse_tree"
@@ -98,24 +106,47 @@ data class BrowseTree(
98
106
  val title = catMap.getString("title") ?: continue
99
107
  val itemsArray = catMap.getArray("items") ?: continue
100
108
 
101
- val items = mutableListOf<BrowseMediaItem>()
102
- for (j in 0 until itemsArray.size()) {
103
- val itemMap = itemsArray.getMap(j) ?: continue
104
- items.add(BrowseMediaItem(
105
- mediaId = itemMap.getString("mediaId") ?: continue,
106
- url = itemMap.getString("url") as? String ?: continue,
107
- title = if (itemMap.hasKey("title")) itemMap.getString("title") else null,
108
- artist = if (itemMap.hasKey("artist")) itemMap.getString("artist") else null,
109
- albumTitle = if (itemMap.hasKey("albumTitle")) itemMap.getString("albumTitle") else null,
110
- artworkUrl = if (itemMap.hasKey("artworkUrl")) itemMap.getString("artworkUrl") else null,
111
- duration = if (itemMap.hasKey("duration")) itemMap.getDouble("duration") else null,
112
- isLive = if (itemMap.hasKey("isLive")) itemMap.getBoolean("isLive") else null,
113
- ))
114
- }
115
-
109
+ val items = parseItems(itemsArray)
116
110
  categories.add(BrowseCategory(mediaId = mediaId, title = title, items = items))
117
111
  }
118
112
  return BrowseTree(categories)
119
113
  }
114
+
115
+ private fun parseItems(data: ReadableArray): List<BrowseMediaItem> {
116
+ val items = mutableListOf<BrowseMediaItem>()
117
+ for (i in 0 until data.size()) {
118
+ val itemMap = data.getMap(i) ?: continue
119
+ val mediaId = itemMap.getString("mediaId") ?: continue
120
+
121
+ val children = if (itemMap.hasKey("children")) {
122
+ itemMap.getArray("children")?.let { parseItems(it) }
123
+ } else null
124
+
125
+ items.add(BrowseMediaItem(
126
+ mediaId = mediaId,
127
+ url = if (itemMap.hasKey("url")) itemMap.getString("url") else null,
128
+ title = if (itemMap.hasKey("title")) itemMap.getString("title") else null,
129
+ artist = if (itemMap.hasKey("artist")) itemMap.getString("artist") else null,
130
+ albumTitle = if (itemMap.hasKey("albumTitle")) itemMap.getString("albumTitle") else null,
131
+ artworkUrl = if (itemMap.hasKey("artworkUrl")) itemMap.getString("artworkUrl") else null,
132
+ duration = if (itemMap.hasKey("duration")) itemMap.getDouble("duration") else null,
133
+ isLive = if (itemMap.hasKey("isLive")) itemMap.getBoolean("isLive") else null,
134
+ children = children,
135
+ ))
136
+ }
137
+ return items
138
+ }
139
+
140
+ private fun findItemRecursive(items: List<BrowseMediaItem>, mediaId: String): List<BrowseMediaItem> {
141
+ for (item in items) {
142
+ if (item.mediaId == mediaId) return listOf(item)
143
+ val children = item.children
144
+ if (children != null) {
145
+ val found = findItemRecursive(children, mediaId)
146
+ if (found.isNotEmpty()) return found
147
+ }
148
+ }
149
+ return emptyList()
150
+ }
120
151
  }
121
152
  }
@@ -21,6 +21,7 @@ enum class EmitEventType(val value: String) {
21
21
  REMOTE_SKIP_FORWARD("event.remote-skip-forward"),
22
22
  REMOTE_SKIP_BACKWARD("event.remote-skip-backward"),
23
23
  PLAYBACK_PROGRESS_UPDATED("event.playback-progress-updated"),
24
+ SLEEP_TIMER_TRIGGERED("event.sleep-timer-triggered"),
24
25
  }
25
26
 
26
27
  interface EmitEvent {
@@ -116,3 +117,13 @@ data class PlaybackProgressUpdatedEvent(
116
117
  )
117
118
  }
118
119
  }
120
+
121
+ data class SleepTimerTriggeredEvent(
122
+ val sleepType: String,
123
+ ): EmitEvent {
124
+ override val type = EmitEventType.SLEEP_TIMER_TRIGGERED
125
+
126
+ override fun pairs(): Array<Pair<String, Any>> {
127
+ return arrayOf("type" to sleepType)
128
+ }
129
+ }
@@ -0,0 +1,40 @@
1
+ //
2
+ // Copyright (c) Double Symmetry GmbH
3
+ // Commercial use requires a license. See https://rntp.dev/pricing
4
+ //
5
+
6
+ import Foundation
7
+
8
+ class BrowseTreeStore {
9
+ static let shared = BrowseTreeStore()
10
+
11
+ static let didChangeNotification = Notification.Name("RNTPBrowseTreeDidChange")
12
+ static let nowPlayingChangedNotification = Notification.Name("RNTPNowPlayingChanged")
13
+
14
+ /// The current browse tree categories as raw dictionaries from JS.
15
+ private(set) var categories: [[String: Any]] = []
16
+
17
+ /// Reference to the AudioPlayer, set during setupPlayer().
18
+ weak var player: AudioPlayer?
19
+
20
+ /// The mediaId of the currently playing item, if any.
21
+ private(set) var currentMediaId: String?
22
+
23
+ private init() {}
24
+
25
+ func updateNowPlaying(mediaId: String?) {
26
+ currentMediaId = mediaId
27
+ NotificationCenter.default.post(name: Self.nowPlayingChangedNotification, object: nil)
28
+ }
29
+
30
+ func update(categories: [[String: Any]]) {
31
+ self.categories = categories
32
+ NotificationCenter.default.post(name: Self.didChangeNotification, object: nil)
33
+ }
34
+
35
+ func clear() {
36
+ self.categories = []
37
+ self.player = nil
38
+ NotificationCenter.default.post(name: Self.didChangeNotification, object: nil)
39
+ }
40
+ }