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>
<span id="select-local-folder-text">Select Music Folder</span>
</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)">
Select a folder on your device to play local files. <br />
Note: Metadata reading is basic (FLAC/MP3 tags).

View file

@ -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 });
}
});

View file

@ -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;
}
}
}

View file

@ -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) {

View file

@ -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];

View file

@ -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;
}

View file

@ -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',

View file

@ -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
? `<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 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');

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