local music warning, lyrics panel always open, shuffle improvements & fixes

This commit is contained in:
Samidy 2026-01-17 04:04:12 +03:00
parent 8759cae36b
commit e5792d035c
9 changed files with 64 additions and 37 deletions

View file

@ -677,6 +677,9 @@
</svg> </svg>
<span id="select-local-folder-text">Select Music Folder</span> <span id="select-local-folder-text">Select Music Folder</span>
</button> </button>
<p id="local-browser-warning" style="display: none; color: #ef4444; margin-top: 10px; font-size: 0.9rem;">
Please use Google Chrome or Microsoft Edge to play local files.
</p>
<p style="margin-top: 10px; font-size: 0.9rem; color: var(--muted-foreground)"> <p style="margin-top: 10px; font-size: 0.9rem; color: var(--muted-foreground)">
Select a folder on your device to play local files. <br /> Select a folder on your device to play local files. <br />
Note: Metadata reading is basic (FLAC/MP3 tags). Note: Metadata reading is basic (FLAC/MP3 tags).

View file

@ -194,6 +194,21 @@ document.addEventListener('DOMContentLoaded', async () => {
const scrobbler = new LastFMScrobbler(); const scrobbler = new LastFMScrobbler();
const lyricsManager = new LyricsManager(api); 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) // Pre-load Kuroshiro for romaji conversion in background (always load so it's ready instantly)
lyricsManager.loadKuroshiro().catch((err) => { lyricsManager.loadKuroshiro().catch((err) => {
console.warn('Failed to pre-load Kuroshiro:', err); console.warn('Failed to pre-load Kuroshiro:', err);
@ -318,7 +333,7 @@ document.addEventListener('DOMContentLoaded', async () => {
// Update lyrics panel if it's open // Update lyrics panel if it's open
if (sidePanelManager.isActive('lyrics')) { if (sidePanelManager.isActive('lyrics')) {
// Re-open forces update/refresh of content and sync // 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 // Update Fullscreen if it's open

View file

@ -1,4 +1,5 @@
//js/lastfm.js //js/lastfm.js
import { delay, getTrackArtists } from './utils.js';
export class LastFMScrobbler { export class LastFMScrobbler {
constructor() { constructor() {
@ -65,7 +66,7 @@ export class LastFMScrobbler {
try { try {
const { default: md5 } = await import('https://cdn.jsdelivr.net/npm/md5@2.3.0/+esm'); const { default: md5 } = await import('https://cdn.jsdelivr.net/npm/md5@2.3.0/+esm');
return md5(signatureString); return md5(signatureString);
} catch { } catch (e) {
console.error('MD5 library not available'); console.error('MD5 library not available');
throw new Error('MD5 library required for Last.fm'); throw new Error('MD5 library required for Last.fm');
} }
@ -155,7 +156,7 @@ export class LastFMScrobbler {
try { try {
const params = { 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, track: track.title,
}; };
@ -204,7 +205,7 @@ export class LastFMScrobbler {
const timestamp = Math.floor(Date.now() / 1000); const timestamp = Math.floor(Date.now() / 1000);
const params = { 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, track: this.currentTrack.title,
timestamp: timestamp, timestamp: timestamp,
}; };
@ -235,7 +236,7 @@ export class LastFMScrobbler {
try { try {
const params = { 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, track: track.title,
}; };

View file

@ -1,9 +1,10 @@
//js/lyrics.js //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'; import { sidePanelManager } from './side-panel.js';
// Dictionary path for kuromoji // Dictionary path for kuromoji
// Using CDN - the kuroshiro-analyzer loaded from unpkg will use this as base for fetching dict files // 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 { export class LyricsManager {
constructor(api) { constructor(api) {
@ -189,7 +190,7 @@ export class LyricsManager {
getRomajiMode() { getRomajiMode() {
try { try {
return localStorage.getItem('lyricsRomajiMode') === 'true'; return localStorage.getItem('lyricsRomajiMode') === 'true';
} catch { } catch (e) {
return false; return false;
} }
} }
@ -498,21 +499,15 @@ export class LyricsManager {
} }
} }
export async function openLyricsPanel(track, audioPlayer, lyricsManager) { export function openLyricsPanel(track, audioPlayer, lyricsManager, forceOpen = false) {
const manager = lyricsManager || new LyricsManager(); 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.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) => { manager.loadKuroshiro().catch((err) => {
console.warn('Failed to load Kuroshiro for Romaji conversion:', err); console.warn('Failed to load Kuroshiro for Romaji conversion:', err);
}); });
} }
}
const renderControls = (container) => { const renderControls = (container) => {
const isRomajiMode = manager.getRomajiMode(); const isRomajiMode = manager.getRomajiMode();
@ -548,7 +543,7 @@ export async function openLyricsPanel(track, audioPlayer, lyricsManager) {
romajiBtn.addEventListener('click', async () => { romajiBtn.addEventListener('click', async () => {
const amLyrics = sidePanelManager.panel.querySelector('am-lyrics'); const amLyrics = sidePanelManager.panel.querySelector('am-lyrics');
if (amLyrics) { if (amLyrics) {
await manager.toggleRomajiMode(amLyrics); const newMode = await manager.toggleRomajiMode(amLyrics);
updateRomajiBtn(); updateRomajiBtn();
} }
}); });
@ -558,9 +553,13 @@ export async function openLyricsPanel(track, audioPlayer, lyricsManager) {
const renderContent = async (container) => { const renderContent = async (container) => {
clearLyricsPanelSync(audioPlayer, sidePanelManager.panel); clearLyricsPanelSync(audioPlayer, sidePanelManager.panel);
await renderLyricsComponent(container, track, audioPlayer, manager); 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) { async function renderLyricsComponent(container, track, audioPlayer, lyricsManager) {

View file

@ -426,12 +426,20 @@ export class Player {
if (this.shuffleActive) { if (this.shuffleActive) {
this.originalQueueBeforeShuffle = [...this.queue]; this.originalQueueBeforeShuffle = [...this.queue];
const currentTrack = this.queue[this.currentQueueIndex]; 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);
if (this.currentQueueIndex === -1 && currentTrack) { const tracksToShuffle = [...this.queue];
this.shuffledQueue.unshift(currentTrack); if (currentTrack && this.currentQueueIndex >= 0) {
tracksToShuffle.splice(this.currentQueueIndex, 1);
}
tracksToShuffle.sort(() => Math.random() - 0.5);
if (currentTrack) {
this.shuffledQueue = [currentTrack, ...tracksToShuffle];
this.currentQueueIndex = 0; this.currentQueueIndex = 0;
} else {
this.shuffledQueue = tracksToShuffle;
this.currentQueueIndex = -1;
} }
} else { } else {
const currentTrack = this.shuffledQueue[this.currentQueueIndex]; const currentTrack = this.shuffledQueue[this.currentQueueIndex];

View file

@ -7,9 +7,9 @@ export class SidePanelManager {
this.currentView = null; // 'queue' or 'lyrics' 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 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(); this.close();
return; return;
} }

View file

@ -1,7 +1,7 @@
//storage.js //storage.js
export const apiSettings = { export const apiSettings = {
STORAGE_KEY: 'monochrome-api-instances-v2', 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_KEY: 'monochrome-instance-speeds',
SPEED_TEST_CACHE_DURATION: 1000 * 60 * 60, SPEED_TEST_CACHE_DURATION: 1000 * 60 * 60,
defaultInstances: { api: [], streaming: [] }, defaultInstances: { api: [], streaming: [] },
@ -53,7 +53,7 @@ export const apiSettings = {
} catch (error) { } catch (error) {
console.error('Failed to load instances from GitHub:', error); console.error('Failed to load instances from GitHub:', error);
this.defaultInstances = { 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: [ streaming: [
'https://triton.squid.wtf', 'https://triton.squid.wtf',
'https://wolf.qqdl.site', 'https://wolf.qqdl.site',

View file

@ -16,9 +16,8 @@ import {
escapeHtml, escapeHtml,
} from './utils.js'; } from './utils.js';
import { openLyricsPanel } from './lyrics.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 { db } from './db.js';
import { showNotification } from './downloads.js';
import { getVibrantColorFromImage } from './vibrant-color.js'; import { getVibrantColorFromImage } from './vibrant-color.js';
import { syncManager } from './accounts/pocketbase.js'; import { syncManager } from './accounts/pocketbase.js';
@ -70,7 +69,7 @@ export class UIRenderer {
this.vibrantColorCache.set(url, null); this.vibrantColorCache.set(url, null);
this.resetVibrantColor(); this.resetVibrantColor();
} }
} catch { } catch (e) {
this.vibrantColorCache.set(url, null); this.vibrantColorCache.set(url, null);
this.resetVibrantColor(); this.resetVibrantColor();
} }
@ -163,6 +162,7 @@ export class UIRenderer {
} }
createTrackItemHTML(track, index, showCover = false, hasMultipleDiscs = false) { createTrackItemHTML(track, index, showCover = false, hasMultipleDiscs = false) {
const playIconSmall = SVG_PLAY;
const trackImageHTML = showCover const trackImageHTML = showCover
? `<img src="${this.api.getCoverUrl(track.album?.cover)}" alt="Track Cover" class="track-item-cover" loading="lazy">` ? `<img src="${this.api.getCoverUrl(track.album?.cover)}" alt="Track Cover" class="track-item-cover" loading="lazy">`
: ''; : '';
@ -629,6 +629,7 @@ export class UIRenderer {
const title = document.getElementById('fullscreen-track-title'); const title = document.getElementById('fullscreen-track-title');
const artist = document.getElementById('fullscreen-track-artist'); const artist = document.getElementById('fullscreen-track-artist');
const nextTrackEl = document.getElementById('fullscreen-next-track'); const nextTrackEl = document.getElementById('fullscreen-next-track');
const lyricsContainer = document.getElementById('fullscreen-lyrics-container');
const lyricsToggleBtn = document.getElementById('toggle-fullscreen-lyrics-btn'); const lyricsToggleBtn = document.getElementById('toggle-fullscreen-lyrics-btn');
const coverUrl = this.api.getCoverUrl(track.album?.cover, '1280'); const coverUrl = this.api.getCoverUrl(track.album?.cover, '1280');

View file

@ -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": [ "streaming": [
"https://triton.squid.wtf", "https://triton.squid.wtf",
"https://wolf.qqdl.site", "https://wolf.qqdl.site",