local music warning, lyrics panel always open, shuffle improvements & fixes
This commit is contained in:
parent
8759cae36b
commit
e5792d035c
9 changed files with 64 additions and 37 deletions
|
|
@ -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).
|
||||
|
|
|
|||
25
js/app.js
25
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 });
|
||||
}
|
||||
});
|
||||
|
|
|
|||
11
js/lastfm.js
11
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
29
js/lyrics.js
29
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) {
|
||||
|
|
|
|||
16
js/player.js
16
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];
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
7
js/ui.js
7
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
|
||||
? `<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');
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue