From e5792d035c60758005b99b491c8909a9adaada73 Mon Sep 17 00:00:00 2001 From: Samidy Date: Sat, 17 Jan 2026 04:04:12 +0300 Subject: [PATCH] local music warning, lyrics panel always open, shuffle improvements & fixes --- index.html | 3 +++ js/app.js | 25 ++++++++++++++++++++----- js/lastfm.js | 11 ++++++----- js/lyrics.js | 29 ++++++++++++++--------------- js/player.js | 16 ++++++++++++---- js/side-panel.js | 4 ++-- js/storage.js | 4 ++-- js/ui.js | 7 ++++--- public/instances.json | 2 +- 9 files changed, 64 insertions(+), 37 deletions(-) diff --git a/index.html b/index.html index 642b2a5..83bde63 100644 --- a/index.html +++ b/index.html @@ -677,6 +677,9 @@ Select Music Folder +

Select a folder on your device to play local files.
Note: Metadata reading is basic (FLAC/MP3 tags). diff --git a/js/app.js b/js/app.js index 002c73e..9b51a36 100644 --- a/js/app.js +++ b/js/app.js @@ -194,6 +194,21 @@ document.addEventListener('DOMContentLoaded', async () => { const scrobbler = new LastFMScrobbler(); const lyricsManager = new LyricsManager(api); + // Check browser support for local files + const selectLocalBtn = document.getElementById('select-local-folder-btn'); + const browserWarning = document.getElementById('local-browser-warning'); + + if (selectLocalBtn && browserWarning) { + const ua = navigator.userAgent; + const isChromeOrEdge = (ua.indexOf('Chrome') > -1 || ua.indexOf('Edg') > -1) && !/Mobile|Android/.test(ua); + const hasFileSystemApi = 'showDirectoryPicker' in window; + + if (!isChromeOrEdge || !hasFileSystemApi) { + selectLocalBtn.style.display = 'none'; + browserWarning.style.display = 'block'; + } + } + // Pre-load Kuroshiro for romaji conversion in background (always load so it's ready instantly) lyricsManager.loadKuroshiro().catch((err) => { console.warn('Failed to pre-load Kuroshiro:', err); @@ -318,7 +333,7 @@ document.addEventListener('DOMContentLoaded', async () => { // Update lyrics panel if it's open if (sidePanelManager.isActive('lyrics')) { // Re-open forces update/refresh of content and sync - openLyricsPanel(player.currentTrack, audioPlayer, lyricsManager); + openLyricsPanel(player.currentTrack, audioPlayer, lyricsManager, true); } // Update Fullscreen if it's open @@ -1016,17 +1031,17 @@ document.addEventListener('DOMContentLoaded', async () => { const track = contextMenu._contextTrack; const albumItem = contextMenu.querySelector('[data-action="go-to-album"]'); const artistItem = contextMenu.querySelector('[data-action="go-to-artist"]'); - + if (track) { if (albumItem) { let label = 'Album'; const albumType = track.album?.type?.toUpperCase(); const trackCount = track.album?.numberOfTracks; - + if (albumType === 'SINGLE' || trackCount === 1) label = 'Single'; else if (albumType === 'EP') label = 'EP'; else if (trackCount && trackCount <= 6) label = 'EP'; - + albumItem.textContent = `Go to ${label}`; albumItem.style.display = track.album ? 'block' : 'none'; } @@ -1039,7 +1054,7 @@ document.addEventListener('DOMContentLoaded', async () => { } }); }); - + observer.observe(contextMenu, { attributes: true }); } }); diff --git a/js/lastfm.js b/js/lastfm.js index 7d49cfe..196b560 100644 --- a/js/lastfm.js +++ b/js/lastfm.js @@ -1,4 +1,5 @@ //js/lastfm.js +import { delay, getTrackArtists } from './utils.js'; export class LastFMScrobbler { constructor() { @@ -65,7 +66,7 @@ export class LastFMScrobbler { try { const { default: md5 } = await import('https://cdn.jsdelivr.net/npm/md5@2.3.0/+esm'); return md5(signatureString); - } catch { + } catch (e) { console.error('MD5 library not available'); throw new Error('MD5 library required for Last.fm'); } @@ -155,7 +156,7 @@ export class LastFMScrobbler { try { const params = { - artist: track.artist?.name || track.artists?.[0]?.name || 'Unknown Artist', + artist: track.artists?.[0]?.name || track.artist?.name || 'Unknown Artist', track: track.title, }; @@ -204,7 +205,7 @@ export class LastFMScrobbler { const timestamp = Math.floor(Date.now() / 1000); const params = { - artist: this.currentTrack.artist?.name || this.currentTrack.artists?.[0]?.name || 'Unknown Artist', + artist: this.currentTrack.artists?.[0]?.name || this.currentTrack.artist?.name || 'Unknown Artist', track: this.currentTrack.title, timestamp: timestamp, }; @@ -235,7 +236,7 @@ export class LastFMScrobbler { try { const params = { - artist: track.artist?.name || track.artists?.[0]?.name || 'Unknown Artist', + artist: track.artists?.[0]?.name || track.artist?.name || 'Unknown Artist', track: track.title, }; @@ -260,4 +261,4 @@ export class LastFMScrobbler { this.clearScrobbleTimer(); this.currentTrack = null; } -} +} \ No newline at end of file diff --git a/js/lyrics.js b/js/lyrics.js index 403fb87..175f9bc 100644 --- a/js/lyrics.js +++ b/js/lyrics.js @@ -1,9 +1,10 @@ //js/lyrics.js -import { getTrackTitle, getTrackArtists, buildTrackFilename, SVG_CLOSE } from './utils.js'; +import { getTrackTitle, getTrackArtists, buildTrackFilename, SVG_DOWNLOAD, SVG_CLOSE } from './utils.js'; import { sidePanelManager } from './side-panel.js'; // Dictionary path for kuromoji // Using CDN - the kuroshiro-analyzer loaded from unpkg will use this as base for fetching dict files +const KUROMOJI_DICT_PATH = 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/'; export class LyricsManager { constructor(api) { @@ -189,7 +190,7 @@ export class LyricsManager { getRomajiMode() { try { return localStorage.getItem('lyricsRomajiMode') === 'true'; - } catch { + } catch (e) { return false; } } @@ -498,20 +499,14 @@ export class LyricsManager { } } -export async function openLyricsPanel(track, audioPlayer, lyricsManager) { +export function openLyricsPanel(track, audioPlayer, lyricsManager, forceOpen = false) { const manager = lyricsManager || new LyricsManager(); - // Load Kuroshiro early for Kanji conversion (blocking if Romaji mode is enabled) + // Load Kuroshiro in background if needed if (!manager.kuroshiroLoaded && !manager.kuroshiroLoading) { - if (manager.getRomajiMode()) { - // If Romaji mode is enabled, wait for Kuroshiro to load before continuing - await manager.loadKuroshiro(); - } else { - // Otherwise, load in background - manager.loadKuroshiro().catch((err) => { - console.warn('Failed to load Kuroshiro for Romaji conversion:', err); - }); - } + manager.loadKuroshiro().catch((err) => { + console.warn('Failed to load Kuroshiro for Romaji conversion:', err); + }); } const renderControls = (container) => { @@ -548,7 +543,7 @@ export async function openLyricsPanel(track, audioPlayer, lyricsManager) { romajiBtn.addEventListener('click', async () => { const amLyrics = sidePanelManager.panel.querySelector('am-lyrics'); if (amLyrics) { - await manager.toggleRomajiMode(amLyrics); + const newMode = await manager.toggleRomajiMode(amLyrics); updateRomajiBtn(); } }); @@ -558,9 +553,13 @@ export async function openLyricsPanel(track, audioPlayer, lyricsManager) { const renderContent = async (container) => { clearLyricsPanelSync(audioPlayer, sidePanelManager.panel); await renderLyricsComponent(container, track, audioPlayer, manager); + if (container.lyricsCleanup) { + sidePanelManager.panel.lyricsCleanup = container.lyricsCleanup; + sidePanelManager.panel.lyricsManager = container.lyricsManager; + } }; - sidePanelManager.open('lyrics', 'Lyrics', renderControls, renderContent); + sidePanelManager.open('lyrics', 'Lyrics', renderControls, renderContent, forceOpen); } async function renderLyricsComponent(container, track, audioPlayer, lyricsManager) { diff --git a/js/player.js b/js/player.js index 6f9ce59..c1d5543 100644 --- a/js/player.js +++ b/js/player.js @@ -426,12 +426,20 @@ export class Player { if (this.shuffleActive) { this.originalQueueBeforeShuffle = [...this.queue]; const currentTrack = this.queue[this.currentQueueIndex]; - this.shuffledQueue = [...this.queue].sort(() => Math.random() - 0.5); - this.currentQueueIndex = this.shuffledQueue.findIndex((t) => t.id === currentTrack?.id); + + const tracksToShuffle = [...this.queue]; + if (currentTrack && this.currentQueueIndex >= 0) { + tracksToShuffle.splice(this.currentQueueIndex, 1); + } + + tracksToShuffle.sort(() => Math.random() - 0.5); - if (this.currentQueueIndex === -1 && currentTrack) { - this.shuffledQueue.unshift(currentTrack); + if (currentTrack) { + this.shuffledQueue = [currentTrack, ...tracksToShuffle]; this.currentQueueIndex = 0; + } else { + this.shuffledQueue = tracksToShuffle; + this.currentQueueIndex = -1; } } else { const currentTrack = this.shuffledQueue[this.currentQueueIndex]; diff --git a/js/side-panel.js b/js/side-panel.js index 2efff92..fb5d5ad 100644 --- a/js/side-panel.js +++ b/js/side-panel.js @@ -7,9 +7,9 @@ export class SidePanelManager { this.currentView = null; // 'queue' or 'lyrics' } - open(view, title, renderControlsCallback, renderContentCallback) { + open(view, title, renderControlsCallback, renderContentCallback, forceOpen = false) { // If clicking the same view that is already open, close it - if (this.currentView === view && this.panel.classList.contains('active')) { + if (!forceOpen && this.currentView === view && this.panel.classList.contains('active')) { this.close(); return; } diff --git a/js/storage.js b/js/storage.js index 1e520d8..0847d76 100644 --- a/js/storage.js +++ b/js/storage.js @@ -1,7 +1,7 @@ //storage.js export const apiSettings = { STORAGE_KEY: 'monochrome-api-instances-v2', - INSTANCES_URL: 'instances.json', + INSTANCES_URL: '../public/instances.json', SPEED_TEST_CACHE_KEY: 'monochrome-instance-speeds', SPEED_TEST_CACHE_DURATION: 1000 * 60 * 60, defaultInstances: { api: [], streaming: [] }, @@ -53,7 +53,7 @@ export const apiSettings = { } catch (error) { console.error('Failed to load instances from GitHub:', error); this.defaultInstances = { - api: ['https://triton.squid.wtf', 'https://tidal-api.binimum.org', 'https://vogel.qqdl.site'], + api: ['https://triton.squid.wtf', 'https://wolf.qqdl.site', "https://tidal-api.binimum.org", "https://monochrome-api.samidy.com"], streaming: [ 'https://triton.squid.wtf', 'https://wolf.qqdl.site', diff --git a/js/ui.js b/js/ui.js index bde3bff..ba56701 100644 --- a/js/ui.js +++ b/js/ui.js @@ -16,9 +16,8 @@ import { escapeHtml, } from './utils.js'; import { openLyricsPanel } from './lyrics.js'; -import { recentActivityManager, backgroundSettings, cardSettings } from './storage.js'; +import { recentActivityManager, backgroundSettings, trackListSettings, cardSettings } from './storage.js'; import { db } from './db.js'; -import { showNotification } from './downloads.js'; import { getVibrantColorFromImage } from './vibrant-color.js'; import { syncManager } from './accounts/pocketbase.js'; @@ -70,7 +69,7 @@ export class UIRenderer { this.vibrantColorCache.set(url, null); this.resetVibrantColor(); } - } catch { + } catch (e) { this.vibrantColorCache.set(url, null); this.resetVibrantColor(); } @@ -163,6 +162,7 @@ export class UIRenderer { } createTrackItemHTML(track, index, showCover = false, hasMultipleDiscs = false) { + const playIconSmall = SVG_PLAY; const trackImageHTML = showCover ? `Track Cover` : ''; @@ -629,6 +629,7 @@ export class UIRenderer { const title = document.getElementById('fullscreen-track-title'); const artist = document.getElementById('fullscreen-track-artist'); const nextTrackEl = document.getElementById('fullscreen-next-track'); + const lyricsContainer = document.getElementById('fullscreen-lyrics-container'); const lyricsToggleBtn = document.getElementById('toggle-fullscreen-lyrics-btn'); const coverUrl = this.api.getCoverUrl(track.album?.cover, '1280'); diff --git a/public/instances.json b/public/instances.json index 0e2962e..21ae8c4 100644 --- a/public/instances.json +++ b/public/instances.json @@ -1,5 +1,5 @@ { - "api": ["https://tidal-api.binimum.org", "https://monochrome-api.samidy.com"], + "api": ["https://tidal-api.binimum.org", "https://monochrome-api.samidy.com", "https://triton.squid.wtf", "https://wolf.qqdl.site"], "streaming": [ "https://triton.squid.wtf", "https://wolf.qqdl.site",