@roitium/expo-orpheus 0.7.2 → 0.9.0

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 (27) hide show
  1. package/README.md +11 -0
  2. package/android/build.gradle +1 -0
  3. package/android/src/main/AndroidManifest.xml +2 -0
  4. package/android/src/main/java/expo/modules/orpheus/ExpoOrpheusModule.kt +47 -0
  5. package/android/src/main/java/expo/modules/orpheus/FloatingLyricsManager.kt +424 -0
  6. package/android/src/main/java/expo/modules/orpheus/OrpheusHeadlessTaskService.kt +23 -0
  7. package/android/src/main/java/expo/modules/orpheus/OrpheusMusicService.kt +55 -5
  8. package/android/src/main/java/expo/modules/orpheus/models/LyricsModels.kt +12 -0
  9. package/android/src/main/java/expo/modules/orpheus/utils/GeneralStorage.kt +8 -0
  10. package/android/src/main/res/drawable/outline_close_24.xml +5 -0
  11. package/android/src/main/res/drawable/outline_lock_24.xml +5 -0
  12. package/android/src/main/res/drawable/outline_pause_24.xml +5 -0
  13. package/android/src/main/res/drawable/outline_play_arrow_24.xml +5 -0
  14. package/android/src/main/res/drawable/outline_skip_next_24.xml +5 -0
  15. package/android/src/main/res/drawable/outline_skip_previous_24.xml +5 -0
  16. package/android/src/main/res/values/strings.xml +7 -0
  17. package/bilibili--BV1DL4y1V7xH--584235509.json +1 -0
  18. package/build/ExpoOrpheusModule.d.ts +8 -0
  19. package/build/ExpoOrpheusModule.d.ts.map +1 -1
  20. package/build/ExpoOrpheusModule.js.map +1 -1
  21. package/build/index.d.ts +7 -0
  22. package/build/index.d.ts.map +1 -1
  23. package/build/index.js +5 -0
  24. package/build/index.js.map +1 -1
  25. package/package.json +1 -1
  26. package/src/ExpoOrpheusModule.ts +9 -0
  27. package/src/index.ts +17 -0
package/README.md CHANGED
@@ -25,6 +25,17 @@ Orpheus 集成了 Media3 的 DownloadManager,抛弃了原先 BBPlayer 中繁
25
25
 
26
26
  默认启用,只对未缓存的 b 站音频生效
27
27
 
28
+ ## 桌面歌词
29
+
30
+ 相信聪明的你去看一下公开方法名就知道怎么使用了!
31
+ (需要注意的是,在切歌时,会自动清空当前的歌词)
32
+
33
+ ## 注意事项
34
+
35
+ 该库一些修改比较随意,我怕后续我自己都忘了,所以在这里进行一下记录。
36
+
37
+ 1. `onTrackStarted` 事件在 v0.9.0 版本后不再存在。需要使用 `registerOrpheusHeadlessTask` 注册事件并自行判断事件是否为 `onTrackStarted`
38
+
28
39
  ## 使用
29
40
 
30
41
  虽然该包是公开的,但仍然主要供 BBPlayer 内部使用。可能不会有完整的文档覆盖。我们欢迎你 fork 后自行修改使用。
@@ -51,4 +51,5 @@ dependencies {
51
51
  implementation "com.squareup.retrofit2:converter-gson:2.9.0"
52
52
  implementation 'com.tencent:mmkv:2.3.0'
53
53
  implementation "com.github.bumptech.glide:glide:5.0.5"
54
+ compileOnly "com.facebook.react:react-android"
54
55
  }
@@ -6,6 +6,7 @@
6
6
  <uses-permission android:name="android.permission.WAKE_LOCK" />
7
7
  <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
8
8
  <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
9
+ <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
9
10
 
10
11
  <uses-permission android:name="android.permission.INTERNET" />
11
12
 
@@ -30,5 +31,6 @@
30
31
  <category android:name="android.intent.category.DEFAULT" />
31
32
  </intent-filter>
32
33
  </service>
34
+ <service android:name=".OrpheusHeadlessTaskService" />
33
35
  </application>
34
36
  </manifest>
@@ -230,6 +230,15 @@ class ExpoOrpheusModule : Module() {
230
230
  GeneralStorage.isAutoplayOnStartEnabled()
231
231
  }
232
232
 
233
+ Constant("isDesktopLyricsShown") {
234
+ GeneralStorage.isDesktopLyricsShown()
235
+ }
236
+
237
+ Constant("isDesktopLyricsLocked") {
238
+ GeneralStorage.isDesktopLyricsLocked()
239
+ }
240
+
241
+
233
242
  Function("setAutoplayOnStartEnabled") { enabled: Boolean ->
234
243
  GeneralStorage.setAutoplayOnStartEnabled(enabled)
235
244
  }
@@ -592,6 +601,44 @@ class ExpoOrpheusModule : Module() {
592
601
  }
593
602
  return@AsyncFunction result
594
603
  }
604
+
605
+ AsyncFunction("checkOverlayPermission") {
606
+ val context = appContext.reactContext ?: return@AsyncFunction false
607
+ android.provider.Settings.canDrawOverlays(context)
608
+ }.runOnQueue(Queues.MAIN)
609
+
610
+ AsyncFunction("requestOverlayPermission") {
611
+ val context = appContext.reactContext ?: return@AsyncFunction false
612
+ if (!android.provider.Settings.canDrawOverlays(context)) {
613
+ val intent = android.content.Intent(
614
+ android.provider.Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
615
+ "package:${context.packageName}".toUri()
616
+ )
617
+ intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
618
+ context.startActivity(intent)
619
+ }
620
+ }.runOnQueue(Queues.MAIN)
621
+
622
+ AsyncFunction("showDesktopLyrics") {
623
+ OrpheusMusicService.instance?.floatingLyricsManager?.show()
624
+ }.runOnQueue(Queues.MAIN)
625
+
626
+ AsyncFunction("hideDesktopLyrics") {
627
+ OrpheusMusicService.instance?.floatingLyricsManager?.hide()
628
+ }.runOnQueue(Queues.MAIN)
629
+
630
+ AsyncFunction("setDesktopLyrics") { lyricsJson: String ->
631
+ try {
632
+ val data = gson.fromJson(lyricsJson, expo.modules.orpheus.models.LyricsData::class.java)
633
+ OrpheusMusicService.instance?.floatingLyricsManager?.setLyrics(data.lyrics, data.offset)
634
+ } catch (e: Exception) {
635
+ e.printStackTrace()
636
+ }
637
+ }.runOnQueue(Queues.MAIN)
638
+
639
+ AsyncFunction("setDesktopLyricsLocked") { locked: Boolean ->
640
+ OrpheusMusicService.instance?.floatingLyricsManager?.setLocked(locked)
641
+ }.runOnQueue(Queues.MAIN)
595
642
  }
596
643
 
597
644
  private fun getDownloadMap(download: Download): Map<String, Any> {
@@ -0,0 +1,424 @@
1
+ package expo.modules.orpheus
2
+
3
+ import android.content.Context
4
+ import android.graphics.Color
5
+ import android.graphics.PixelFormat
6
+ import android.graphics.drawable.GradientDrawable
7
+ import android.os.Build
8
+ import android.os.Handler
9
+ import android.os.Looper
10
+ import android.view.ContextThemeWrapper
11
+ import android.view.Gravity
12
+ import android.view.MotionEvent
13
+ import android.view.View
14
+ import android.view.WindowManager
15
+ import android.widget.FrameLayout
16
+ import android.widget.HorizontalScrollView
17
+ import android.widget.ImageButton
18
+ import android.widget.ImageView
19
+ import android.widget.LinearLayout
20
+ import android.widget.SeekBar
21
+ import android.widget.TextView
22
+ import androidx.core.graphics.toColorInt
23
+ import androidx.media3.common.Player
24
+ import androidx.media3.exoplayer.ExoPlayer
25
+ import expo.modules.orpheus.models.LyricsLine
26
+ import expo.modules.orpheus.utils.GeneralStorage
27
+ import kotlin.math.abs
28
+
29
+ class FloatingLyricsManager(context: Context, private val player: ExoPlayer?) {
30
+
31
+ private val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
32
+ private var floatingView: FrameLayout? = null
33
+ private var lyricsTextView: TextView? = null
34
+ private var settingsPanel: LinearLayout? = null
35
+ private var playPauseButton: ImageButton? = null
36
+ private var params: WindowManager.LayoutParams? = null
37
+
38
+ private val uiContext = ContextThemeWrapper(context, android.R.style.Theme_DeviceDefault)
39
+
40
+ private var lyrics: List<LyricsLine> = emptyList()
41
+ private var offset: Double = 0.0
42
+ private var currentLineIndex = -1
43
+
44
+ private var textSize = 18f
45
+ private var textColor = "#FFFFFF".toColorInt()
46
+ private var isLocked = false
47
+
48
+ private val colors = listOf(
49
+ "#FFFFFF", "#CCCCCC",
50
+ "#FF0000", "#FFC107",
51
+ "#2196F3", "#9C27B0",
52
+ "#000000", "#4CAF50"
53
+ )
54
+
55
+ private val playerListener = object : Player.Listener {
56
+ override fun onIsPlayingChanged(isPlaying: Boolean) {
57
+ updatePlayPauseButton(isPlaying)
58
+ }
59
+ }
60
+
61
+ init {
62
+ isLocked = GeneralStorage.isDesktopLyricsLocked()
63
+ }
64
+
65
+ fun show() {
66
+ if (floatingView != null) return
67
+
68
+ val type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
69
+ WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
70
+ } else {
71
+ WindowManager.LayoutParams.TYPE_PHONE
72
+ }
73
+
74
+ params = WindowManager.LayoutParams(
75
+ WindowManager.LayoutParams.MATCH_PARENT,
76
+ WindowManager.LayoutParams.WRAP_CONTENT,
77
+ type,
78
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
79
+ WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or
80
+ WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
81
+ PixelFormat.TRANSLUCENT
82
+ )
83
+ params?.gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
84
+ params?.y = 200
85
+
86
+ createView()
87
+
88
+ try {
89
+ windowManager.addView(floatingView, params)
90
+ player?.addListener(playerListener)
91
+ updatePlayPauseButton(player?.isPlaying == true)
92
+ GeneralStorage.setDesktopLyricsShown(true)
93
+ } catch (e: Exception) {
94
+ e.printStackTrace()
95
+ }
96
+ }
97
+
98
+ fun hide() {
99
+ floatingView?.let {
100
+ try {
101
+ windowManager.removeView(it)
102
+ } catch (e: Exception) {
103
+ e.printStackTrace()
104
+ }
105
+ player?.removeListener(playerListener)
106
+ floatingView = null
107
+ lyricsTextView = null
108
+ settingsPanel = null
109
+ playPauseButton = null
110
+ GeneralStorage.setDesktopLyricsShown(false)
111
+ }
112
+ }
113
+
114
+ fun setLyrics(newLyrics: List<LyricsLine>, newOffset: Double = 0.0) {
115
+ lyrics = newLyrics.sortedBy { it.timestamp }
116
+ offset = newOffset
117
+ currentLineIndex = -1
118
+ updateText(null)
119
+ if (lyrics.isEmpty()) {
120
+ Handler(Looper.getMainLooper()).post {
121
+ settingsPanel?.visibility = View.GONE
122
+ }
123
+ }
124
+ }
125
+
126
+ fun updateTime(seconds: Double) {
127
+ if (floatingView == null || lyrics.isEmpty()) return
128
+
129
+ val adjustedTime = seconds - offset
130
+ val index = lyrics.indexOfLast { it.timestamp <= adjustedTime }
131
+ if (index != currentLineIndex && index >= 0) {
132
+ currentLineIndex = index
133
+ updateText(lyrics[index])
134
+ }
135
+ }
136
+
137
+ fun setLocked(locked: Boolean) {
138
+ isLocked = locked
139
+ GeneralStorage.setDesktopLyricsLocked(locked)
140
+ updateTouchableFlags()
141
+ if (locked) {
142
+ settingsPanel?.visibility = View.GONE
143
+ }
144
+ }
145
+
146
+ private fun updateTouchableFlags() {
147
+ val p = params ?: return
148
+ if (isLocked) {
149
+ p.flags = p.flags or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
150
+ } else {
151
+ p.flags = p.flags and WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE.inv()
152
+ }
153
+ try {
154
+ if (floatingView?.isAttachedToWindow == true) {
155
+ windowManager.updateViewLayout(floatingView, p)
156
+ }
157
+ } catch (e: Exception) {
158
+ e.printStackTrace()
159
+ }
160
+ }
161
+
162
+ private fun updateText(line: LyricsLine?) {
163
+ Handler(Looper.getMainLooper()).post {
164
+ if (line == null) {
165
+ lyricsTextView?.text = ""
166
+ } else {
167
+ val text = if (line.translation.isNullOrEmpty()) {
168
+ line.text
169
+ } else {
170
+ "${line.text}\n${line.translation}"
171
+ }
172
+ lyricsTextView?.text = text
173
+ }
174
+ }
175
+ }
176
+
177
+ private fun updatePlayPauseButton(isPlaying: Boolean) {
178
+ Handler(Looper.getMainLooper()).post {
179
+ if (isPlaying) {
180
+ playPauseButton?.setImageResource(R.drawable.outline_pause_24)
181
+ } else {
182
+ playPauseButton?.setImageResource(R.drawable.outline_play_arrow_24)
183
+ }
184
+ }
185
+ }
186
+
187
+ private fun createView() {
188
+ val frame = FrameLayout(uiContext)
189
+
190
+ val contentContainer = LinearLayout(uiContext).apply {
191
+ orientation = LinearLayout.VERTICAL
192
+ gravity = Gravity.CENTER_HORIZONTAL
193
+ }
194
+
195
+ // Lyrics View
196
+ lyricsTextView = TextView(uiContext).apply {
197
+ text = context.getString(R.string.desktop_lyrics)
198
+ textSize = this@FloatingLyricsManager.textSize
199
+ setTextColor(this@FloatingLyricsManager.textColor)
200
+ setShadowLayer(4f, 0f, 0f, Color.BLACK)
201
+ gravity = Gravity.CENTER
202
+ setPadding(20, 10, 20, 10)
203
+
204
+ setOnClickListener {
205
+ toggleSettings()
206
+ }
207
+ }
208
+
209
+ // Settings Panel
210
+ settingsPanel = createSettingsPanel()
211
+ settingsPanel?.visibility = View.GONE
212
+
213
+ contentContainer.addView(lyricsTextView, LinearLayout.LayoutParams(
214
+ LinearLayout.LayoutParams.WRAP_CONTENT,
215
+ LinearLayout.LayoutParams.WRAP_CONTENT
216
+ ).apply {
217
+ bottomMargin = 10
218
+ })
219
+
220
+ contentContainer.addView(settingsPanel, LinearLayout.LayoutParams(
221
+ LinearLayout.LayoutParams.WRAP_CONTENT,
222
+ LinearLayout.LayoutParams.WRAP_CONTENT
223
+ ))
224
+
225
+ frame.addView(contentContainer, FrameLayout.LayoutParams(
226
+ FrameLayout.LayoutParams.WRAP_CONTENT,
227
+ FrameLayout.LayoutParams.WRAP_CONTENT,
228
+ Gravity.CENTER_HORIZONTAL
229
+ ))
230
+
231
+ var initialY = 0
232
+ var initialTouchY = 0f
233
+ var isClick = false
234
+ val touchSlop = 10
235
+
236
+ lyricsTextView?.setOnTouchListener {
237
+ v, event ->
238
+ if (isLocked) return@setOnTouchListener false
239
+
240
+ when (event.action) {
241
+ MotionEvent.ACTION_DOWN -> {
242
+ initialY = params?.y ?: 0
243
+ initialTouchY = event.rawY
244
+ isClick = true
245
+ true
246
+ }
247
+ MotionEvent.ACTION_MOVE -> {
248
+ val dy = (event.rawY - initialTouchY).toInt()
249
+ if (abs(dy) > touchSlop) {
250
+ isClick = false
251
+ params?.y = initialY + dy
252
+ try {
253
+ windowManager.updateViewLayout(floatingView, params)
254
+ } catch (e: Exception) {
255
+ e.printStackTrace()
256
+ }
257
+ }
258
+ true
259
+ }
260
+ MotionEvent.ACTION_UP -> {
261
+ if (isClick) {
262
+ v.performClick()
263
+ }
264
+ true
265
+ }
266
+ else -> false
267
+ }
268
+ }
269
+
270
+ floatingView = frame
271
+ }
272
+
273
+ private fun createSettingsPanel(): LinearLayout {
274
+ val panel = LinearLayout(uiContext).apply {
275
+ orientation = LinearLayout.VERTICAL
276
+ val gd = GradientDrawable()
277
+ gd.setColor("#DD1A1A1A".toColorInt())
278
+ gd.cornerRadius = 32f
279
+ background = gd
280
+ setPadding(32, 24, 32, 24)
281
+ gravity = Gravity.CENTER_HORIZONTAL
282
+ }
283
+
284
+ // Row 1: Playback Controls
285
+ val controlsRow = LinearLayout(uiContext).apply {
286
+ orientation = LinearLayout.HORIZONTAL
287
+ gravity = Gravity.CENTER
288
+ setPadding(0, 0, 0, 24)
289
+ }
290
+
291
+ val prevBtn = createControlButton(R.drawable.outline_skip_previous_24) { player?.seekToPreviousMediaItem() }
292
+ playPauseButton = createControlButton(R.drawable.outline_play_arrow_24) {
293
+ if (player?.isPlaying == true) player.pause() else player?.play()
294
+ }.apply { textSize = 28f }
295
+ val nextBtn = createControlButton(R.drawable.outline_skip_next_24) { player?.seekToNextMediaItem() }
296
+
297
+ controlsRow.addView(prevBtn)
298
+ controlsRow.addView(View(uiContext), LinearLayout.LayoutParams(40, 1)) // Spacer
299
+ controlsRow.addView(playPauseButton)
300
+ controlsRow.addView(View(uiContext), LinearLayout.LayoutParams(40, 1)) // Spacer
301
+ controlsRow.addView(nextBtn)
302
+
303
+ // Row 2: Size Slider
304
+ val sizeRow = LinearLayout(uiContext).apply {
305
+ orientation = LinearLayout.HORIZONTAL
306
+ gravity = Gravity.CENTER_VERTICAL
307
+ setPadding(0, 0, 0, 16)
308
+ }
309
+ val sizeLabel = TextView(uiContext).apply {
310
+ text = context.getString(R.string.size)
311
+ setTextColor(Color.LTGRAY)
312
+ textSize = 12f
313
+ }
314
+ val sizeSeekBar = SeekBar(uiContext).apply {
315
+ max = 30
316
+ progress = (textSize - 10).toInt()
317
+ setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
318
+ override fun onProgressChanged(p0: SeekBar?, p1: Int, p2: Boolean) {
319
+ textSize = (p1 + 10).toFloat()
320
+ lyricsTextView?.textSize = textSize
321
+ }
322
+ override fun onStartTrackingTouch(p0: SeekBar?) {}
323
+ override fun onStopTrackingTouch(p0: SeekBar?) {}
324
+ })
325
+ }
326
+ sizeRow.addView(sizeLabel)
327
+ sizeRow.addView(sizeSeekBar, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f).apply {
328
+ marginStart = 16
329
+ })
330
+
331
+ // Row 3: Colors
332
+ val colorsScroll = HorizontalScrollView(uiContext).apply {
333
+ isHorizontalScrollBarEnabled = false
334
+ overScrollMode = View.OVER_SCROLL_NEVER
335
+ setPadding(0, 0, 0, 24)
336
+ }
337
+ val colorContainer = LinearLayout(uiContext).apply {
338
+ orientation = LinearLayout.HORIZONTAL
339
+ gravity = Gravity.CENTER_VERTICAL
340
+ }
341
+
342
+ colors.forEach { colorString ->
343
+ val color = colorString.toColorInt()
344
+ val colorView = View(uiContext).apply {
345
+ val size = 60
346
+ layoutParams = LinearLayout.LayoutParams(size, size).apply {
347
+ marginEnd = 16
348
+ }
349
+ val circle = GradientDrawable()
350
+ circle.shape = GradientDrawable.OVAL
351
+ circle.setColor(color)
352
+ circle.setStroke(2, Color.DKGRAY)
353
+ background = circle
354
+
355
+ setOnClickListener {
356
+ textColor = color
357
+ lyricsTextView?.setTextColor(textColor)
358
+ }
359
+ }
360
+ colorContainer.addView(colorView)
361
+ }
362
+ colorsScroll.addView(colorContainer)
363
+
364
+ // Row 4: Actions (Lock & Close)
365
+ val actionsRow = LinearLayout(uiContext).apply {
366
+ orientation = LinearLayout.HORIZONTAL
367
+ gravity = Gravity.CENTER
368
+ }
369
+
370
+ val lockBtn = createActionButton(R.string.lock, R.drawable.outline_lock_24) { setLocked(true) }
371
+ val closeBtn = createActionButton(R.string.close, R.drawable.outline_close_24) { settingsPanel?.visibility = View.GONE }
372
+
373
+ actionsRow.addView(lockBtn)
374
+ actionsRow.addView(View(uiContext), LinearLayout.LayoutParams(32, 1)) // Spacer
375
+ actionsRow.addView(closeBtn)
376
+
377
+ panel.addView(controlsRow)
378
+ panel.addView(sizeRow, LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT))
379
+ panel.addView(colorsScroll)
380
+ panel.addView(actionsRow)
381
+
382
+ return panel
383
+ }
384
+
385
+ private fun createControlButton(resId: Int, onClick: () -> Unit): ImageButton {
386
+ return ImageButton(uiContext).apply {
387
+ setImageResource(resId)
388
+ setBackgroundColor(Color.TRANSPARENT)
389
+ setColorFilter(Color.WHITE)
390
+ scaleType = ImageView.ScaleType.FIT_CENTER
391
+ setPadding(16, 16, 16, 16)
392
+ setOnClickListener { onClick() }
393
+ }
394
+ }
395
+
396
+ private fun createActionButton(textId: Int, iconId: Int, onClick: () -> Unit): TextView {
397
+ return TextView(uiContext).apply {
398
+ text = context.getString(textId)
399
+ textSize = 14f
400
+ setTextColor(Color.WHITE)
401
+ gravity = Gravity.CENTER
402
+ setPadding(32, 16, 32, 16)
403
+
404
+ setCompoundDrawablesWithIntrinsicBounds(iconId, 0, 0, 0)
405
+
406
+ compoundDrawablePadding = 12
407
+
408
+ background = GradientDrawable().apply {
409
+ setColor("#33FFFFFF".toColorInt())
410
+ cornerRadius = 50f
411
+ }
412
+
413
+ setOnClickListener { onClick() }
414
+ }
415
+ }
416
+
417
+ private fun toggleSettings() {
418
+ if (settingsPanel?.visibility == View.VISIBLE) {
419
+ settingsPanel?.visibility = View.GONE
420
+ } else {
421
+ settingsPanel?.visibility = View.VISIBLE
422
+ }
423
+ }
424
+ }
@@ -0,0 +1,23 @@
1
+ package expo.modules.orpheus
2
+
3
+ import android.content.Intent
4
+ import com.facebook.react.HeadlessJsTaskService
5
+ import com.facebook.react.bridge.Arguments
6
+ import com.facebook.react.jstasks.HeadlessJsTaskConfig
7
+
8
+ class OrpheusHeadlessTaskService : HeadlessJsTaskService() {
9
+
10
+ override fun getTaskConfig(intent: Intent?): HeadlessJsTaskConfig? {
11
+ val extras = intent?.extras
12
+ return if (extras != null) {
13
+ HeadlessJsTaskConfig(
14
+ "OrpheusHeadlessTask",
15
+ Arguments.fromBundle(extras),
16
+ 5000, // timeout for the task
17
+ true // allowed in foreground
18
+ )
19
+ } else {
20
+ null
21
+ }
22
+ }
23
+ }
@@ -1,5 +1,6 @@
1
1
  package expo.modules.orpheus
2
2
 
3
+ import android.R
3
4
  import android.app.PendingIntent
4
5
  import android.content.Intent
5
6
  import android.util.Log
@@ -37,6 +38,19 @@ class OrpheusMusicService : MediaLibraryService() {
37
38
  private var volumeFadeJob: Job? = null
38
39
  private var scope = MainScope()
39
40
 
41
+ lateinit var floatingLyricsManager: FloatingLyricsManager
42
+ private val serviceHandler = android.os.Handler(android.os.Looper.getMainLooper())
43
+ private val lyricsUpdateRunnable = object : Runnable {
44
+ override fun run() {
45
+ player?.let {
46
+ if (it.isPlaying) {
47
+ floatingLyricsManager.updateTime(it.currentPosition / 1000.0)
48
+ }
49
+ }
50
+ serviceHandler.postDelayed(this, 200)
51
+ }
52
+ }
53
+
40
54
  companion object {
41
55
  var instance: OrpheusMusicService? = null
42
56
  private set(value) {
@@ -84,6 +98,11 @@ class OrpheusMusicService : MediaLibraryService() {
84
98
  )
85
99
  .build()
86
100
 
101
+ floatingLyricsManager = FloatingLyricsManager(this, player)
102
+ if (GeneralStorage.isDesktopLyricsShown()) {
103
+ serviceHandler.post { floatingLyricsManager.show() }
104
+ }
105
+
87
106
  setupListeners()
88
107
 
89
108
  var launchIntent = packageManager.getLaunchIntentForPackage(packageName)
@@ -124,6 +143,8 @@ class OrpheusMusicService : MediaLibraryService() {
124
143
  }
125
144
 
126
145
  override fun onDestroy() {
146
+ serviceHandler.removeCallbacks(lyricsUpdateRunnable)
147
+ floatingLyricsManager.hide()
127
148
  scope.cancel()
128
149
  instance = null
129
150
 
@@ -159,13 +180,10 @@ class OrpheusMusicService : MediaLibraryService() {
159
180
  .build()
160
181
  }
161
182
 
162
- /**
163
- * 修复 UnsupportedOperationException 的关键!
164
- * 当系统尝试恢复播放(比如从“最近播放”卡片点击)时触发。
165
- */
166
183
  override fun onPlaybackResumption(
167
184
  mediaSession: MediaSession,
168
- controller: MediaSession.ControllerInfo
185
+ controller: MediaSession.ControllerInfo,
186
+ isPlayback: Boolean
169
187
  ): ListenableFuture<MediaSession.MediaItemsWithStartPosition> {
170
188
  return Futures.immediateFuture(
171
189
  MediaSession.MediaItemsWithStartPosition(
@@ -202,16 +220,48 @@ class OrpheusMusicService : MediaLibraryService() {
202
220
 
203
221
  player.playWhenReady = GeneralStorage.isAutoplayOnStartEnabled()
204
222
  player.prepare()
223
+
224
+ // 软件冷启动时,恢复的歌曲并不会触发 onMediaTransition 事件,我们需要手动补发一个
225
+ if (player.currentMediaItem != null) {
226
+ sendTrackStartEvent(player.currentMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)
227
+ }
228
+ }
229
+ }
230
+
231
+ @OptIn(UnstableApi::class)
232
+ private fun sendTrackStartEvent(mediaItem: androidx.media3.common.MediaItem?, reason: Int) {
233
+ if (mediaItem == null) return
234
+
235
+ try {
236
+ val intent = Intent(this, OrpheusHeadlessTaskService::class.java)
237
+ intent.putExtra("eventName", "onTrackStarted")
238
+ intent.putExtra("trackId", mediaItem.mediaId)
239
+ intent.putExtra("reason", reason)
240
+ startService(intent)
241
+ } catch (e: Exception) {
242
+ e.printStackTrace()
205
243
  }
206
244
  }
207
245
 
208
246
  private fun setupListeners() {
209
247
  player?.addListener(object : Player.Listener {
248
+ override fun onIsPlayingChanged(isPlaying: Boolean) {
249
+ if (isPlaying) {
250
+ serviceHandler.removeCallbacks(lyricsUpdateRunnable)
251
+ serviceHandler.post(lyricsUpdateRunnable)
252
+ } else {
253
+ serviceHandler.removeCallbacks(lyricsUpdateRunnable)
254
+ }
255
+ }
256
+
210
257
  @OptIn(UnstableApi::class)
211
258
  override fun onMediaItemTransition(
212
259
  mediaItem: androidx.media3.common.MediaItem?,
213
260
  reason: Int
214
261
  ) {
262
+ sendTrackStartEvent(mediaItem, reason)
263
+
264
+ floatingLyricsManager.setLyrics(emptyList())
215
265
  saveCurrentQueue()
216
266
  val uri = mediaItem?.localConfiguration?.uri?.toString() ?: return
217
267
 
@@ -0,0 +1,12 @@
1
+ package expo.modules.orpheus.models
2
+
3
+ data class LyricsLine(
4
+ val timestamp: Double,
5
+ val text: String,
6
+ val translation: String? = null
7
+ )
8
+
9
+ data class LyricsData(
10
+ val lyrics: List<LyricsLine>,
11
+ val offset: Double = 0.0
12
+ )
@@ -21,6 +21,8 @@ object GeneralStorage {
21
21
  private const val KEY_SAVED_REPEAT_MODE = "saved_repeat_mode"
22
22
  private const val KEY_SAVED_SHUFFLE_MODE = "saved_shuffle_mode"
23
23
  private const val KEY_AUTOPLAY_ON_START_ENABLED = "config_autoplay_on_start_enabled"
24
+ private const val KEY_DESKTOP_LYRICS_SHOWN = "state_desktop_lyrics_shown"
25
+ private const val KEY_DESKTOP_LYRICS_LOCKED = "state_desktop_lyrics_locked"
24
26
 
25
27
 
26
28
  @Synchronized
@@ -122,4 +124,10 @@ object GeneralStorage {
122
124
  fun getSavedPosition() = kv?.decodeLong(KEY_SAVED_POSITION, 0L) ?: 0L
123
125
  fun getRepeatMode() = kv?.decodeInt(KEY_SAVED_REPEAT_MODE, 0) ?: 0
124
126
  fun getShuffleMode() = kv?.decodeBool(KEY_SAVED_SHUFFLE_MODE, false) ?: false
127
+
128
+ fun isDesktopLyricsShown() = kv?.decodeBool(KEY_DESKTOP_LYRICS_SHOWN, false) ?: false
129
+ fun setDesktopLyricsShown(shown: Boolean) = safeKv.encode(KEY_DESKTOP_LYRICS_SHOWN, shown)
130
+
131
+ fun isDesktopLyricsLocked() = kv?.decodeBool(KEY_DESKTOP_LYRICS_LOCKED, false) ?: false
132
+ fun setDesktopLyricsLocked(locked: Boolean) = safeKv.encode(KEY_DESKTOP_LYRICS_LOCKED, locked)
125
133
  }
@@ -0,0 +1,5 @@
1
+ <vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
2
+
3
+ <path android:fillColor="@android:color/white" android:pathData="M256,760L200,704L424,480L200,256L256,200L480,424L704,200L760,256L536,480L760,704L704,760L480,536L256,760Z"/>
4
+
5
+ </vector>
@@ -0,0 +1,5 @@
1
+ <vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
2
+
3
+ <path android:fillColor="@android:color/white" android:pathData="M240,880Q207,880 183.5,856.5Q160,833 160,800L160,400Q160,367 183.5,343.5Q207,320 240,320L280,320L280,240Q280,157 338.5,98.5Q397,40 480,40Q563,40 621.5,98.5Q680,157 680,240L680,320L720,320Q753,320 776.5,343.5Q800,367 800,400L800,800Q800,833 776.5,856.5Q753,880 720,880L240,880ZM240,800L720,800Q720,800 720,800Q720,800 720,800L720,400Q720,400 720,400Q720,400 720,400L240,400Q240,400 240,400Q240,400 240,400L240,800Q240,800 240,800Q240,800 240,800ZM480,680Q513,680 536.5,656.5Q560,633 560,600Q560,567 536.5,543.5Q513,520 480,520Q447,520 423.5,543.5Q400,567 400,600Q400,633 423.5,656.5Q447,680 480,680ZM360,320L600,320L600,240Q600,190 565,155Q530,120 480,120Q430,120 395,155Q360,190 360,240L360,320ZM240,800Q240,800 240,800Q240,800 240,800L240,400Q240,400 240,400Q240,400 240,400L240,400Q240,400 240,400Q240,400 240,400L240,800Q240,800 240,800Q240,800 240,800Z"/>
4
+
5
+ </vector>
@@ -0,0 +1,5 @@
1
+ <vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
2
+
3
+ <path android:fillColor="@android:color/white" android:pathData="M520,760L520,200L760,200L760,760L520,760ZM200,760L200,200L440,200L440,760L200,760ZM600,680L680,680L680,280L600,280L600,680ZM280,680L360,680L360,280L280,280L280,680ZM280,280L280,280L280,680L280,680L280,280ZM600,280L600,280L600,680L600,680L600,280Z"/>
4
+
5
+ </vector>
@@ -0,0 +1,5 @@
1
+ <vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
2
+
3
+ <path android:fillColor="@android:color/white" android:pathData="M320,760L320,200L760,480L320,760ZM400,480L400,480L400,480ZM400,614L610,480L400,346L400,614Z"/>
4
+
5
+ </vector>
@@ -0,0 +1,5 @@
1
+ <vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
2
+
3
+ <path android:fillColor="@android:color/white" android:pathData="M660,720L660,240L740,240L740,720L660,720ZM220,720L220,240L580,480L220,720ZM300,480L300,480L300,480ZM300,570L436,480L300,390L300,570Z"/>
4
+
5
+ </vector>
@@ -0,0 +1,5 @@
1
+ <vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
2
+
3
+ <path android:fillColor="@android:color/white" android:pathData="M220,720L220,240L300,240L300,720L220,720ZM740,720L380,480L740,240L740,720ZM660,480L660,480L660,480ZM660,570L660,390L524,480L660,570Z"/>
4
+
5
+ </vector>
@@ -0,0 +1,7 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <resources>
3
+ <string name="desktop_lyrics">桌面歌词</string>
4
+ <string name="size">大小</string>
5
+ <string name="lock">锁定</string>
6
+ <string name="close">关闭</string>
7
+ </resources>
@@ -0,0 +1 @@
1
+ {"tags":{"00":"51.81","01":"47.24","02":"53.70","03":"39.62"},"lyrics":[{"timestamp":0,"text":"作词 : えびかれー伯爵/知代/くりーむぱい太郎"},{"timestamp":1,"text":"作曲 : えびかれー伯爵"},{"timestamp":7.66,"text":"I can't be a superstar","translation":"我成为不了超级明星"},{"timestamp":10.66,"text":"Not happy like in the silver screen","translation":"无法像银幕里那样幸福美满"},{"timestamp":15.05,"text":"But we can get everything here","translation":"但在这里,我们能获得想要的一切"},{"timestamp":37.18,"text":"It's kinda fiction","translation":"这是一篇小说"},{"timestamp":38.56,"text":"It's a made up story","translation":"是虚构的故事"},{"timestamp":40.18,"text":"That would be too good for me, sometimes","translation":"虽然它有时也对我太过友好"},{"timestamp":44.6,"text":"But the beginning is from that point","translation":"但一切都要从那里说起"},{"timestamp":52.07,"text":"Perfect story is waiting on me who failure","translation":"完美的故事正等着失败的我书写"},{"timestamp":59.51,"text":"Even though I'm not honest man","translation":"我并非诚实之人,但也无妨"},{"timestamp":66.7,"text":"It's a breeze","translation":"这事小菜一碟"},{"timestamp":70.4,"text":"I can't be a superstar","translation":"我成为不了超级明星"},{"timestamp":73.43,"text":"Not happy like in the silver screen","translation":"无法像银幕里那样幸福美满"},{"timestamp":77.77,"text":"But we can get everything here","translation":"但在这里,我们能获得想要的一切"},{"timestamp":92.58,"text":"It's Low-key life","translation":"这是低调生活"},{"timestamp":93.94,"text":"It's so small role","translation":"属于边缘配角"},{"timestamp":95.59,"text":"Cursed this terrible doom fades me away","translation":"尽管被诅咒的可怕厄运企图将我扼杀"},{"timestamp":99.92,"text":"But it's a chance to change the life brand new","translation":"但同时它也是令生活焕然一新的良机"},{"timestamp":107.52,"text":"Perfect story is waiting on me who failure","translation":"完美的故事正等着失败的我书写"},{"timestamp":114.92,"text":"Even though I'm not honest man","translation":"我并非诚实之人,但也无妨"},{"timestamp":122.13,"text":"It's a breeze","translation":"这事小菜一碟"},{"timestamp":125.78,"text":"I can't be a superstar","translation":"我成为不了超级明星"},{"timestamp":128.76,"text":"Not happy like in the silver screen","translation":"无法像银幕里那样幸福美满"},{"timestamp":133.18,"text":"But we can get everything here","translation":"但在这里,我们能获得想要的一切"},{"timestamp":155.51,"text":"It's kinda fiction","translation":"这是一篇小说"},{"timestamp":157.39,"text":"It's a made up story","translation":"是虚构的故事"},{"timestamp":159.27,"text":"That would be too good for me, sometimes","translation":"虽然它有时也对我太过友好"},{"timestamp":162.96,"text":"But the beginning is from that point","translation":"但一切都要从那里说起"},{"timestamp":170.01,"text":"It's a breeze","translation":"这事小菜一碟"},{"timestamp":173.8,"text":"I can't be a superstar","translation":"我成为不了超级明星"},{"timestamp":176.75,"text":"Not happy like in the silver screen","translation":"无法像银幕里那样幸福美满"},{"timestamp":181.16,"text":"But we can get everything here","translation":"但在这里,我们能获得想要的一切"},{"timestamp":189.55,"text":"I can't be a superstar","translation":"我成为不了超级明星"},{"timestamp":192.45,"text":"Not happy like in the silver screen","translation":"无法像银幕里那样幸福美满"},{"timestamp":196.89,"text":"But we can get everything here","translation":"但在这里,我们能获得想要的一切"},{"timestamp":211.65,"text":"But we can get everything here","translation":"但在这里,我们能获得想要的一切"}],"rawOriginalLyrics":"[00:00.00] 作词 : えびかれー伯爵/知代/くりーむぱい太郎\n[00:01.00] 作曲 : えびかれー伯爵\n[00:07.66]I can't be a superstar\n[00:10.66]Not happy like in the silver screen\n[00:15.05]But we can get everything here\n[00:22.44]\n[00:37.18]It's kinda fiction\n[00:38.56]It's a made up story\n[00:40.18]That would be too good for me, sometimes\n[00:44.60]But the beginning is from that point\n[00:51.81]\n[00:52.07]Perfect story is waiting on me who failure\n[00:59.51]Even though I'm not honest man\n[01:06.70]It's a breeze\n[01:10.30]\n[01:10.40]I can't be a superstar\n[01:13.43]Not happy like in the silver screen\n[01:17.77]But we can get everything here\n[01:28.93]\n[01:32.58]It's Low-key life\n[01:33.94]It's so small role\n[01:35.59]Cursed this terrible doom fades me away\n[01:39.92]But it's a chance to change the life brand new\n[01:47.24]\n[01:47.52]Perfect story is waiting on me who failure\n[01:54.92]Even though I'm not honest man\n[02:02.13]It's a breeze\n[02:05.68]\n[02:05.78]I can't be a superstar\n[02:08.76]Not happy like in the silver screen\n[02:13.18]But we can get everything here\n[02:24.30]\n[02:35.51]It's kinda fiction\n[02:37.39]It's a made up story\n[02:39.27]That would be too good for me, sometimes\n[02:42.96]But the beginning is from that point\n[02:48.99]\n[02:50.01]It's a breeze\n[02:53.70]\n[02:53.80]I can't be a superstar\n[02:56.75]Not happy like in the silver screen\n[03:01.16]But we can get everything here\n[03:08.61]\n[03:09.55]I can't be a superstar\n[03:12.45]Not happy like in the silver screen\n[03:16.89]But we can get everything here\n[03:28.02]\n[03:31.65]But we can get everything here\n[03:39.62]\n","rawTranslatedLyrics":"\n[00:07.66]我成为不了超级明星\n[00:10.66]无法像银幕里那样幸福美满\n[00:15.05]但在这里,我们能获得想要的一切\n[00:22.44]\n[00:37.18]这是一篇小说\n[00:38.56]是虚构的故事\n[00:40.18]虽然它有时也对我太过友好\n[00:44.60]但一切都要从那里说起\n[00:51.81]\n[00:52.07]完美的故事正等着失败的我书写\n[00:59.51]我并非诚实之人,但也无妨\n[01:06.70]这事小菜一碟\n[01:10.30]\n[01:10.40]我成为不了超级明星\n[01:13.43]无法像银幕里那样幸福美满\n[01:17.77]但在这里,我们能获得想要的一切\n[01:28.93]\n[01:32.58]这是低调生活\n[01:33.94]属于边缘配角\n[01:35.59]尽管被诅咒的可怕厄运企图将我扼杀\n[01:39.92]但同时它也是令生活焕然一新的良机\n[01:47.24]\n[01:47.52]完美的故事正等着失败的我书写\n[01:54.92]我并非诚实之人,但也无妨\n[02:02.13]这事小菜一碟\n[02:05.68]\n[02:05.78]我成为不了超级明星\n[02:08.76]无法像银幕里那样幸福美满\n[02:13.18]但在这里,我们能获得想要的一切\n[02:24.30]\n[02:35.51]这是一篇小说\n[02:37.39]是虚构的故事\n[02:39.27]虽然它有时也对我太过友好\n[02:42.96]但一切都要从那里说起\n[02:48.99]\n[02:50.01]这事小菜一碟\n[02:53.70]\n[02:53.80]我成为不了超级明星\n[02:56.75]无法像银幕里那样幸福美满\n[03:01.16]但在这里,我们能获得想要的一切\n[03:08.61]\n[03:09.55]我成为不了超级明星\n[03:12.45]无法像银幕里那样幸福美满\n[03:16.89]但在这里,我们能获得想要的一切\n[03:28.02]\n[03:31.65]但在这里,我们能获得想要的一切\n[03:39.62]"}
@@ -59,6 +59,8 @@ declare class OrpheusModule extends NativeModule<OrpheusEvents> {
59
59
  restorePlaybackPositionEnabled: boolean;
60
60
  loudnessNormalizationEnabled: boolean;
61
61
  autoplayOnStartEnabled: boolean;
62
+ isDesktopLyricsShown: boolean;
63
+ isDesktopLyricsLocked: boolean;
62
64
  /**
63
65
  * 获取当前进度(秒)
64
66
  */
@@ -166,6 +168,12 @@ declare class OrpheusModule extends NativeModule<OrpheusEvents> {
166
168
  * 获取所有未完成的下载任务
167
169
  */
168
170
  getUncompletedDownloadTasks(): Promise<DownloadTask[]>;
171
+ checkOverlayPermission(): Promise<boolean>;
172
+ requestOverlayPermission(): Promise<void>;
173
+ showDesktopLyrics(): Promise<void>;
174
+ hideDesktopLyrics(): Promise<void>;
175
+ setDesktopLyrics(lyricsJson: string): Promise<void>;
176
+ setDesktopLyricsLocked(locked: boolean): Promise<void>;
169
177
  }
170
178
  export declare enum DownloadState {
171
179
  QUEUED = 0,
@@ -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;AA0LD,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 autoplayOnStartEnabled: 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 setAutoplayOnStartEnabled(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;AAmMD,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 autoplayOnStartEnabled: boolean;\n isDesktopLyricsShown: boolean;\n isDesktopLyricsLocked: 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 setAutoplayOnStartEnabled(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 checkOverlayPermission(): Promise<boolean>;\n requestOverlayPermission(): Promise<void>;\n showDesktopLyrics(): Promise<void>;\n hideDesktopLyrics(): Promise<void>;\n setDesktopLyrics(lyricsJson: string): Promise<void>;\n setDesktopLyricsLocked(locked: boolean): Promise<void>;\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"]}
package/build/index.d.ts CHANGED
@@ -1,3 +1,10 @@
1
1
  export * from "./ExpoOrpheusModule";
2
2
  export * from "./hooks";
3
+ export type OrpheusHeadlessTrackStartedEvent = {
4
+ eventName: "onTrackStarted";
5
+ trackId: string;
6
+ reason: number;
7
+ };
8
+ export type OrpheusHeadlessEvent = OrpheusHeadlessTrackStartedEvent;
9
+ export declare function registerOrpheusHeadlessTask(task: (event: OrpheusHeadlessEvent) => Promise<void>): void;
3
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,qBAAqB,CAAC;AACpC,cAAc,SAAS,CAAC;AAIxB,MAAM,MAAM,gCAAgC,GAAG;IAC7C,SAAS,EAAE,gBAAgB,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG,gCAAgC,CAAC;AAEpE,wBAAgB,2BAA2B,CACzC,IAAI,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,OAAO,CAAC,IAAI,CAAC,QAGrD"}
package/build/index.js CHANGED
@@ -1,3 +1,8 @@
1
+ import { AppRegistry } from "react-native";
1
2
  export * from "./ExpoOrpheusModule";
2
3
  export * from "./hooks";
4
+ const ORPHEUS_HEADLESS_TASK = "OrpheusHeadlessTask";
5
+ export function registerOrpheusHeadlessTask(task) {
6
+ AppRegistry.registerHeadlessTask(ORPHEUS_HEADLESS_TASK, () => task);
7
+ }
3
8
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,SAAS,CAAC","sourcesContent":["export * from \"./ExpoOrpheusModule\";\nexport * from \"./hooks\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3C,cAAc,qBAAqB,CAAC;AACpC,cAAc,SAAS,CAAC;AAExB,MAAM,qBAAqB,GAAG,qBAAqB,CAAC;AAUpD,MAAM,UAAU,2BAA2B,CACzC,IAAoD;IAEpD,WAAW,CAAC,oBAAoB,CAAC,qBAAqB,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;AACtE,CAAC","sourcesContent":["import { AppRegistry } from \"react-native\";\nexport * from \"./ExpoOrpheusModule\";\nexport * from \"./hooks\";\n\nconst ORPHEUS_HEADLESS_TASK = \"OrpheusHeadlessTask\";\n\nexport type OrpheusHeadlessTrackStartedEvent = {\n eventName: \"onTrackStarted\";\n trackId: string;\n reason: number;\n};\n\nexport type OrpheusHeadlessEvent = OrpheusHeadlessTrackStartedEvent;\n\nexport function registerOrpheusHeadlessTask(\n task: (event: OrpheusHeadlessEvent) => Promise<void>\n) {\n AppRegistry.registerHeadlessTask(ORPHEUS_HEADLESS_TASK, () => task);\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roitium/expo-orpheus",
3
- "version": "0.7.2",
3
+ "version": "0.9.0",
4
4
  "description": "A player for bbplayer",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -56,6 +56,8 @@ declare class OrpheusModule extends NativeModule<OrpheusEvents> {
56
56
  restorePlaybackPositionEnabled: boolean;
57
57
  loudnessNormalizationEnabled: boolean;
58
58
  autoplayOnStartEnabled: boolean;
59
+ isDesktopLyricsShown: boolean;
60
+ isDesktopLyricsLocked: boolean;
59
61
 
60
62
  /**
61
63
  * 获取当前进度(秒)
@@ -202,6 +204,13 @@ declare class OrpheusModule extends NativeModule<OrpheusEvents> {
202
204
  * 获取所有未完成的下载任务
203
205
  */
204
206
  getUncompletedDownloadTasks(): Promise<DownloadTask[]>;
207
+
208
+ checkOverlayPermission(): Promise<boolean>;
209
+ requestOverlayPermission(): Promise<void>;
210
+ showDesktopLyrics(): Promise<void>;
211
+ hideDesktopLyrics(): Promise<void>;
212
+ setDesktopLyrics(lyricsJson: string): Promise<void>;
213
+ setDesktopLyricsLocked(locked: boolean): Promise<void>;
205
214
  }
206
215
 
207
216
  export enum DownloadState {
package/src/index.ts CHANGED
@@ -1,2 +1,19 @@
1
+ import { AppRegistry } from "react-native";
1
2
  export * from "./ExpoOrpheusModule";
2
3
  export * from "./hooks";
4
+
5
+ const ORPHEUS_HEADLESS_TASK = "OrpheusHeadlessTask";
6
+
7
+ export type OrpheusHeadlessTrackStartedEvent = {
8
+ eventName: "onTrackStarted";
9
+ trackId: string;
10
+ reason: number;
11
+ };
12
+
13
+ export type OrpheusHeadlessEvent = OrpheusHeadlessTrackStartedEvent;
14
+
15
+ export function registerOrpheusHeadlessTask(
16
+ task: (event: OrpheusHeadlessEvent) => Promise<void>
17
+ ) {
18
+ AppRegistry.registerHeadlessTask(ORPHEUS_HEADLESS_TASK, () => task);
19
+ }