@roitium/expo-orpheus 0.4.7 → 0.5.1

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.
@@ -3,27 +3,14 @@ package expo.modules.orpheus
3
3
  import android.content.Context
4
4
  import androidx.media3.common.util.UnstableApi
5
5
  import androidx.media3.database.StandaloneDatabaseProvider
6
- import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
7
6
  import androidx.media3.datasource.cache.NoOpCacheEvictor
8
7
  import androidx.media3.datasource.cache.SimpleCache
9
8
  import java.io.File
10
9
 
11
10
  @UnstableApi
12
11
  object DownloadCache {
13
- private var lruCache: SimpleCache? = null
14
12
  private var stableCache: SimpleCache? = null
15
13
 
16
- @Synchronized
17
- fun getLruCache(context: Context): SimpleCache {
18
- if (lruCache == null) {
19
- val cacheDir = File(context.cacheDir, "media_cache")
20
- val evictor = LeastRecentlyUsedCacheEvictor(256 * 1024 * 1024)
21
- val databaseProvider = StandaloneDatabaseProvider(context)
22
- lruCache = SimpleCache(cacheDir, evictor, databaseProvider)
23
- }
24
- return lruCache!!
25
- }
26
-
27
14
  @Synchronized
28
15
  fun getStableCache(context: Context): SimpleCache {
29
16
  if (stableCache == null) {
@@ -30,7 +30,7 @@ import expo.modules.kotlin.modules.Module
30
30
  import expo.modules.kotlin.modules.ModuleDefinition
31
31
  import expo.modules.orpheus.models.TrackRecord
32
32
  import expo.modules.orpheus.utils.DownloadUtil
33
- import expo.modules.orpheus.utils.MediaItemStorer
33
+ import expo.modules.orpheus.utils.Storage
34
34
  import expo.modules.orpheus.utils.toMediaItem
35
35
 
36
36
  @UnstableApi
@@ -67,7 +67,7 @@ class ExpoOrpheusModule : Module() {
67
67
 
68
68
  OnCreate {
69
69
  val context = appContext.reactContext ?: return@OnCreate
70
- MediaItemStorer.initialize(context)
70
+ Storage.initialize(context)
71
71
  val sessionToken = SessionToken(
72
72
  context,
73
73
  ComponentName(context, OrpheusMusicService::class.java)
@@ -97,16 +97,23 @@ class ExpoOrpheusModule : Module() {
97
97
  }
98
98
 
99
99
  Constant("restorePlaybackPositionEnabled") {
100
- MediaItemStorer.isRestoreEnabled()
100
+ Storage.isRestoreEnabled()
101
+ }
102
+
103
+ Constant("loudnessNormalizationEnabled") {
104
+ Storage.isLoudnessNormalizationEnabled()
101
105
  }
102
106
 
103
107
  Function("setBilibiliCookie") { cookie: String ->
104
108
  OrpheusConfig.bilibiliCookie = cookie
105
109
  }
106
110
 
111
+ Function("setLoudnessNormalizationEnabled") { enabled: Boolean ->
112
+ Storage.setLoudnessNormalizationEnabled(enabled)
113
+ }
107
114
 
108
115
  Function("setRestorePlaybackPositionEnabled") { enabled: Boolean ->
109
- MediaItemStorer.setRestoreEnabled(enabled)
116
+ Storage.setRestoreEnabled(enabled)
110
117
  }
111
118
 
112
119
  AsyncFunction("getPosition") {
@@ -175,6 +182,7 @@ class ExpoOrpheusModule : Module() {
175
182
  checkController()
176
183
  controller?.clearMediaItems()
177
184
  durationCache.clear()
185
+ DownloadUtil.itemVolumeMap.clear()
178
186
  }.runOnQueue(Queues.MAIN)
179
187
 
180
188
  AsyncFunction("skipTo") { index: Int ->
@@ -253,6 +261,7 @@ class ExpoOrpheusModule : Module() {
253
261
  }
254
262
 
255
263
  controller?.sendCustomCommand(command, args)
264
+ return@AsyncFunction null
256
265
  }.runOnQueue(Queues.MAIN)
257
266
 
258
267
  AsyncFunction("getSleepTimerEndTime") {
@@ -282,6 +291,7 @@ class ExpoOrpheusModule : Module() {
282
291
  checkController()
283
292
  val command = SessionCommand(CustomCommands.CMD_CANCEL_TIMER, Bundle.EMPTY)
284
293
  controller?.sendCustomCommand(command, Bundle.EMPTY)
294
+ return@AsyncFunction null
285
295
  }.runOnQueue(Queues.MAIN)
286
296
 
287
297
  AsyncFunction("addToEnd") { tracks: List<TrackRecord>, startFromId: String?, clearQueue: Boolean? ->
@@ -293,6 +303,7 @@ class ExpoOrpheusModule : Module() {
293
303
  if (clearQueue == true) {
294
304
  player.clearMediaItems()
295
305
  durationCache.clear()
306
+ DownloadUtil.itemVolumeMap.clear()
296
307
  }
297
308
  val initialSize = player.mediaItemCount
298
309
  player.addMediaItems(mediaItems)
@@ -316,6 +327,40 @@ class ExpoOrpheusModule : Module() {
316
327
  }
317
328
  }.runOnQueue(Queues.MAIN)
318
329
 
330
+ AsyncFunction("playNext") { track: TrackRecord ->
331
+ checkController()
332
+ val player = controller ?: return@AsyncFunction
333
+
334
+ val mediaItem = track.toMediaItem(gson)
335
+ val targetIndex = player.currentMediaItemIndex + 1
336
+
337
+ var existingIndex = -1
338
+ for (i in 0 until player.mediaItemCount) {
339
+ if (player.getMediaItemAt(i).mediaId == track.id) {
340
+ existingIndex = i
341
+ break
342
+ }
343
+ }
344
+
345
+ if (existingIndex != -1) {
346
+ if (existingIndex == player.currentMediaItemIndex) {
347
+ return@AsyncFunction
348
+ }
349
+ val safeTargetIndex = targetIndex.coerceAtMost(player.mediaItemCount)
350
+
351
+ player.moveMediaItem(existingIndex, safeTargetIndex)
352
+
353
+ } else {
354
+ val safeTargetIndex = targetIndex.coerceAtMost(player.mediaItemCount)
355
+
356
+ player.addMediaItem(safeTargetIndex, mediaItem)
357
+ }
358
+
359
+ if (player.playbackState == Player.STATE_IDLE) {
360
+ player.prepare()
361
+ }
362
+ }.runOnQueue(Queues.MAIN)
363
+
319
364
  AsyncFunction("downloadTrack") { track: TrackRecord ->
320
365
  val context = appContext.reactContext ?: return@AsyncFunction
321
366
  val downloadRequest = DownloadRequest.Builder(track.id, track.url.toUri())
@@ -616,6 +661,16 @@ class ExpoOrpheusModule : Module() {
616
661
  )
617
662
  )
618
663
  }
664
+
665
+ override fun onRepeatModeChanged(repeatMode: Int) {
666
+ super.onRepeatModeChanged(repeatMode)
667
+ Storage.saveRepeatMode(repeatMode)
668
+ }
669
+
670
+ override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {
671
+ super.onShuffleModeEnabledChanged(shuffleModeEnabled)
672
+ Storage.saveShuffleMode(shuffleModeEnabled)
673
+ }
619
674
  })
620
675
  }
621
676
 
@@ -686,7 +741,7 @@ class ExpoOrpheusModule : Module() {
686
741
  private fun saveCurrentPosition() {
687
742
  val player = controller ?: return
688
743
  if (player.playbackState != Player.STATE_IDLE) {
689
- MediaItemStorer.savePosition(
744
+ Storage.savePosition(
690
745
  player.currentMediaItemIndex,
691
746
  player.currentPosition
692
747
  )
@@ -19,21 +19,31 @@ import androidx.media3.session.SessionCommand
19
19
  import androidx.media3.session.SessionResult
20
20
  import com.google.common.util.concurrent.Futures
21
21
  import com.google.common.util.concurrent.ListenableFuture
22
+ import expo.modules.orpheus.bilibili.VolumeData
22
23
  import expo.modules.orpheus.utils.DownloadUtil
23
- import expo.modules.orpheus.utils.MediaItemStorer
24
24
  import expo.modules.orpheus.utils.SleepTimeController
25
+ import expo.modules.orpheus.utils.Storage
26
+ import expo.modules.orpheus.utils.calculateLoudnessGain
27
+ import expo.modules.orpheus.utils.fadeInTo
28
+ import kotlinx.coroutines.Job
29
+ import kotlinx.coroutines.MainScope
30
+ import kotlinx.coroutines.cancel
31
+ import kotlinx.coroutines.launch
32
+ import kotlin.math.abs
25
33
 
26
34
  class OrpheusMusicService : MediaLibraryService() {
27
35
 
28
36
  private var player: ExoPlayer? = null
29
37
  private var mediaSession: MediaLibrarySession? = null
30
38
  private var sleepTimerManager: SleepTimeController? = null
39
+ private var volumeFadeJob: Job? = null
40
+ private var scope = MainScope()
31
41
 
32
42
  @OptIn(UnstableApi::class)
33
43
  override fun onCreate() {
34
44
  super.onCreate()
35
45
 
36
- MediaItemStorer.initialize(this)
46
+ Storage.initialize(this)
37
47
 
38
48
 
39
49
  val dataSourceFactory = DownloadUtil.getPlayerDataSourceFactory(this)
@@ -83,8 +93,18 @@ class OrpheusMusicService : MediaLibraryService() {
83
93
  .setSessionActivity(contentIntent)
84
94
  .build()
85
95
 
86
- restorePlayerState(MediaItemStorer.isRestoreEnabled())
96
+ restorePlayerState(Storage.isRestoreEnabled())
87
97
  sleepTimerManager = SleepTimeController(player!!)
98
+
99
+ // 当有新的响度数据时,如果是当前这首歌的就直接应用,否则是预加载,等待 onMediaItemTransition 处理
100
+ scope.launch {
101
+ DownloadUtil.volumeResolvedEvent.collect { (uri, volumeData) ->
102
+ val currentUri = player?.currentMediaItem?.localConfiguration?.uri?.toString()
103
+ if (currentUri == uri) {
104
+ applyVolumeForCurrentItem(volumeData)
105
+ }
106
+ }
107
+ }
88
108
  }
89
109
 
90
110
  override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaLibrarySession? {
@@ -92,6 +112,8 @@ class OrpheusMusicService : MediaLibraryService() {
92
112
  }
93
113
 
94
114
  override fun onDestroy() {
115
+ scope.cancel()
116
+
95
117
  mediaSession?.run {
96
118
  player.release()
97
119
  release()
@@ -187,13 +209,15 @@ class OrpheusMusicService : MediaLibraryService() {
187
209
  private fun restorePlayerState(restorePosition: Boolean) {
188
210
  val player = player ?: return
189
211
 
190
- val restoredItems = MediaItemStorer.restoreQueue()
212
+ val restoredItems = Storage.restoreQueue()
191
213
 
192
214
  if (restoredItems.isNotEmpty()) {
193
215
  player.setMediaItems(restoredItems)
194
216
 
195
- val savedIndex = MediaItemStorer.getSavedIndex()
196
- val savedPosition = MediaItemStorer.getSavedPosition()
217
+ val savedIndex = Storage.getSavedIndex()
218
+ val savedPosition = Storage.getSavedPosition()
219
+ val savedShuffleMode = Storage.getShuffleMode()
220
+ val savedRepeatMode = Storage.getRepeatMode()
197
221
 
198
222
  if (savedIndex >= 0 && savedIndex < restoredItems.size) {
199
223
  player.seekTo(savedIndex, if (restorePosition) savedPosition else C.TIME_UNSET)
@@ -201,6 +225,9 @@ class OrpheusMusicService : MediaLibraryService() {
201
225
  player.seekTo(0, 0L)
202
226
  }
203
227
 
228
+ player.shuffleModeEnabled = savedShuffleMode
229
+ player.repeatMode = savedRepeatMode
230
+
204
231
  player.playWhenReady = false
205
232
  player.prepare()
206
233
  }
@@ -208,11 +235,19 @@ class OrpheusMusicService : MediaLibraryService() {
208
235
 
209
236
  private fun setupListeners() {
210
237
  player?.addListener(object : Player.Listener {
238
+
239
+ @OptIn(UnstableApi::class)
211
240
  override fun onMediaItemTransition(
212
241
  mediaItem: androidx.media3.common.MediaItem?,
213
242
  reason: Int
214
243
  ) {
215
244
  saveCurrentQueue()
245
+ val uri = mediaItem?.localConfiguration?.uri?.toString() ?: return
246
+
247
+ val volumeData = DownloadUtil.itemVolumeMap[uri]
248
+ if (volumeData != null) {
249
+ applyVolumeForCurrentItem(volumeData)
250
+ }
216
251
  }
217
252
 
218
253
  override fun onTimelineChanged(timeline: Timeline, reason: Int) {
@@ -225,7 +260,30 @@ class OrpheusMusicService : MediaLibraryService() {
225
260
  val player = player ?: return
226
261
  val queue = List(player.mediaItemCount) { i -> player.getMediaItemAt(i) }
227
262
  if (queue.isNotEmpty()) {
228
- MediaItemStorer.saveQueue(queue)
263
+ Storage.saveQueue(queue)
229
264
  }
230
265
  }
266
+
267
+ @OptIn(UnstableApi::class)
268
+ private fun applyVolumeForCurrentItem(volumeData: VolumeData) {
269
+ val player = player ?: return
270
+ volumeFadeJob?.cancel()
271
+ val isLoudnessNormalizationEnabled = Storage.isLoudnessNormalizationEnabled()
272
+ if (!isLoudnessNormalizationEnabled) return
273
+ val gain = run {
274
+ val measured = volumeData.measuredI
275
+ val target = volumeData.targetI
276
+
277
+ if (measured == 0.0) 1.0f else calculateLoudnessGain(measured, target)
278
+ }
279
+
280
+ val targetVol = 1.0f * gain
281
+ val currentVolume = player.volume
282
+
283
+ if (abs(currentVolume - targetVol) < 0.001f) {
284
+ return
285
+ }
286
+
287
+ volumeFadeJob = player.fadeInTo(targetVol, 600L, scope)
288
+ }
231
289
  }
@@ -58,7 +58,7 @@ object BilibiliRepository {
58
58
  enableDolby: Boolean,
59
59
  enableHiRes: Boolean,
60
60
  cookie: String?
61
- ): String {
61
+ ): Pair<String, VolumeData?> {
62
62
  var cidInternal = cid
63
63
  val (imgKey, subKey) = getWbiKeys()
64
64
  if (cidInternal === null) {
@@ -94,22 +94,23 @@ object BilibiliRepository {
94
94
  val data = apiResponse.data
95
95
  val dash = data.dash
96
96
  val durl = data.durl
97
+ val volume = data.volume
97
98
 
98
99
  if (dash == null) {
99
100
  if (durl.isNullOrEmpty()) {
100
101
  throw IOException("AudioStreamError: 请求到的流数据不包含 dash 或 durl 任一字段")
101
102
  }
102
- return durl[0].url
103
+ return durl[0].url to volume
103
104
  }
104
105
 
105
106
  if (enableDolby && dash.dolby?.audio?.isNotEmpty() == true) {
106
107
  Log.d(TAG, "select dolby source")
107
- return dash.dolby.audio[0].baseUrl
108
+ return dash.dolby.audio[0].baseUrl to volume
108
109
  }
109
110
 
110
111
  if (enableHiRes && dash.flac?.audio != null) {
111
112
  Log.d(TAG, "select hires source")
112
- return dash.flac.audio.baseUrl
113
+ return dash.flac.audio.baseUrl to volume
113
114
  }
114
115
 
115
116
  if (dash.audio.isNullOrEmpty()) {
@@ -119,10 +120,10 @@ object BilibiliRepository {
119
120
  val targetAudio = dash.audio.find { it.id == audioQuality }
120
121
 
121
122
  if (targetAudio != null) {
122
- return targetAudio.baseUrl
123
+ return targetAudio.baseUrl to volume
123
124
  } else {
124
125
  val highestQualityAudio = dash.audio[0]
125
- return highestQualityAudio.baseUrl
126
+ return highestQualityAudio.baseUrl to volume
126
127
  }
127
128
  }
128
129
 
@@ -3,14 +3,6 @@ package expo.modules.orpheus.models
3
3
  import expo.modules.kotlin.records.Field
4
4
  import expo.modules.kotlin.records.Record
5
5
 
6
- class LoudnessRecord : Record {
7
- @Field
8
- var measured_i: Double = 0.0
9
-
10
- @Field
11
- var target_i: Double = 0.0
12
- }
13
-
14
6
  class TrackRecord : Record {
15
7
  @Field
16
8
  var id: String = ""
@@ -30,7 +22,4 @@ class TrackRecord : Record {
30
22
  // unit: second
31
23
  @Field
32
24
  var duration: Double? = null
33
-
34
- @Field
35
- var loudness: LoudnessRecord? = null
36
25
  }
@@ -16,6 +16,13 @@ import expo.modules.orpheus.DownloadCache
16
16
  import expo.modules.orpheus.OrpheusConfig
17
17
  import expo.modules.orpheus.OrpheusDownloadService
18
18
  import expo.modules.orpheus.bilibili.BilibiliRepository
19
+ import expo.modules.orpheus.bilibili.VolumeData
20
+ import kotlinx.coroutines.CoroutineScope
21
+ import kotlinx.coroutines.Dispatchers
22
+ import kotlinx.coroutines.channels.BufferOverflow
23
+ import kotlinx.coroutines.flow.MutableSharedFlow
24
+ import kotlinx.coroutines.flow.asSharedFlow
25
+ import kotlinx.coroutines.launch
19
26
  import java.io.IOException
20
27
  import java.util.concurrent.Executors
21
28
 
@@ -28,6 +35,15 @@ object DownloadUtil {
28
35
 
29
36
  private var downloadNotificationHelper: DownloadNotificationHelper? = null
30
37
 
38
+ var itemVolumeMap: MutableMap<String, VolumeData> = mutableMapOf()
39
+
40
+ private val _volumeResolvedEvent = MutableSharedFlow<Pair<String, VolumeData>>(
41
+ replay = 0,
42
+ extraBufferCapacity = 1,
43
+ onBufferOverflow = BufferOverflow.DROP_OLDEST
44
+ )
45
+ val volumeResolvedEvent = _volumeResolvedEvent.asSharedFlow()
46
+
31
47
  @Synchronized
32
48
  fun getDownloadManager(context: Context): DownloadManager {
33
49
  if (downloadManager == null) {
@@ -51,17 +67,11 @@ object DownloadUtil {
51
67
  if (playerDataSourceFactory == null) {
52
68
  val upstreamFactory = getUpstreamFactory()
53
69
 
54
- val lruCache = DownloadCache.getLruCache(context)
55
70
  val downloadCache = DownloadCache.getStableCache(context)
56
71
 
57
- val lruFactory = CacheDataSource.Factory()
58
- .setCache(lruCache)
59
- .setUpstreamDataSourceFactory(upstreamFactory)
60
- .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
61
-
62
72
  val downloadFactory = CacheDataSource.Factory()
63
73
  .setCache(downloadCache)
64
- .setUpstreamDataSourceFactory(lruFactory)
74
+ .setUpstreamDataSourceFactory(upstreamFactory)
65
75
  .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
66
76
  .setCacheWriteDataSinkFactory(null)
67
77
 
@@ -99,7 +109,12 @@ object DownloadUtil {
99
109
  return downloadNotificationHelper!!
100
110
  }
101
111
 
102
- private class BilibiliResolver : ResolvingDataSource.Resolver {
112
+ suspend fun emitVolumeEvent(uri: String, data: VolumeData) {
113
+ _volumeResolvedEvent.emit(uri to data)
114
+ }
115
+
116
+ private class BilibiliResolver :
117
+ ResolvingDataSource.Resolver {
103
118
  override fun resolveDataSpec(dataSpec: DataSpec): DataSpec {
104
119
  val uri = dataSpec.uri
105
120
  if (uri.scheme == "orpheus" && uri.host == "bilibili") {
@@ -107,7 +122,7 @@ object DownloadUtil {
107
122
  val bvid = uri.getQueryParameter("bvid")
108
123
  val cid = uri.getQueryParameter("cid")?.toLongOrNull()
109
124
  val quality = uri.getQueryParameter("quality")?.toIntOrNull() ?: 30280
110
- val realUrl = BilibiliRepository.resolveAudioUrl(
125
+ val (realUrl, volume) = BilibiliRepository.resolveAudioUrl(
111
126
  bvid = bvid!!,
112
127
  cid = cid,
113
128
  audioQuality = quality,
@@ -115,6 +130,13 @@ object DownloadUtil {
115
130
  enableHiRes = uri.getQueryParameter("hires") == "1",
116
131
  cookie = OrpheusConfig.bilibiliCookie
117
132
  )
133
+ // 在这里保存响度均衡数据,并且直接发一个事件,在 OrpheusMusicService 监听
134
+ if (volume !== null) {
135
+ itemVolumeMap[dataSpec.uri.toString()] = volume
136
+ CoroutineScope(Dispatchers.IO).launch {
137
+ emitVolumeEvent(dataSpec.uri.toString(), volume)
138
+ }
139
+ }
118
140
 
119
141
  val headers = HashMap<String, String>()
120
142
  headers["Referer"] = "https://www.bilibili.com/"
@@ -11,13 +11,17 @@ import com.tencent.mmkv.MMKV
11
11
  import expo.modules.orpheus.models.TrackRecord
12
12
 
13
13
  @OptIn(UnstableApi::class)
14
- object MediaItemStorer {
14
+ object Storage {
15
15
  private var kv: MMKV? = null
16
16
  private val gson = Gson()
17
17
  private const val KEY_RESTORE_POSITION_ENABLED = "config_restore_position_enabled"
18
+
19
+ private const val KEY_LOUDNESS_NORMALIZATION_ENABLED = "config_loudness_normalization_enabled"
18
20
  private const val KEY_SAVED_QUEUE = "saved_queue_json_list"
19
21
  private const val KEY_SAVED_INDEX = "saved_index"
20
22
  private const val KEY_SAVED_POSITION = "saved_position"
23
+ private const val KEY_SAVED_REPEAT_MODE = "saved_repeat_mode"
24
+ private const val KEY_SAVED_SHUFFLE_MODE = "saved_shuffle_mode"
21
25
 
22
26
 
23
27
  @Synchronized
@@ -39,10 +43,22 @@ object MediaItemStorer {
39
43
  }
40
44
  }
41
45
 
46
+ fun setLoudnessNormalizationEnabled(enabled: Boolean) {
47
+ try {
48
+ safeKv.encode(KEY_LOUDNESS_NORMALIZATION_ENABLED, enabled)
49
+ } catch (e: Exception) {
50
+ Log.e("MediaItemStorer", "Failed to set loudness normalization enabled", e)
51
+ }
52
+ }
53
+
42
54
  fun isRestoreEnabled(): Boolean {
43
55
  return safeKv.decodeBool(KEY_RESTORE_POSITION_ENABLED, false)
44
56
  }
45
57
 
58
+ fun isLoudnessNormalizationEnabled(): Boolean {
59
+ return safeKv.decodeBool(KEY_LOUDNESS_NORMALIZATION_ENABLED, true)
60
+ }
61
+
46
62
  @OptIn(UnstableApi::class)
47
63
  fun saveQueue(mediaItems: List<MediaItem>) {
48
64
  try {
@@ -89,6 +105,11 @@ object MediaItemStorer {
89
105
  safeKv.encode(KEY_SAVED_POSITION, position)
90
106
  }
91
107
 
108
+ fun saveRepeatMode(repeatMode: Int) = safeKv.encode(KEY_SAVED_REPEAT_MODE, repeatMode)
109
+ fun saveShuffleMode(shuffleMode: Boolean) = safeKv.encode(KEY_SAVED_SHUFFLE_MODE, shuffleMode)
110
+
92
111
  fun getSavedIndex() = kv?.decodeInt(KEY_SAVED_INDEX, 0) ?: 0
93
112
  fun getSavedPosition() = kv?.decodeLong(KEY_SAVED_POSITION, 0L) ?: 0L
113
+ fun getRepeatMode() = kv?.decodeInt(KEY_SAVED_REPEAT_MODE, 0) ?: 0
114
+ fun getShuffleMode() = kv?.decodeBool(KEY_SAVED_SHUFFLE_MODE, false) ?: false
94
115
  }
@@ -13,11 +13,6 @@ fun TrackRecord.toMediaItem(gson: Gson): MediaItem {
13
13
  val extras = Bundle()
14
14
  extras.putString("track_json", trackJson)
15
15
 
16
- this.loudness?.let {
17
- extras.putDouble("loudness_measured_i", it.measured_i)
18
- extras.putDouble("loudness_target_i", it.target_i)
19
- }
20
-
21
16
  val artUri = if (!this.artwork.isNullOrEmpty()) this.artwork?.toUri() else null
22
17
 
23
18
  val metadata = MediaMetadata.Builder()
@@ -0,0 +1,57 @@
1
+ package expo.modules.orpheus.utils
2
+
3
+ import android.util.Log
4
+ import kotlinx.coroutines.CoroutineScope
5
+ import kotlinx.coroutines.Job
6
+ import kotlinx.coroutines.delay
7
+ import kotlinx.coroutines.launch
8
+ import kotlin.math.pow
9
+
10
+ /**
11
+ * 响度均衡计算
12
+ * @param measuredI 实测响度
13
+ * @param targetI 目标响度
14
+ * @return gain
15
+ */
16
+ fun calculateLoudnessGain(measuredI: Double, targetI: Double = -14.0): Float {
17
+
18
+ if (measuredI == 0.0) {
19
+ return 1.0f
20
+ }
21
+
22
+ val gainDb = targetI - measuredI
23
+ val linearFactor = 10.0.pow(gainDb / 20.0).toFloat()
24
+
25
+ val finalResult = linearFactor.coerceIn(0.0f, 1.0f)
26
+
27
+ return finalResult
28
+ }
29
+
30
+ /**
31
+ * Volume Fade In
32
+ * @param targetVolume 最终要达到的音量
33
+ * @param durationMs 淡入持续时间
34
+ * @param scope 协程作用域
35
+ */
36
+ fun androidx.media3.common.Player.fadeInTo(
37
+ targetVolume: Float,
38
+ durationMs: Long = 600L,
39
+ scope: CoroutineScope
40
+ ): Job {
41
+ this.volume = 0f
42
+
43
+ return scope.launch {
44
+ val stepInterval = 16L
45
+ val steps = (durationMs / stepInterval).toInt()
46
+ val volumeStep = targetVolume / steps
47
+
48
+ for (i in 1..steps) {
49
+ val newVol = volumeStep * i
50
+ val finalVol = newVol.coerceAtMost(targetVolume)
51
+ Log.d("Loudness", "finalVol $finalVol")
52
+ volume = finalVol
53
+ delay(stepInterval)
54
+ }
55
+ volume = targetVolume
56
+ }
57
+ }
@@ -57,6 +57,7 @@ export type OrpheusEvents = {
57
57
  };
58
58
  declare class OrpheusModule extends NativeModule<OrpheusEvents> {
59
59
  restorePlaybackPositionEnabled: boolean;
60
+ loudnessNormalizationEnabled: boolean;
60
61
  /**
61
62
  * 获取当前进度(秒)
62
63
  */
@@ -92,6 +93,7 @@ declare class OrpheusModule extends NativeModule<OrpheusEvents> {
92
93
  getRepeatMode(): Promise<RepeatMode>;
93
94
  setBilibiliCookie(cookie: string): void;
94
95
  setRestorePlaybackPositionEnabled(enabled: boolean): void;
96
+ setLoudnessNormalizationEnabled(enabled: boolean): void;
95
97
  play(): Promise<void>;
96
98
  pause(): Promise<void>;
97
99
  clear(): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoOrpheusModule.d.ts","sourceRoot":"","sources":["../src/ExpoOrpheusModule.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ExpoOrpheusModule.d.ts","sourceRoot":"","sources":["../src/ExpoOrpheusModule.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoOrpheusModule.js","sourceRoot":"","sources":["../src/ExpoOrpheusModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAgB,MAAM,mBAAmB,CAAC;AAEtE,MAAM,CAAN,IAAY,aAKX;AALD,WAAY,aAAa;IACvB,iDAAQ,CAAA;IACR,2DAAa,CAAA;IACb,mDAAS,CAAA;IACT,mDAAS,CAAA;AACX,CAAC,EALW,aAAa,KAAb,aAAa,QAKxB;AAED,MAAM,CAAN,IAAY,UAIX;AAJD,WAAY,UAAU;IACpB,yCAAO,CAAA;IACP,6CAAS,CAAA;IACT,6CAAS,CAAA;AACX,CAAC,EAJW,UAAU,KAAV,UAAU,QAIrB;AAED,MAAM,CAAN,IAAY,gBAKX;AALD,WAAY,gBAAgB;IAC1B,2DAAU,CAAA;IACV,uDAAQ,CAAA;IACR,uDAAQ,CAAA;IACR,+EAAoB,CAAA;AACtB,CAAC,EALW,gBAAgB,KAAhB,gBAAgB,QAK3B;AAsLD,MAAM,CAAN,IAAY,aAQX;AARD,WAAY,aAAa;IACvB,qDAAU,CAAA;IACV,uDAAW,CAAA;IACX,+DAAe,CAAA;IACf,2DAAa,CAAA;IACb,qDAAU,CAAA;IACV,yDAAY,CAAA;IACZ,6DAAc,CAAA;AAChB,CAAC,EARW,aAAa,KAAb,aAAa,QAQxB;AAWD,MAAM,CAAC,MAAM,OAAO,GAAG,mBAAmB,CAAgB,SAAS,CAAC,CAAC","sourcesContent":["import { requireNativeModule, NativeModule } from \"expo-modules-core\";\n\nexport enum PlaybackState {\n IDLE = 1,\n BUFFERING = 2,\n READY = 3,\n ENDED = 4,\n}\n\nexport enum RepeatMode {\n OFF = 0,\n TRACK = 1,\n QUEUE = 2,\n}\n\nexport enum TransitionReason {\n REPEAT = 0,\n AUTO = 1,\n SEEK = 2,\n PLAYLIST_CHANGED = 3,\n}\n\nexport interface Track {\n id: string;\n url: string;\n title?: string;\n artist?: string;\n artwork?: string;\n duration?: number;\n loudness?: {\n measured_i: number;\n target_i: number;\n }\n}\n\nexport type OrpheusEvents = {\n onPlaybackStateChanged(event: { state: PlaybackState }): void;\n onTrackStarted(event: { trackId: string; reason: TransitionReason }): void;\n onTrackFinished(event: {\n trackId: string;\n finalPosition: number;\n duration: number;\n }): void;\n onPlayerError(event: { code: string; message: string }): void;\n onPositionUpdate(event: {\n position: number;\n duration: number;\n buffered: number;\n }): void;\n onIsPlayingChanged(event: { status: boolean }): void;\n onDownloadUpdated(event: DownloadTask): void;\n};\n\ndeclare class OrpheusModule extends NativeModule<OrpheusEvents> {\n \n restorePlaybackPositionEnabled: boolean;\n\n /**\n * 获取当前进度(秒)\n */\n getPosition(): Promise<number>;\n\n /**\n * 获取总时长(秒)\n */\n getDuration(): Promise<number>;\n\n /**\n * 获取缓冲进度(秒)\n */\n getBuffered(): Promise<number>;\n\n /**\n * 获取是否正在播放\n */\n getIsPlaying(): Promise<boolean>;\n\n /**\n * 获取当前播放索引\n */\n getCurrentIndex(): Promise<number>;\n\n /**\n * 获取当前播放的 Track 对象\n */\n getCurrentTrack(): Promise<Track | null>;\n\n /**\n * 获取随机模式状态\n */\n getShuffleMode(): Promise<boolean>;\n\n /**\n * 获取指定索引的 Track\n */\n getIndexTrack(index: number): Promise<Track | null>;\n\n getRepeatMode(): Promise<RepeatMode>;\n\n setBilibiliCookie(cookie: string): void;\n \n setRestorePlaybackPositionEnabled(enabled: boolean): void;\n\n play(): Promise<void>;\n\n pause(): Promise<void>;\n\n clear(): Promise<void>;\n\n skipTo(index: number): Promise<void>;\n\n skipToNext(): Promise<void>;\n\n skipToPrevious(): Promise<void>;\n\n /**\n * 跳转进度\n * @param seconds 秒数\n */\n seekTo(seconds: number): Promise<void>;\n\n setRepeatMode(mode: RepeatMode): Promise<void>;\n\n setShuffleMode(enabled: boolean): Promise<void>;\n\n getQueue(): Promise<Track[]>;\n\n /**\n * 添加到队列末尾,且不去重。\n * @param tracks\n * @param startFromId 可选,添加后立即播放该 ID 的曲目\n * @param clearQueue 可选,是否清空当前队列\n */\n addToEnd(\n tracks: Track[],\n startFromId?: string,\n clearQueue?: boolean\n ): Promise<void>;\n\n /**\n * 播放下一首\n * @param track\n */\n playNext(track: Track): Promise<void>;\n\n removeTrack(index: number): Promise<void>;\n\n /**\n * 设置睡眠定时器\n * @param durationMs 单位毫秒\n */\n setSleepTimer(durationMs: number): Promise<void>;\n\n /**\n * 获取睡眠定时器结束时间\n * @returns 单位毫秒,如果没有设置则返回 null\n */\n getSleepTimerEndTime(): Promise<number | null>;\n\n cancelSleepTimer(): Promise<void>;\n\n /**\n * 下载单首歌曲\n */\n downloadTrack(track: Track): Promise<void>;\n\n /**\n * 移除下载任务\n */\n removeDownload(id: string): Promise<void>;\n\n /**\n * 批量下载歌曲\n */\n multiDownload(tracks: Track[]): Promise<void>;\n\n /**\n * 移除所有下载任务(包括已完成的及源文件)\n */\n removeAllDownloads(): Promise<void>;\n\n /**\n * 获取所有下载任务\n */\n getDownloads(): Promise<DownloadTask[]>;\n\n /**\n * 批量返回指定 ID 的下载状态\n */\n getDownloadStatusByIds(ids: string[]): Promise<Record<string, DownloadState>>;\n\n /**\n * 清除未完成的下载任务\n */\n clearUncompletedDownloadTasks(): Promise<void>;\n\n /**\n * 获取所有未完成的下载任务\n */\n getUncompletedDownloadTasks(): Promise<DownloadTask[]>;\n}\n\nexport enum DownloadState {\n QUEUED = 0,\n STOPPED = 1,\n DOWNLOADING = 2,\n COMPLETED = 3,\n FAILED = 4,\n REMOVING = 5,\n RESTARTING = 7,\n}\n\nexport interface DownloadTask {\n id: string;\n state: DownloadState;\n percentDownloaded: number;\n bytesDownloaded: number;\n contentLength: number;\n track?: Track;\n}\n\nexport const Orpheus = requireNativeModule<OrpheusModule>(\"Orpheus\");\n"]}
1
+ {"version":3,"file":"ExpoOrpheusModule.js","sourceRoot":"","sources":["../src/ExpoOrpheusModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAgB,MAAM,mBAAmB,CAAC;AAEtE,MAAM,CAAN,IAAY,aAKX;AALD,WAAY,aAAa;IACvB,iDAAQ,CAAA;IACR,2DAAa,CAAA;IACb,mDAAS,CAAA;IACT,mDAAS,CAAA;AACX,CAAC,EALW,aAAa,KAAb,aAAa,QAKxB;AAED,MAAM,CAAN,IAAY,UAIX;AAJD,WAAY,UAAU;IACpB,yCAAO,CAAA;IACP,6CAAS,CAAA;IACT,6CAAS,CAAA;AACX,CAAC,EAJW,UAAU,KAAV,UAAU,QAIrB;AAED,MAAM,CAAN,IAAY,gBAKX;AALD,WAAY,gBAAgB;IAC1B,2DAAU,CAAA;IACV,uDAAQ,CAAA;IACR,uDAAQ,CAAA;IACR,+EAAoB,CAAA;AACtB,CAAC,EALW,gBAAgB,KAAhB,gBAAgB,QAK3B;AAwLD,MAAM,CAAN,IAAY,aAQX;AARD,WAAY,aAAa;IACvB,qDAAU,CAAA;IACV,uDAAW,CAAA;IACX,+DAAe,CAAA;IACf,2DAAa,CAAA;IACb,qDAAU,CAAA;IACV,yDAAY,CAAA;IACZ,6DAAc,CAAA;AAChB,CAAC,EARW,aAAa,KAAb,aAAa,QAQxB;AAWD,MAAM,CAAC,MAAM,OAAO,GAAG,mBAAmB,CAAgB,SAAS,CAAC,CAAC","sourcesContent":["import { requireNativeModule, NativeModule } from \"expo-modules-core\";\n\nexport enum PlaybackState {\n IDLE = 1,\n BUFFERING = 2,\n READY = 3,\n ENDED = 4,\n}\n\nexport enum RepeatMode {\n OFF = 0,\n TRACK = 1,\n QUEUE = 2,\n}\n\nexport enum TransitionReason {\n REPEAT = 0,\n AUTO = 1,\n SEEK = 2,\n PLAYLIST_CHANGED = 3,\n}\n\nexport interface Track {\n id: string;\n url: string;\n title?: string;\n artist?: string;\n artwork?: string;\n duration?: number;\n loudness?: {\n measured_i: number;\n target_i: number;\n }\n}\n\nexport type OrpheusEvents = {\n onPlaybackStateChanged(event: { state: PlaybackState }): void;\n onTrackStarted(event: { trackId: string; reason: TransitionReason }): void;\n onTrackFinished(event: {\n trackId: string;\n finalPosition: number;\n duration: number;\n }): void;\n onPlayerError(event: { code: string; message: string }): void;\n onPositionUpdate(event: {\n position: number;\n duration: number;\n buffered: number;\n }): void;\n onIsPlayingChanged(event: { status: boolean }): void;\n onDownloadUpdated(event: DownloadTask): void;\n};\n\ndeclare class OrpheusModule extends NativeModule<OrpheusEvents> {\n \n restorePlaybackPositionEnabled: boolean;\n loudnessNormalizationEnabled: boolean;\n\n /**\n * 获取当前进度(秒)\n */\n getPosition(): Promise<number>;\n\n /**\n * 获取总时长(秒)\n */\n getDuration(): Promise<number>;\n\n /**\n * 获取缓冲进度(秒)\n */\n getBuffered(): Promise<number>;\n\n /**\n * 获取是否正在播放\n */\n getIsPlaying(): Promise<boolean>;\n\n /**\n * 获取当前播放索引\n */\n getCurrentIndex(): Promise<number>;\n\n /**\n * 获取当前播放的 Track 对象\n */\n getCurrentTrack(): Promise<Track | null>;\n\n /**\n * 获取随机模式状态\n */\n getShuffleMode(): Promise<boolean>;\n\n /**\n * 获取指定索引的 Track\n */\n getIndexTrack(index: number): Promise<Track | null>;\n\n getRepeatMode(): Promise<RepeatMode>;\n\n setBilibiliCookie(cookie: string): void;\n \n setRestorePlaybackPositionEnabled(enabled: boolean): void;\n setLoudnessNormalizationEnabled(enabled: boolean): void;\n\n play(): Promise<void>;\n\n pause(): Promise<void>;\n\n clear(): Promise<void>;\n\n skipTo(index: number): Promise<void>;\n\n skipToNext(): Promise<void>;\n\n skipToPrevious(): Promise<void>;\n\n /**\n * 跳转进度\n * @param seconds 秒数\n */\n seekTo(seconds: number): Promise<void>;\n\n setRepeatMode(mode: RepeatMode): Promise<void>;\n\n setShuffleMode(enabled: boolean): Promise<void>;\n\n getQueue(): Promise<Track[]>;\n\n /**\n * 添加到队列末尾,且不去重。\n * @param tracks\n * @param startFromId 可选,添加后立即播放该 ID 的曲目\n * @param clearQueue 可选,是否清空当前队列\n */\n addToEnd(\n tracks: Track[],\n startFromId?: string,\n clearQueue?: boolean\n ): Promise<void>;\n\n /**\n * 播放下一首\n * @param track\n */\n playNext(track: Track): Promise<void>;\n\n removeTrack(index: number): Promise<void>;\n\n /**\n * 设置睡眠定时器\n * @param durationMs 单位毫秒\n */\n setSleepTimer(durationMs: number): Promise<void>;\n\n /**\n * 获取睡眠定时器结束时间\n * @returns 单位毫秒,如果没有设置则返回 null\n */\n getSleepTimerEndTime(): Promise<number | null>;\n\n cancelSleepTimer(): Promise<void>;\n\n /**\n * 下载单首歌曲\n */\n downloadTrack(track: Track): Promise<void>;\n\n /**\n * 移除下载任务\n */\n removeDownload(id: string): Promise<void>;\n\n /**\n * 批量下载歌曲\n */\n multiDownload(tracks: Track[]): Promise<void>;\n\n /**\n * 移除所有下载任务(包括已完成的及源文件)\n */\n removeAllDownloads(): Promise<void>;\n\n /**\n * 获取所有下载任务\n */\n getDownloads(): Promise<DownloadTask[]>;\n\n /**\n * 批量返回指定 ID 的下载状态\n */\n getDownloadStatusByIds(ids: string[]): Promise<Record<string, DownloadState>>;\n\n /**\n * 清除未完成的下载任务\n */\n clearUncompletedDownloadTasks(): Promise<void>;\n\n /**\n * 获取所有未完成的下载任务\n */\n getUncompletedDownloadTasks(): Promise<DownloadTask[]>;\n}\n\nexport enum DownloadState {\n QUEUED = 0,\n STOPPED = 1,\n DOWNLOADING = 2,\n COMPLETED = 3,\n FAILED = 4,\n REMOVING = 5,\n RESTARTING = 7,\n}\n\nexport interface DownloadTask {\n id: string;\n state: DownloadState;\n percentDownloaded: number;\n bytesDownloaded: number;\n contentLength: number;\n track?: Track;\n}\n\nexport const Orpheus = requireNativeModule<OrpheusModule>(\"Orpheus\");\n"]}
@@ -2,6 +2,5 @@ export * from "./useProgress";
2
2
  export * from "./usePlaybackState";
3
3
  export * from "./useIsPlaying";
4
4
  export * from "./useCurrentTrack";
5
- export * from "./useShuffleMode";
6
5
  export * from "./useOrpheus";
7
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,cAAc,CAAC"}
@@ -2,6 +2,5 @@ export * from "./useProgress";
2
2
  export * from "./usePlaybackState";
3
3
  export * from "./useIsPlaying";
4
4
  export * from "./useCurrentTrack";
5
- export * from "./useShuffleMode";
6
5
  export * from "./useOrpheus";
7
6
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC","sourcesContent":["export * from \"./useProgress\";\nexport * from \"./usePlaybackState\";\nexport * from \"./useIsPlaying\";\nexport * from \"./useCurrentTrack\";\nexport * from \"./useShuffleMode\";\nexport * from \"./useOrpheus\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,cAAc,CAAC","sourcesContent":["export * from \"./useProgress\";\nexport * from \"./usePlaybackState\";\nexport * from \"./useIsPlaying\";\nexport * from \"./useCurrentTrack\";\nexport * from \"./useOrpheus\";\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roitium/expo-orpheus",
3
- "version": "0.4.7",
3
+ "version": "0.5.1",
4
4
  "description": "A player for bbplayer",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -54,6 +54,7 @@ export type OrpheusEvents = {
54
54
  declare class OrpheusModule extends NativeModule<OrpheusEvents> {
55
55
 
56
56
  restorePlaybackPositionEnabled: boolean;
57
+ loudnessNormalizationEnabled: boolean;
57
58
 
58
59
  /**
59
60
  * 获取当前进度(秒)
@@ -100,6 +101,7 @@ declare class OrpheusModule extends NativeModule<OrpheusEvents> {
100
101
  setBilibiliCookie(cookie: string): void;
101
102
 
102
103
  setRestorePlaybackPositionEnabled(enabled: boolean): void;
104
+ setLoudnessNormalizationEnabled(enabled: boolean): void;
103
105
 
104
106
  play(): Promise<void>;
105
107
 
@@ -2,5 +2,4 @@ export * from "./useProgress";
2
2
  export * from "./usePlaybackState";
3
3
  export * from "./useIsPlaying";
4
4
  export * from "./useCurrentTrack";
5
- export * from "./useShuffleMode";
6
5
  export * from "./useOrpheus";
@@ -1,2 +0,0 @@
1
- export declare function useShuffleMode(): readonly [boolean, () => Promise<void>];
2
- //# sourceMappingURL=useShuffleMode.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"useShuffleMode.d.ts","sourceRoot":"","sources":["../../src/hooks/useShuffleMode.ts"],"names":[],"mappings":"AAGA,wBAAgB,cAAc,4CAsB7B"}
@@ -1,22 +0,0 @@
1
- import { useState, useEffect } from "react";
2
- import { Orpheus } from "../ExpoOrpheusModule";
3
- export function useShuffleMode() {
4
- const [shuffleMode, setShuffleMode] = useState(false);
5
- const refresh = async () => {
6
- const val = await Orpheus.getShuffleMode();
7
- setShuffleMode(val);
8
- };
9
- useEffect(() => {
10
- refresh();
11
- const sub = Orpheus.addListener("onTrackStarted", refresh);
12
- return () => sub.remove();
13
- }, []);
14
- const toggleShuffle = async () => {
15
- const newVal = !shuffleMode;
16
- setShuffleMode(newVal);
17
- await Orpheus.setShuffleMode(newVal);
18
- refresh();
19
- };
20
- return [shuffleMode, toggleShuffle];
21
- }
22
- //# sourceMappingURL=useShuffleMode.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"useShuffleMode.js","sourceRoot":"","sources":["../../src/hooks/useShuffleMode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAE/C,MAAM,UAAU,cAAc;IAC5B,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAEtD,MAAM,OAAO,GAAG,KAAK,IAAI,EAAE;QACzB,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,cAAc,EAAE,CAAC;QAC3C,cAAc,CAAC,GAAG,CAAC,CAAC;IACtB,CAAC,CAAC;IAEF,SAAS,CAAC,GAAG,EAAE;QACb,OAAO,EAAE,CAAC;QACV,MAAM,GAAG,GAAG,OAAO,CAAC,WAAW,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAC;QAC3D,OAAO,GAAG,EAAE,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC;IAC5B,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,aAAa,GAAG,KAAK,IAAI,EAAE;QAC/B,MAAM,MAAM,GAAG,CAAC,WAAW,CAAC;QAC5B,cAAc,CAAC,MAAM,CAAC,CAAC;QACvB,MAAM,OAAO,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;QACrC,OAAO,EAAE,CAAC;IACZ,CAAC,CAAC;IAEF,OAAO,CAAC,WAAW,EAAE,aAAa,CAAU,CAAC;AAC/C,CAAC","sourcesContent":["import { useState, useEffect } from \"react\";\nimport { Orpheus } from \"../ExpoOrpheusModule\";\n\nexport function useShuffleMode() {\n const [shuffleMode, setShuffleMode] = useState(false);\n\n const refresh = async () => {\n const val = await Orpheus.getShuffleMode();\n setShuffleMode(val);\n };\n\n useEffect(() => {\n refresh();\n const sub = Orpheus.addListener(\"onTrackStarted\", refresh);\n return () => sub.remove();\n }, []);\n\n const toggleShuffle = async () => {\n const newVal = !shuffleMode;\n setShuffleMode(newVal);\n await Orpheus.setShuffleMode(newVal);\n refresh();\n };\n\n return [shuffleMode, toggleShuffle] as const;\n}\n"]}
@@ -1,26 +0,0 @@
1
- import { useState, useEffect } from "react";
2
- import { Orpheus } from "../ExpoOrpheusModule";
3
-
4
- export function useShuffleMode() {
5
- const [shuffleMode, setShuffleMode] = useState(false);
6
-
7
- const refresh = async () => {
8
- const val = await Orpheus.getShuffleMode();
9
- setShuffleMode(val);
10
- };
11
-
12
- useEffect(() => {
13
- refresh();
14
- const sub = Orpheus.addListener("onTrackStarted", refresh);
15
- return () => sub.remove();
16
- }, []);
17
-
18
- const toggleShuffle = async () => {
19
- const newVal = !shuffleMode;
20
- setShuffleMode(newVal);
21
- await Orpheus.setShuffleMode(newVal);
22
- refresh();
23
- };
24
-
25
- return [shuffleMode, toggleShuffle] as const;
26
- }