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
? ``
: '';
@@ -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",