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:
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
|
||||
|
||||
+8
-8
@@ -10,18 +10,18 @@ import {
|
||||
toggleRadioMode, starCurrent, downloadCurrent, renderQueue, playPreview,
|
||||
toggleStar, dlSongLocal, _applyRadioBtn, _applyShuffleRepeat, _restoreLastPlayed, setPlayIcon,
|
||||
npToggleStar, updateMediaSession, updateNpColorMode, reconcileNativeState,
|
||||
} from './player.js';
|
||||
} from './player/player.js';
|
||||
import {
|
||||
navigate, _navGoBack, _navLoad, onHeaderSearch, closeHeaderSearch,
|
||||
onGlobalSearch, s_goArtist, s_goAlbum, playerBarGoArtist, playerBarGoAlbum,
|
||||
searchSimilarArtist, searchAndPreview, loadItunesAlbum, expandArtistBio,
|
||||
loadAlbums, loadStarred, loadRandom, filterStarred, filterPlaylist,
|
||||
plReorderEnter, plReorderSave, plReorderCancel, toggleAllGenres,
|
||||
} from './views.js';
|
||||
} from './views/views.js';
|
||||
import { loadSpotifyImporter, resolveSpotifyLink, clearSpotifyPreview, spotifyDlAll,
|
||||
onImporterInput, resolveImporterLink } from './spotify.js';
|
||||
import { resolveYouTubeLink, executeYouTubeDownload } from './youtube.js';
|
||||
import { loadSoundCloudImporter, resolveSoundCloudLink, clearSoundCloudPreview, scDlAll, scDlSingle, scDlTrack } from './soundcloud.js';
|
||||
onImporterInput, resolveImporterLink } from './views/spotify.js';
|
||||
import { resolveYouTubeLink, executeYouTubeDownload } from './views/youtube.js';
|
||||
import { loadSoundCloudImporter, resolveSoundCloudLink, clearSoundCloudPreview, scDlAll, scDlSingle, scDlTrack } from './views/soundcloud.js';
|
||||
import {
|
||||
absInitFromStorage, absOpenSettings, absConnectSettings, absSetLibrary, absDisconnect, absVerifyApiKey, absToggleConnection,
|
||||
absPlayItem, absTogglePlay, absSeek, absSkipSecs, absNextChapter, absPrevChapter,
|
||||
@@ -29,11 +29,11 @@ import {
|
||||
absToggleSpeedPicker, absToggleSleepPicker, absSeekToChapter, absProgDown, absProgMove,
|
||||
absProgUp, loadAbsBooks, absOpenBook, loadAbsBookDetail, absPlayChapter,
|
||||
absLoadSettingsLibraries, absToggleChapterTrack, absInterrupt,
|
||||
} from './abs.js';
|
||||
} from './views/abs.js';
|
||||
import {
|
||||
openNowPlaying, closeNowPlaying, switchNpTab, npGoArtist, npGoAlbum,
|
||||
openNpMenu, closeNpMenu,
|
||||
} from './nowplaying.js';
|
||||
} from './player/nowplaying.js';
|
||||
import {
|
||||
openSettings, closeSettings, saveSettings, confirmDisconnect,
|
||||
checkBackend, openSidebar, closeSidebar, switchNavMode, showCtxMenu,
|
||||
@@ -48,7 +48,7 @@ import {
|
||||
toggleInfoEdit, onInfoArtFile, autofillMusicBrainz, autofillItunes, saveMetadata, deleteSong,
|
||||
addRomajiOverrideUI, removeRomajiOverrideUI,
|
||||
openRomajiOverridesModal, closeRomajiOverridesModal,
|
||||
} from './ui.js';
|
||||
} from './ui/ui.js';
|
||||
|
||||
// ─── CONNECT ───
|
||||
async function doConnect() {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { S } from './state.js';
|
||||
import { esc } from './utils.js';
|
||||
import { coverUrl } from './api.js';
|
||||
import { S } from '../state.js';
|
||||
import { esc } from '../utils.js';
|
||||
import { coverUrl } from '../api.js';
|
||||
// Circular imports — safe (only used inside function bodies)
|
||||
import { playCurrent, _syncNpDirect, dlSongLocal, npToggleStar, _syncOverlayState, isPlaying, seekToMs, updatePlayerUI } from './player.js';
|
||||
import { navigate } from './views.js';
|
||||
import { openPlModal, showSongInfo } from './ui.js';
|
||||
import { navigate } from '../views/views.js';
|
||||
import { openPlModal, showSongInfo } from '../ui/ui.js';
|
||||
|
||||
// ─── NOW PLAYING OVERLAY ───
|
||||
export function openNowPlaying() {
|
||||
@@ -148,7 +148,7 @@ async function _loadNpLyrics(songId) {
|
||||
|
||||
const token = ++_lyricsLoadToken;
|
||||
const s = S.queue[S.qIdx];
|
||||
const { getLyricsBySongId } = await import('./api.js');
|
||||
const { getLyricsBySongId } = await import('../api.js');
|
||||
if(token !== _lyricsLoadToken) return;
|
||||
|
||||
// 1. Navidrome (plugin handles fetching + caching server-side)
|
||||
@@ -1,8 +1,8 @@
|
||||
import { S } from './state.js';
|
||||
import { esc, formatDur, toast } from './utils.js';
|
||||
import { api, streamUrl, coverUrl, downloadUrl, dispTitle, dispArtist, dispArtistParts, dispAlbum } from './api.js';
|
||||
import { S } from '../state.js';
|
||||
import { esc, formatDur, toast } from '../utils.js';
|
||||
import { api, streamUrl, coverUrl, downloadUrl, dispTitle, dispArtist, dispArtistParts, dispAlbum } from '../api.js';
|
||||
// views.js/ui.js circular imports — safe (only used inside function bodies)
|
||||
import { loadRecommendations } from './views.js';
|
||||
import { loadRecommendations } from '../views/views.js';
|
||||
import { updateLyricsHighlight, onNpSongChange, syncNowPlaying } from './nowplaying.js';
|
||||
|
||||
// ─── NATIVE BRIDGE DETECTION ───────────────────────────────────────────────
|
||||
@@ -1,5 +1,5 @@
|
||||
import { S } from './state.js';
|
||||
import { esc, formatDur } from './utils.js';
|
||||
import { S } from '../state.js';
|
||||
import { esc, formatDur } from '../utils.js';
|
||||
|
||||
// Exposed on window so inline onerror= handlers can call it.
|
||||
// Retries the same src up to 3 times with increasing delays, then shows fallback.
|
||||
@@ -8,11 +8,11 @@ window._imgRetry = function(img, fallback) {
|
||||
if (n > 3 || !img.src) { img.parentNode.innerHTML = `<div class="no-art">${fallback}</div>`; return; }
|
||||
setTimeout(() => { const s = img.src; img.src = ''; img.src = s; }, 1500 * n);
|
||||
};
|
||||
import { coverUrl, dispTitle, dispArtistParts, dispAlbum, dispArtistById, dispAlbumName } from './api.js';
|
||||
import { coverUrl, dispTitle, dispArtistParts, dispAlbum, dispArtistById, dispAlbumName } from '../api.js';
|
||||
// Circular imports — safe (only used inside event handlers after module init)
|
||||
import { playQueue, toggleStar, playPreview, parseArtists } from './player.js';
|
||||
import { playQueue, toggleStar, playPreview, parseArtists } from '../player/player.js';
|
||||
import { showCtxMenu, openDlModal } from './ui.js';
|
||||
import { navigate, quickPlayAlbum, quickPlayArtist } from './views.js';
|
||||
import { navigate, quickPlayAlbum, quickPlayArtist } from '../views/views.js';
|
||||
|
||||
// ─── GENERIC CARD ───
|
||||
export function makeCard(title, sub, imgUrl, fallback='💿') {
|
||||
@@ -1,10 +1,10 @@
|
||||
import { S } from './state.js';
|
||||
import { esc, formatDur, toast } from './utils.js';
|
||||
import { api, coverUrl, ndAuth, fetchRomajiIndex, romajiConvert, romajiLookup, invalidateRomajiIndex, invalidateCoverCache, getRomajiOverrides, setRomajiOverride, removeRomajiOverride } from './api.js';
|
||||
import { THEMES, applyTheme, getCurrentThemeId } from './themes.js';
|
||||
import { S } from '../state.js';
|
||||
import { esc, formatDur, toast } from '../utils.js';
|
||||
import { api, coverUrl, ndAuth, fetchRomajiIndex, romajiConvert, romajiLookup, invalidateRomajiIndex, invalidateCoverCache, getRomajiOverrides, setRomajiOverride, removeRomajiOverride } from '../api.js';
|
||||
import { THEMES, applyTheme, getCurrentThemeId } from '../themes.js';
|
||||
// Circular imports — safe (only used inside function bodies)
|
||||
import { playQueue, dlSongLocal, toggleStar, togglePlay, renderQueue, syncNpStar, updateStarBtn, npToggleStar, playCurrent, setPlayIcon, nativeInsertAt, refreshDisplayMode, updateNpColorMode } from './player.js';
|
||||
import { navigate, loadPlaylists, loadPlaylistDetail } from './views.js';
|
||||
import { playQueue, dlSongLocal, toggleStar, togglePlay, renderQueue, syncNpStar, updateStarBtn, npToggleStar, playCurrent, setPlayIcon, nativeInsertAt, refreshDisplayMode, updateNpColorMode } from '../player/player.js';
|
||||
import { navigate, loadPlaylists, loadPlaylistDetail } from '../views/views.js';
|
||||
|
||||
// ─── SIDEBAR ───
|
||||
export function openSidebar() {
|
||||
@@ -30,7 +30,7 @@ export function toggleMobileSearch() {
|
||||
document.getElementById('header-search-input')?.focus();
|
||||
}
|
||||
export function closeMobileSearch() {
|
||||
import('./views.js').then(m => m.closeHeaderSearch());
|
||||
import('../views/views.js').then(m => m.closeHeaderSearch());
|
||||
}
|
||||
|
||||
// ─── SETUP ───
|
||||
@@ -41,7 +41,7 @@ export function switchSetupTab(t) {
|
||||
// ─── SETTINGS ───
|
||||
export function openSettings() {
|
||||
// Lazy import to avoid circular dep — abs.js imports views.js
|
||||
import('./abs.js').then(m => m.absOpenSettings());
|
||||
import('../views/abs.js').then(m => m.absOpenSettings());
|
||||
document.getElementById('s-url').value = S.cfg.url || '';
|
||||
document.getElementById('s-user').value = S.cfg.user || '';
|
||||
document.getElementById('s-pass').value = '';
|
||||
@@ -1,6 +1,6 @@
|
||||
import { S } from './state.js';
|
||||
import { esc, toast } from './utils.js';
|
||||
import { cancelMusicAutoplay } from './player.js';
|
||||
import { S } from '../state.js';
|
||||
import { esc, toast } from '../utils.js';
|
||||
import { cancelMusicAutoplay } from '../player/player.js';
|
||||
|
||||
// ─── HTTP HELPERS ───
|
||||
// API calls are routed through the NovaSonic backend proxy to avoid CORS restrictions.
|
||||
@@ -1,5 +1,5 @@
|
||||
import { S } from './state.js';
|
||||
import { esc, toast } from './utils.js';
|
||||
import { S } from '../state.js';
|
||||
import { esc, toast } from '../utils.js';
|
||||
|
||||
export function loadSoundCloudImporter() {
|
||||
// Hint is managed by the unified importer input in spotify.js
|
||||
@@ -1,9 +1,9 @@
|
||||
import { S } from './state.js';
|
||||
import { esc, toast } from './utils.js';
|
||||
import { itunesSearch } from './api.js';
|
||||
import { S } from '../state.js';
|
||||
import { esc, toast } from '../utils.js';
|
||||
import { itunesSearch } from '../api.js';
|
||||
// Circular imports — safe (only used inside function bodies)
|
||||
import { makeGlobalTrackItem } from './components.js';
|
||||
import { openDlModal } from './ui.js';
|
||||
import { makeGlobalTrackItem } from '../ui/components.js';
|
||||
import { openDlModal } from '../ui/ui.js';
|
||||
|
||||
// ─── UNIFIED IMPORTER INPUT ───
|
||||
export function onImporterInput(val) {
|
||||
@@ -1,12 +1,12 @@
|
||||
import { S } from './state.js';
|
||||
import { esc, formatDur, toast } from './utils.js';
|
||||
import { api, apiCached, apiIsCached, coverUrl, itunesSearch, itunesLookupAlbum, itunesLookupArtistSongs, itunesLookupArtistAlbums, itunesTopCharts, itunesTopSongs } from './api.js';
|
||||
import { S } from '../state.js';
|
||||
import { esc, formatDur, toast } from '../utils.js';
|
||||
import { api, apiCached, apiIsCached, coverUrl, itunesSearch, itunesLookupAlbum, itunesLookupArtistSongs, itunesLookupArtistAlbums, itunesTopCharts, itunesTopSongs } from '../api.js';
|
||||
// Circular imports — safe (only used inside function bodies)
|
||||
import { playQueue, dlSongLocal, playPreview, toggleStar, parseArtists } from './player.js';
|
||||
import { showCtxMenu, openEditPlModal, openDlModal, closeSidebar, switchNavMode } from './ui.js';
|
||||
import { closeNowPlaying, closeNpMenu } from './nowplaying.js';
|
||||
import { makeCard, makeAlbumCard, makeSongItem, makeGlobalTrackItem, mkSection } from './components.js';
|
||||
import { fetchRomajiIndex, dispArtistById, dispAlbumName, dispAlbum, romajiLookup } from './api.js';
|
||||
import { playQueue, dlSongLocal, playPreview, toggleStar, parseArtists } from '../player/player.js';
|
||||
import { showCtxMenu, openEditPlModal, openDlModal, closeSidebar, switchNavMode } from '../ui/ui.js';
|
||||
import { closeNowPlaying, closeNpMenu } from '../player/nowplaying.js';
|
||||
import { makeCard, makeAlbumCard, makeSongItem, makeGlobalTrackItem, mkSection } from '../ui/components.js';
|
||||
import { fetchRomajiIndex, dispArtistById, dispAlbumName, dispAlbum, romajiLookup } from '../api.js';
|
||||
import { loadSpotifyImporter } from './spotify.js';
|
||||
import { loadSoundCloudImporter } from './soundcloud.js';
|
||||
|
||||
@@ -496,7 +496,7 @@ export async function loadArtistDetail(artist) {
|
||||
shuffleBtn.onclick = async () => {
|
||||
S.shuffle = true;
|
||||
localStorage.setItem('ns_shuffle', '1');
|
||||
import('./player.js').then(({ _applyShuffleRepeat }) => _applyShuffleRepeat());
|
||||
import('../player/player.js').then(({ _applyShuffleRepeat }) => _applyShuffleRepeat());
|
||||
await quickPlayArtist(artist);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { S } from './state.js';
|
||||
import { esc, toast } from './utils.js';
|
||||
import { makeGlobalTrackItem } from './components.js';
|
||||
import { S } from '../state.js';
|
||||
import { esc, toast } from '../utils.js';
|
||||
import { makeGlobalTrackItem } from '../ui/components.js';
|
||||
|
||||
export async function resolveYouTubeLink() {
|
||||
const url = document.getElementById('importer-link-input')?.value.trim();
|
||||
Reference in new issue
Block a user