refactor: restructure src into player/views/ui subfolders; fix notification skip buttons

- Move player.js + nowplaying.js → src/player/
- Move views.js + spotify/soundcloud/youtube/abs → src/views/
- Move ui.js + components.js → src/ui/
- Update all import paths (static + lazy) across every file
- MusicService: wrap ExoPlayer in ForwardingPlayer that always advertises
  next/prev commands so the notification skip buttons stay visible in radio
  mode even when the JS queue has only one native track

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-06-06 13:12:27 +02:00
1 parent d8908c69e9
commit 308a27d293
11 files changed
+78 -54

No files matched your search

@@ -6,6 +6,7 @@ import android.os.Handler
import android.os.Looper
import androidx.media3.common.AudioAttributes
import androidx.media3.common.C
import androidx.media3.common.ForwardingPlayer
import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
@@ -97,6 +98,28 @@ class MusicService : MediaSessionService() {
.setWakeMode(C.WAKE_MODE_NETWORK) // holds WakeLock + WifiLock through Doze
.build()
// ForwardingPlayer that always reports next/prev as available commands.
// ExoPlayer drops those commands when its queue has only one item (single-track model),
// which makes Media3 hide the skip buttons in the notification. JS still owns queue
// logic — the MediaSession callback intercepts the commands and delegates to JS.
val sessionPlayer = object : ForwardingPlayer(player) {
override fun getAvailableCommands(): Player.Commands =
super.getAvailableCommands().buildUpon()
.add(Player.COMMAND_SEEK_TO_NEXT)
.add(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
.add(Player.COMMAND_SEEK_TO_PREVIOUS)
.add(Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
.build()
override fun isCommandAvailable(command: Int): Boolean = when (command) {
Player.COMMAND_SEEK_TO_NEXT,
Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM,
Player.COMMAND_SEEK_TO_PREVIOUS,
Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM -> true
else -> super.isCommandAvailable(command)
}
}
// Tapping the notification opens the app directly to the Now Playing screen.
val notifTapIntent = Intent(this, MainActivity::class.java).apply {
putExtra("open_now_playing", true)
@@ -107,7 +130,7 @@ class MusicService : MediaSessionService() {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
mediaSession = MediaSession.Builder(this, player)
mediaSession = MediaSession.Builder(this, sessionPlayer)
.setBitmapLoader(bitmapLoader)
.setSessionActivity(notifTapPending)
.setCallback(object : MediaSession.Callback {
@@ -148,6 +171,7 @@ class MusicService : MediaSessionService() {
// No play() — wait for JS to decide whether to auto-play
}
} catch (_: Exception) {}
}
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo) = mediaSession