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

+8 -8
View File
@@ -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)
+4 -4
View File
@@ -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 ───────────────────────────────────────────────
+5 -5
View File
@@ -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='💿') {
+8 -8
View File
@@ -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 = '';
+3 -3
View File
@@ -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
+5 -5
View File
@@ -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) {
+9 -9
View File
@@ -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);
};
+3 -3
View File
@@ -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();