Merge branch 'main' into feature/tabbed-settings
This commit is contained in:
commit
617da19f02
12 changed files with 914 additions and 23 deletions
229
index.html
229
index.html
|
|
@ -755,7 +755,7 @@
|
|||
</div>
|
||||
<nav class="sidebar-nav main">
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<li class="nav-item" id="sidebar-nav-home">
|
||||
<a href="/">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
|
@ -777,7 +777,7 @@
|
|||
<span>Home</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<li class="nav-item" id="sidebar-nav-library">
|
||||
<a href="/library">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
|
@ -798,7 +798,7 @@
|
|||
<span>Library</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<li class="nav-item" id="sidebar-nav-recent">
|
||||
<a href="/recent">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
|
@ -818,7 +818,7 @@
|
|||
<span>Recent</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<li class="nav-item" id="sidebar-nav-unreleased">
|
||||
<a href="/unreleased">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
|
@ -839,7 +839,32 @@
|
|||
<span>Unreleased</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<li class="nav-item" id="sidebar-nav-donate">
|
||||
<a href="https://pally.gg/p/monochrome" target="_blank" rel="noopener noreferrer">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M11 14h2a2 2 0 0 0 0-4h-3c-.6 0-1.1.2-1.4.6L3 16" />
|
||||
<path
|
||||
d="m14.45 13.39 5.05-4.694C20.196 8 21 6.85 21 5.75a2.75 2.75 0 0 0-4.797-1.837.276.276 0 0 1-.406 0A2.75 2.75 0 0 0 11 5.75c0 1.2.802 2.248 1.5 2.946L16 11.95"
|
||||
/>
|
||||
<path d="m2 15 6 6" />
|
||||
<path
|
||||
d="m7 20 1.6-1.4c.3-.4.8-.6 1.4-.6h4c1.1 0 2.1-.4 2.8-1.2l4.6-4.4a1 1 0 0 0-2.75-2.91"
|
||||
/>
|
||||
</svg>
|
||||
<span>Donate</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" id="sidebar-nav-settings">
|
||||
<a href="/settings">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
|
@ -860,7 +885,7 @@
|
|||
<span>Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<li class="nav-item" id="sidebar-nav-account">
|
||||
<a href="/account">
|
||||
<svg
|
||||
width="24"
|
||||
|
|
@ -898,7 +923,7 @@
|
|||
</nav>
|
||||
<nav class="sidebar-nav">
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<li class="nav-item" id="sidebar-nav-about">
|
||||
<a href="/about">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
|
@ -919,7 +944,7 @@
|
|||
<span>About</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<li class="nav-item" id="sidebar-nav-download">
|
||||
<a href="/download">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
|
@ -939,7 +964,7 @@
|
|||
<span>Download</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<li class="nav-item" id="sidebar-nav-discord">
|
||||
<a href="https://monochrome.samidy.com/discord" target="_blank">
|
||||
<svg
|
||||
width="64px"
|
||||
|
|
@ -1378,7 +1403,31 @@
|
|||
</div>
|
||||
|
||||
<div id="page-recent" class="page">
|
||||
<h2 class="section-title">Recently played</h2>
|
||||
<div
|
||||
style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem"
|
||||
>
|
||||
<h2 class="section-title" style="margin-bottom: 0">Recently played</h2>
|
||||
<button class="btn-secondary" id="clear-history-btn" title="Clear History">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M3 6h18" />
|
||||
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
|
||||
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
|
||||
<line x1="10" y1="11" x2="10" y2="17" />
|
||||
<line x1="14" y1="11" x2="14" y2="17" />
|
||||
</svg>
|
||||
<span>Clear</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="track-list" id="recent-tracks-container"></div>
|
||||
</div>
|
||||
|
||||
|
|
@ -2083,6 +2132,164 @@
|
|||
<input type="checkbox" id="listenbrainz-enabled-toggle" />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
|
||||
<div class="settings-group">
|
||||
<div class="setting-item">
|
||||
<div class="info">
|
||||
<span class="label">Show Recommended Songs</span>
|
||||
<span class="description">Display recommended songs on the home page</span>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="show-recommended-songs-toggle" checked />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="info">
|
||||
<span class="label">Show Recommended Albums</span>
|
||||
<span class="description">Display recommended albums on the home page</span>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="show-recommended-albums-toggle" checked />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="info">
|
||||
<span class="label">Show Recommended Artists</span>
|
||||
<span class="description">Display recommended artists on the home page</span>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="show-recommended-artists-toggle" checked />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="info">
|
||||
<span class="label">Show Jump Back In</span>
|
||||
<span class="description"
|
||||
>Display recent albums, playlists, and mixes on the home page</span
|
||||
>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="show-jump-back-in-toggle" checked />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-group">
|
||||
<div class="setting-item">
|
||||
<div class="info">
|
||||
<span class="label">Show Home in Sidebar</span>
|
||||
<span class="description">Display the Home link in the sidebar navigation</span>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="sidebar-show-home-toggle" checked />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="info">
|
||||
<span class="label">Show Library in Sidebar</span>
|
||||
<span class="description">Display the Library link in the sidebar navigation</span>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="sidebar-show-library-toggle" checked />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="info">
|
||||
<span class="label">Show Recent in Sidebar</span>
|
||||
<span class="description">Display the Recent link in the sidebar navigation</span>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="sidebar-show-recent-toggle" checked />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="info">
|
||||
<span class="label">Show Unreleased in Sidebar</span>
|
||||
<span class="description"
|
||||
>Display the Unreleased link in the sidebar navigation</span
|
||||
>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="sidebar-show-unreleased-toggle" checked />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="info">
|
||||
<span class="label">Show Donate in Sidebar</span>
|
||||
<span class="description">Display the Donate link in the sidebar navigation</span>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="sidebar-show-donate-toggle" checked />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="info">
|
||||
<span class="label">Show Settings in Sidebar</span>
|
||||
<span class="description">Display the Settings link in the sidebar navigation</span>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="sidebar-show-settings-toggle" checked />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="info">
|
||||
<span class="label">Show Account in Sidebar</span>
|
||||
<span class="description">Display the Account link in the sidebar navigation</span>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="sidebar-show-account-toggle" checked />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="info">
|
||||
<span class="label">Show About in Sidebar</span>
|
||||
<span class="description">Display the About link in the sidebar navigation</span>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="sidebar-show-about-toggle" checked />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="info">
|
||||
<span class="label">Show Download in Sidebar</span>
|
||||
<span class="description">Display the Download link in the sidebar navigation</span>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="sidebar-show-download-toggle" checked />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="info">
|
||||
<span class="label">Show Discord in Sidebar</span>
|
||||
<span class="description">Display the Discord link in the sidebar navigation</span>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="sidebar-show-discord-toggle" checked />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-group">
|
||||
<div class="setting-item">
|
||||
<div class="info">
|
||||
<span class="label">ListenBrainz Scrobbling</span>
|
||||
<span class="description"
|
||||
>Submit listens to ListenBrainz (requires User Token)</span
|
||||
>
|
||||
</div>
|
||||
<div class="setting-item" id="listenbrainz-token-setting" style="display: none">
|
||||
<div class="info">
|
||||
|
|
@ -2809,7 +3016,7 @@
|
|||
flex-wrap: wrap;
|
||||
"
|
||||
>
|
||||
<a href="https://ko-fi.com/monochromemusic">
|
||||
<a href="https://pally.gg/p/monochrome">
|
||||
<button id="donate-btn" class="btn-secondary">Donate to Monochrome</button>
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -98,6 +98,26 @@ const syncManager = {
|
|||
});
|
||||
return JSON.parse(recovered);
|
||||
} catch {
|
||||
try {
|
||||
// Python-style fallback (Single quotes, True/False, None)
|
||||
// This handles data that was incorrectly serialized as Python repr string
|
||||
if (str.includes("'") || str.includes('True') || str.includes('False')) {
|
||||
const jsFriendly = str
|
||||
.replace(/\bTrue\b/g, 'true')
|
||||
.replace(/\bFalse\b/g, 'false')
|
||||
.replace(/\bNone\b/g, 'null');
|
||||
|
||||
// Basic safety check: ensure it looks like a structure and doesn't contain obvious code vectors
|
||||
if (
|
||||
(jsFriendly.trim().startsWith('[') || jsFriendly.trim().startsWith('{')) &&
|
||||
!jsFriendly.match(/function|=>|window|document|alert|eval/)
|
||||
) {
|
||||
return new Function('return ' + jsFriendly)();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore fallback error
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
|
@ -361,7 +381,7 @@ const syncManager = {
|
|||
image: playlist.cover,
|
||||
cover: playlist.cover,
|
||||
playlist_cover: playlist.cover,
|
||||
tracks: playlist.tracks,
|
||||
tracks: JSON.stringify(playlist.tracks || []),
|
||||
isPublic: true,
|
||||
data: {
|
||||
title: playlist.name,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
//js/app.js
|
||||
import { LosslessAPI } from './api.js';
|
||||
import { apiSettings, themeManager, nowPlayingSettings, downloadQualitySettings } from './storage.js';
|
||||
import { apiSettings, themeManager, nowPlayingSettings, downloadQualitySettings, sidebarSettings } from './storage.js';
|
||||
import { UIRenderer } from './ui.js';
|
||||
import { Player } from './player.js';
|
||||
import { MultiScrobbler } from './multi-scrobbler.js';
|
||||
|
|
@ -313,6 +313,9 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
const currentTheme = themeManager.getTheme();
|
||||
themeManager.setTheme(currentTheme);
|
||||
|
||||
// Restore sidebar state
|
||||
sidebarSettings.restoreState();
|
||||
|
||||
initializeSettings(scrobbler, player, api, ui);
|
||||
initializePlayerEvents(player, audioPlayer, scrobbler, ui);
|
||||
initializeTrackInteractions(
|
||||
|
|
@ -405,6 +408,8 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
? '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>'
|
||||
: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>';
|
||||
}
|
||||
// Save sidebar state to localStorage
|
||||
sidebarSettings.setCollapsed(isCollapsed);
|
||||
});
|
||||
|
||||
document.getElementById('nav-back')?.addEventListener('click', () => {
|
||||
|
|
|
|||
|
|
@ -98,6 +98,17 @@ class AudioContextManager {
|
|||
if (this.isInitialized) return;
|
||||
if (!audioElement) return;
|
||||
|
||||
// Detect iOS - skip Web Audio initialization on iOS to avoid lock screen audio issues
|
||||
// iOS suspends AudioContext when screen locks, and MediaSession controls don't count
|
||||
// as user gestures to resume it, causing audio to play silently
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
const isIOS = /iphone|ipad|ipod/.test(ua) || (ua.includes('mac') && navigator.maxTouchPoints > 1);
|
||||
if (isIOS) {
|
||||
console.log('[AudioContext] Skipping Web Audio initialization on iOS for lock screen compatibility');
|
||||
this.isInitialized = true; // Mark as initialized to prevent repeated attempts
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.audio = audioElement;
|
||||
|
||||
|
|
@ -179,11 +190,28 @@ class AudioContextManager {
|
|||
|
||||
/**
|
||||
* Resume audio context (required after user interaction)
|
||||
* @returns {Promise<boolean>} - Returns true if context is running
|
||||
*/
|
||||
resume() {
|
||||
if (this.audioContext && this.audioContext.state === 'suspended') {
|
||||
this.audioContext.resume();
|
||||
async resume() {
|
||||
if (!this.audioContext) return false;
|
||||
|
||||
console.log('[AudioContext] Current state:', this.audioContext.state);
|
||||
|
||||
if (this.audioContext.state === 'suspended') {
|
||||
try {
|
||||
await this.audioContext.resume();
|
||||
console.log('[AudioContext] Resumed successfully, state:', this.audioContext.state);
|
||||
} catch (e) {
|
||||
console.warn('[AudioContext] Failed to resume:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure graph is connected after resuming (iOS may disconnect when suspended)
|
||||
if (this.isInitialized && this.audioContext.state === 'running') {
|
||||
this._connectGraph();
|
||||
}
|
||||
|
||||
return this.audioContext.state === 'running';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
13
js/db.js
13
js/db.js
|
|
@ -138,6 +138,19 @@ export class MusicDatabase {
|
|||
});
|
||||
}
|
||||
|
||||
async clearHistory() {
|
||||
const storeName = 'history_tracks';
|
||||
const db = await this.open();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction(storeName, 'readwrite');
|
||||
const store = transaction.objectStore(storeName);
|
||||
const request = store.clear();
|
||||
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
// Favorites API
|
||||
async toggleFavorite(type, item) {
|
||||
const plural = type === 'mix' ? 'mixes' : `${type}s`;
|
||||
|
|
|
|||
94
js/lyrics.js
94
js/lyrics.js
|
|
@ -133,6 +133,40 @@ export class LyricsManager {
|
|||
this.geniusManager = new GeniusManager();
|
||||
this.isGeniusMode = false;
|
||||
this.currentGeniusData = null;
|
||||
this.timingOffset = 0; // Offset in milliseconds (positive = delay lyrics, negative = advance lyrics)
|
||||
}
|
||||
|
||||
// Get timing offset for current track
|
||||
getTimingOffset(trackId) {
|
||||
try {
|
||||
const key = `lyrics-offset-${trackId}`;
|
||||
const stored = localStorage.getItem(key);
|
||||
return stored ? parseInt(stored, 10) : 0;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Set timing offset for current track
|
||||
setTimingOffset(trackId, offsetMs) {
|
||||
try {
|
||||
const key = `lyrics-offset-${trackId}`;
|
||||
localStorage.setItem(key, offsetMs.toString());
|
||||
} catch (e) {
|
||||
console.warn('Failed to save lyrics timing offset:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset timing offset for current track
|
||||
resetTimingOffset(trackId) {
|
||||
this.setTimingOffset(trackId, 0);
|
||||
}
|
||||
|
||||
// Get formatted offset display string
|
||||
getOffsetDisplayString(offsetMs) {
|
||||
const sign = offsetMs >= 0 ? '+' : '';
|
||||
const seconds = Math.abs(offsetMs) / 1000;
|
||||
return `${sign}${seconds.toFixed(1)}s`;
|
||||
}
|
||||
|
||||
// Load Kuroshiro from CDN (npm package uses Node.js path which doesn't work in browser)
|
||||
|
|
@ -715,15 +749,38 @@ export function openLyricsPanel(track, audioPlayer, lyricsManager, forceOpen = f
|
|||
});
|
||||
}
|
||||
|
||||
// Load saved timing offset for this track
|
||||
manager.timingOffset = manager.getTimingOffset(track.id);
|
||||
|
||||
const renderControls = (container) => {
|
||||
const isRomajiMode = manager.getRomajiMode();
|
||||
manager.isRomajiMode = isRomajiMode;
|
||||
const isGeniusMode = manager.isGeniusMode;
|
||||
const offsetDisplay = manager.getOffsetDisplayString(manager.timingOffset);
|
||||
|
||||
container.innerHTML = `
|
||||
<button id="close-side-panel-btn" class="btn-icon" title="Close">
|
||||
${SVG_CLOSE}
|
||||
</button>
|
||||
<div class="lyrics-timing-controls">
|
||||
<button id="lyrics-timing-minus-btn" class="btn-icon" title="Decrease delay (lyrics earlier) -0.5s">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M5 12h14"/>
|
||||
</svg>
|
||||
</button>
|
||||
<span id="lyrics-timing-display" class="lyrics-timing-display" title="Current timing offset">${offsetDisplay}</span>
|
||||
<button id="lyrics-timing-plus-btn" class="btn-icon" title="Increase delay (lyrics later) +0.5s">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M5 12h14M12 5v14"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button id="lyrics-timing-reset-btn" class="btn-icon" title="Reset timing offset">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/>
|
||||
<path d="M3 3v5h5"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<button id="romaji-toggle-btn" class="btn-icon" title="Toggle Romaji (Japanese to Latin)" data-enabled="${isRomajiMode}" style="color: ${isRomajiMode ? 'var(--primary)' : ''}">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
|
|
@ -740,6 +797,32 @@ export function openLyricsPanel(track, audioPlayer, lyricsManager, forceOpen = f
|
|||
clearLyricsPanelSync(audioPlayer, sidePanelManager.panel);
|
||||
});
|
||||
|
||||
// Timing adjustment controls
|
||||
const updateTimingDisplay = () => {
|
||||
const display = container.querySelector('#lyrics-timing-display');
|
||||
if (display) {
|
||||
display.textContent = manager.getOffsetDisplayString(manager.timingOffset);
|
||||
}
|
||||
};
|
||||
|
||||
container.querySelector('#lyrics-timing-minus-btn')?.addEventListener('click', () => {
|
||||
manager.timingOffset -= 500; // Decrease by 0.5 seconds
|
||||
manager.setTimingOffset(track.id, manager.timingOffset);
|
||||
updateTimingDisplay();
|
||||
});
|
||||
|
||||
container.querySelector('#lyrics-timing-plus-btn')?.addEventListener('click', () => {
|
||||
manager.timingOffset += 500; // Increase by 0.5 seconds
|
||||
manager.setTimingOffset(track.id, manager.timingOffset);
|
||||
updateTimingDisplay();
|
||||
});
|
||||
|
||||
container.querySelector('#lyrics-timing-reset-btn')?.addEventListener('click', () => {
|
||||
manager.timingOffset = 0;
|
||||
manager.resetTimingOffset(track.id);
|
||||
updateTimingDisplay();
|
||||
});
|
||||
|
||||
// Romaji toggle button handler
|
||||
const romajiBtn = container.querySelector('#romaji-toggle-btn');
|
||||
if (romajiBtn) {
|
||||
|
|
@ -945,11 +1028,17 @@ function setupSync(track, audioPlayer, amLyrics, lyricsManager) {
|
|||
let lastTimestamp = performance.now();
|
||||
let animationFrameId = null;
|
||||
|
||||
// Get timing offset from lyrics manager (in milliseconds)
|
||||
const getTimingOffset = () => {
|
||||
return lyricsManager?.timingOffset || 0;
|
||||
};
|
||||
|
||||
const updateTime = () => {
|
||||
const currentMs = audioPlayer.currentTime * 1000;
|
||||
baseTimeMs = currentMs;
|
||||
lastTimestamp = performance.now();
|
||||
amLyrics.currentTime = currentMs;
|
||||
// Apply timing offset: positive offset delays lyrics, negative advances them
|
||||
amLyrics.currentTime = currentMs - getTimingOffset();
|
||||
};
|
||||
|
||||
const tick = () => {
|
||||
|
|
@ -957,7 +1046,8 @@ function setupSync(track, audioPlayer, amLyrics, lyricsManager) {
|
|||
const now = performance.now();
|
||||
const elapsed = now - lastTimestamp;
|
||||
const nextMs = baseTimeMs + elapsed;
|
||||
amLyrics.currentTime = nextMs;
|
||||
// Apply timing offset: positive offset delays lyrics, negative advances them
|
||||
amLyrics.currentTime = nextMs - getTimingOffset();
|
||||
animationFrameId = requestAnimationFrame(tick);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
43
js/player.js
43
js/player.js
|
|
@ -9,6 +9,7 @@ import {
|
|||
createQualityBadgeHTML,
|
||||
} from './utils.js';
|
||||
import { queueManager, replayGainSettings } from './storage.js';
|
||||
import { audioContextManager } from './audio-context.js';
|
||||
|
||||
export class Player {
|
||||
constructor(audioElement, api, quality = 'HI_RES_LOSSLESS') {
|
||||
|
|
@ -50,6 +51,17 @@ export class Player {
|
|||
window.addEventListener('beforeunload', () => {
|
||||
this.saveQueueState();
|
||||
});
|
||||
|
||||
// Handle visibility change for iOS - AudioContext gets suspended when screen locks
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible' && !this.audio.paused) {
|
||||
// Ensure audio context is resumed when user returns to the app
|
||||
if (!audioContextManager.isReady()) {
|
||||
audioContextManager.init(this.audio);
|
||||
}
|
||||
audioContextManager.resume();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setVolume(value) {
|
||||
|
|
@ -176,19 +188,42 @@ export class Player {
|
|||
setupMediaSession() {
|
||||
if (!('mediaSession' in navigator)) return;
|
||||
|
||||
navigator.mediaSession.setActionHandler('play', () => {
|
||||
this.audio.play().catch(console.error);
|
||||
navigator.mediaSession.setActionHandler('play', async () => {
|
||||
// Initialize and resume audio context first (required for iOS lock screen)
|
||||
// Must happen before audio.play() or audio won't route through Web Audio
|
||||
if (!audioContextManager.isReady()) {
|
||||
audioContextManager.init(this.audio);
|
||||
}
|
||||
await audioContextManager.resume();
|
||||
|
||||
try {
|
||||
await this.audio.play();
|
||||
} catch (e) {
|
||||
console.error('MediaSession play failed:', e);
|
||||
// If play fails, try to handle it like a regular play/pause
|
||||
this.handlePlayPause();
|
||||
}
|
||||
});
|
||||
|
||||
navigator.mediaSession.setActionHandler('pause', () => {
|
||||
this.audio.pause();
|
||||
});
|
||||
|
||||
navigator.mediaSession.setActionHandler('previoustrack', () => {
|
||||
navigator.mediaSession.setActionHandler('previoustrack', async () => {
|
||||
// Ensure audio context is active for iOS lock screen controls
|
||||
if (!audioContextManager.isReady()) {
|
||||
audioContextManager.init(this.audio);
|
||||
}
|
||||
await audioContextManager.resume();
|
||||
this.playPrev();
|
||||
});
|
||||
|
||||
navigator.mediaSession.setActionHandler('nexttrack', () => {
|
||||
navigator.mediaSession.setActionHandler('nexttrack', async () => {
|
||||
// Ensure audio context is active for iOS lock screen controls
|
||||
if (!audioContextManager.isReady()) {
|
||||
audioContextManager.init(this.audio);
|
||||
}
|
||||
await audioContextManager.resume();
|
||||
this.playNext();
|
||||
});
|
||||
|
||||
|
|
|
|||
129
js/settings.js
129
js/settings.js
|
|
@ -17,6 +17,8 @@ import {
|
|||
playlistSettings,
|
||||
equalizerSettings,
|
||||
listenBrainzSettings,
|
||||
homePageSettings,
|
||||
sidebarSectionSettings,
|
||||
} from './storage.js';
|
||||
import { audioContextManager, EQ_PRESETS } from './audio-context.js';
|
||||
import { db } from './db.js';
|
||||
|
|
@ -676,6 +678,133 @@ export function initializeSettings(scrobbler, player, api, ui) {
|
|||
});
|
||||
}
|
||||
|
||||
// Home Page Section Toggles
|
||||
const showRecommendedSongsToggle = document.getElementById('show-recommended-songs-toggle');
|
||||
if (showRecommendedSongsToggle) {
|
||||
showRecommendedSongsToggle.checked = homePageSettings.shouldShowRecommendedSongs();
|
||||
showRecommendedSongsToggle.addEventListener('change', (e) => {
|
||||
homePageSettings.setShowRecommendedSongs(e.target.checked);
|
||||
});
|
||||
}
|
||||
|
||||
const showRecommendedAlbumsToggle = document.getElementById('show-recommended-albums-toggle');
|
||||
if (showRecommendedAlbumsToggle) {
|
||||
showRecommendedAlbumsToggle.checked = homePageSettings.shouldShowRecommendedAlbums();
|
||||
showRecommendedAlbumsToggle.addEventListener('change', (e) => {
|
||||
homePageSettings.setShowRecommendedAlbums(e.target.checked);
|
||||
});
|
||||
}
|
||||
|
||||
const showRecommendedArtistsToggle = document.getElementById('show-recommended-artists-toggle');
|
||||
if (showRecommendedArtistsToggle) {
|
||||
showRecommendedArtistsToggle.checked = homePageSettings.shouldShowRecommendedArtists();
|
||||
showRecommendedArtistsToggle.addEventListener('change', (e) => {
|
||||
homePageSettings.setShowRecommendedArtists(e.target.checked);
|
||||
});
|
||||
}
|
||||
|
||||
const showJumpBackInToggle = document.getElementById('show-jump-back-in-toggle');
|
||||
if (showJumpBackInToggle) {
|
||||
showJumpBackInToggle.checked = homePageSettings.shouldShowJumpBackIn();
|
||||
showJumpBackInToggle.addEventListener('change', (e) => {
|
||||
homePageSettings.setShowJumpBackIn(e.target.checked);
|
||||
});
|
||||
}
|
||||
|
||||
// Sidebar Section Toggles
|
||||
const sidebarShowHomeToggle = document.getElementById('sidebar-show-home-toggle');
|
||||
if (sidebarShowHomeToggle) {
|
||||
sidebarShowHomeToggle.checked = sidebarSectionSettings.shouldShowHome();
|
||||
sidebarShowHomeToggle.addEventListener('change', (e) => {
|
||||
sidebarSectionSettings.setShowHome(e.target.checked);
|
||||
sidebarSectionSettings.applySidebarVisibility();
|
||||
});
|
||||
}
|
||||
|
||||
const sidebarShowLibraryToggle = document.getElementById('sidebar-show-library-toggle');
|
||||
if (sidebarShowLibraryToggle) {
|
||||
sidebarShowLibraryToggle.checked = sidebarSectionSettings.shouldShowLibrary();
|
||||
sidebarShowLibraryToggle.addEventListener('change', (e) => {
|
||||
sidebarSectionSettings.setShowLibrary(e.target.checked);
|
||||
sidebarSectionSettings.applySidebarVisibility();
|
||||
});
|
||||
}
|
||||
|
||||
const sidebarShowRecentToggle = document.getElementById('sidebar-show-recent-toggle');
|
||||
if (sidebarShowRecentToggle) {
|
||||
sidebarShowRecentToggle.checked = sidebarSectionSettings.shouldShowRecent();
|
||||
sidebarShowRecentToggle.addEventListener('change', (e) => {
|
||||
sidebarSectionSettings.setShowRecent(e.target.checked);
|
||||
sidebarSectionSettings.applySidebarVisibility();
|
||||
});
|
||||
}
|
||||
|
||||
const sidebarShowUnreleasedToggle = document.getElementById('sidebar-show-unreleased-toggle');
|
||||
if (sidebarShowUnreleasedToggle) {
|
||||
sidebarShowUnreleasedToggle.checked = sidebarSectionSettings.shouldShowUnreleased();
|
||||
sidebarShowUnreleasedToggle.addEventListener('change', (e) => {
|
||||
sidebarSectionSettings.setShowUnreleased(e.target.checked);
|
||||
sidebarSectionSettings.applySidebarVisibility();
|
||||
});
|
||||
}
|
||||
|
||||
const sidebarShowDonateToggle = document.getElementById('sidebar-show-donate-toggle');
|
||||
if (sidebarShowDonateToggle) {
|
||||
sidebarShowDonateToggle.checked = sidebarSectionSettings.shouldShowDonate();
|
||||
sidebarShowDonateToggle.addEventListener('change', (e) => {
|
||||
sidebarSectionSettings.setShowDonate(e.target.checked);
|
||||
sidebarSectionSettings.applySidebarVisibility();
|
||||
});
|
||||
}
|
||||
|
||||
const sidebarShowSettingsToggle = document.getElementById('sidebar-show-settings-toggle');
|
||||
if (sidebarShowSettingsToggle) {
|
||||
sidebarShowSettingsToggle.checked = sidebarSectionSettings.shouldShowSettings();
|
||||
sidebarShowSettingsToggle.addEventListener('change', (e) => {
|
||||
sidebarSectionSettings.setShowSettings(e.target.checked);
|
||||
sidebarSectionSettings.applySidebarVisibility();
|
||||
});
|
||||
}
|
||||
|
||||
const sidebarShowAccountToggle = document.getElementById('sidebar-show-account-toggle');
|
||||
if (sidebarShowAccountToggle) {
|
||||
sidebarShowAccountToggle.checked = sidebarSectionSettings.shouldShowAccount();
|
||||
sidebarShowAccountToggle.addEventListener('change', (e) => {
|
||||
sidebarSectionSettings.setShowAccount(e.target.checked);
|
||||
sidebarSectionSettings.applySidebarVisibility();
|
||||
});
|
||||
}
|
||||
|
||||
const sidebarShowAboutToggle = document.getElementById('sidebar-show-about-toggle');
|
||||
if (sidebarShowAboutToggle) {
|
||||
sidebarShowAboutToggle.checked = sidebarSectionSettings.shouldShowAbout();
|
||||
sidebarShowAboutToggle.addEventListener('change', (e) => {
|
||||
sidebarSectionSettings.setShowAbout(e.target.checked);
|
||||
sidebarSectionSettings.applySidebarVisibility();
|
||||
});
|
||||
}
|
||||
|
||||
const sidebarShowDownloadToggle = document.getElementById('sidebar-show-download-toggle');
|
||||
if (sidebarShowDownloadToggle) {
|
||||
sidebarShowDownloadToggle.checked = sidebarSectionSettings.shouldShowDownload();
|
||||
sidebarShowDownloadToggle.addEventListener('change', (e) => {
|
||||
sidebarSectionSettings.setShowDownload(e.target.checked);
|
||||
sidebarSectionSettings.applySidebarVisibility();
|
||||
});
|
||||
}
|
||||
|
||||
const sidebarShowDiscordToggle = document.getElementById('sidebar-show-discord-toggle');
|
||||
if (sidebarShowDiscordToggle) {
|
||||
sidebarShowDiscordToggle.checked = sidebarSectionSettings.shouldShowDiscord();
|
||||
sidebarShowDiscordToggle.addEventListener('change', (e) => {
|
||||
sidebarSectionSettings.setShowDiscord(e.target.checked);
|
||||
sidebarSectionSettings.applySidebarVisibility();
|
||||
});
|
||||
}
|
||||
|
||||
// Apply sidebar visibility on initialization
|
||||
sidebarSectionSettings.applySidebarVisibility();
|
||||
|
||||
// Filename template setting
|
||||
const filenameTemplate = document.getElementById('filename-template');
|
||||
if (filenameTemplate) {
|
||||
|
|
|
|||
252
js/storage.js
252
js/storage.js
|
|
@ -814,6 +814,34 @@ export const equalizerSettings = {
|
|||
},
|
||||
};
|
||||
|
||||
export const sidebarSettings = {
|
||||
STORAGE_KEY: 'monochrome-sidebar-collapsed',
|
||||
|
||||
isCollapsed() {
|
||||
try {
|
||||
return localStorage.getItem(this.STORAGE_KEY) === 'true';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
setCollapsed(collapsed) {
|
||||
localStorage.setItem(this.STORAGE_KEY, collapsed ? 'true' : 'false');
|
||||
},
|
||||
|
||||
restoreState() {
|
||||
const isCollapsed = this.isCollapsed();
|
||||
if (isCollapsed) {
|
||||
document.body.classList.add('sidebar-collapsed');
|
||||
const toggleBtn = document.getElementById('sidebar-toggle');
|
||||
if (toggleBtn) {
|
||||
toggleBtn.innerHTML =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>';
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const queueManager = {
|
||||
STORAGE_KEY: 'monochrome-queue',
|
||||
|
||||
|
|
@ -873,6 +901,230 @@ export const listenBrainzSettings = {
|
|||
},
|
||||
};
|
||||
|
||||
export const homePageSettings = {
|
||||
SHOW_RECOMMENDED_SONGS_KEY: 'home-show-recommended-songs',
|
||||
SHOW_RECOMMENDED_ALBUMS_KEY: 'home-show-recommended-albums',
|
||||
SHOW_RECOMMENDED_ARTISTS_KEY: 'home-show-recommended-artists',
|
||||
SHOW_JUMP_BACK_IN_KEY: 'home-show-jump-back-in',
|
||||
|
||||
shouldShowRecommendedSongs() {
|
||||
try {
|
||||
const val = localStorage.getItem(this.SHOW_RECOMMENDED_SONGS_KEY);
|
||||
return val === null ? true : val === 'true';
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
setShowRecommendedSongs(enabled) {
|
||||
localStorage.setItem(this.SHOW_RECOMMENDED_SONGS_KEY, enabled ? 'true' : 'false');
|
||||
},
|
||||
|
||||
shouldShowRecommendedAlbums() {
|
||||
try {
|
||||
const val = localStorage.getItem(this.SHOW_RECOMMENDED_ALBUMS_KEY);
|
||||
return val === null ? true : val === 'true';
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
setShowRecommendedAlbums(enabled) {
|
||||
localStorage.setItem(this.SHOW_RECOMMENDED_ALBUMS_KEY, enabled ? 'true' : 'false');
|
||||
},
|
||||
|
||||
shouldShowRecommendedArtists() {
|
||||
try {
|
||||
const val = localStorage.getItem(this.SHOW_RECOMMENDED_ARTISTS_KEY);
|
||||
return val === null ? true : val === 'true';
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
setShowRecommendedArtists(enabled) {
|
||||
localStorage.setItem(this.SHOW_RECOMMENDED_ARTISTS_KEY, enabled ? 'true' : 'false');
|
||||
},
|
||||
|
||||
shouldShowJumpBackIn() {
|
||||
try {
|
||||
const val = localStorage.getItem(this.SHOW_JUMP_BACK_IN_KEY);
|
||||
return val === null ? true : val === 'true';
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
setShowJumpBackIn(enabled) {
|
||||
localStorage.setItem(this.SHOW_JUMP_BACK_IN_KEY, enabled ? 'true' : 'false');
|
||||
},
|
||||
};
|
||||
|
||||
export const sidebarSectionSettings = {
|
||||
SHOW_HOME_KEY: 'sidebar-show-home',
|
||||
SHOW_LIBRARY_KEY: 'sidebar-show-library',
|
||||
SHOW_RECENT_KEY: 'sidebar-show-recent',
|
||||
SHOW_UNRELEASED_KEY: 'sidebar-show-unreleased',
|
||||
SHOW_DONATE_KEY: 'sidebar-show-donate',
|
||||
SHOW_SETTINGS_KEY: 'sidebar-show-settings',
|
||||
SHOW_ACCOUNT_KEY: 'sidebar-show-account',
|
||||
SHOW_ABOUT_KEY: 'sidebar-show-about',
|
||||
SHOW_DOWNLOAD_KEY: 'sidebar-show-download',
|
||||
SHOW_DISCORD_KEY: 'sidebar-show-discord',
|
||||
|
||||
shouldShowHome() {
|
||||
try {
|
||||
const val = localStorage.getItem(this.SHOW_HOME_KEY);
|
||||
return val === null ? true : val === 'true';
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
setShowHome(enabled) {
|
||||
localStorage.setItem(this.SHOW_HOME_KEY, enabled ? 'true' : 'false');
|
||||
},
|
||||
|
||||
shouldShowLibrary() {
|
||||
try {
|
||||
const val = localStorage.getItem(this.SHOW_LIBRARY_KEY);
|
||||
return val === null ? true : val === 'true';
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
setShowLibrary(enabled) {
|
||||
localStorage.setItem(this.SHOW_LIBRARY_KEY, enabled ? 'true' : 'false');
|
||||
},
|
||||
|
||||
shouldShowRecent() {
|
||||
try {
|
||||
const val = localStorage.getItem(this.SHOW_RECENT_KEY);
|
||||
return val === null ? true : val === 'true';
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
setShowRecent(enabled) {
|
||||
localStorage.setItem(this.SHOW_RECENT_KEY, enabled ? 'true' : 'false');
|
||||
},
|
||||
|
||||
shouldShowUnreleased() {
|
||||
try {
|
||||
const val = localStorage.getItem(this.SHOW_UNRELEASED_KEY);
|
||||
return val === null ? true : val === 'true';
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
setShowUnreleased(enabled) {
|
||||
localStorage.setItem(this.SHOW_UNRELEASED_KEY, enabled ? 'true' : 'false');
|
||||
},
|
||||
|
||||
shouldShowDonate() {
|
||||
try {
|
||||
const val = localStorage.getItem(this.SHOW_DONATE_KEY);
|
||||
return val === null ? true : val === 'true';
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
setShowDonate(enabled) {
|
||||
localStorage.setItem(this.SHOW_DONATE_KEY, enabled ? 'true' : 'false');
|
||||
},
|
||||
|
||||
shouldShowSettings() {
|
||||
try {
|
||||
const val = localStorage.getItem(this.SHOW_SETTINGS_KEY);
|
||||
return val === null ? true : val === 'true';
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
setShowSettings(enabled) {
|
||||
localStorage.setItem(this.SHOW_SETTINGS_KEY, enabled ? 'true' : 'false');
|
||||
},
|
||||
|
||||
shouldShowAccount() {
|
||||
try {
|
||||
const val = localStorage.getItem(this.SHOW_ACCOUNT_KEY);
|
||||
return val === null ? true : val === 'true';
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
setShowAccount(enabled) {
|
||||
localStorage.setItem(this.SHOW_ACCOUNT_KEY, enabled ? 'true' : 'false');
|
||||
},
|
||||
|
||||
shouldShowAbout() {
|
||||
try {
|
||||
const val = localStorage.getItem(this.SHOW_ABOUT_KEY);
|
||||
return val === null ? true : val === 'true';
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
setShowAbout(enabled) {
|
||||
localStorage.setItem(this.SHOW_ABOUT_KEY, enabled ? 'true' : 'false');
|
||||
},
|
||||
|
||||
shouldShowDownload() {
|
||||
try {
|
||||
const val = localStorage.getItem(this.SHOW_DOWNLOAD_KEY);
|
||||
return val === null ? true : val === 'true';
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
setShowDownload(enabled) {
|
||||
localStorage.setItem(this.SHOW_DOWNLOAD_KEY, enabled ? 'true' : 'false');
|
||||
},
|
||||
|
||||
shouldShowDiscord() {
|
||||
try {
|
||||
const val = localStorage.getItem(this.SHOW_DISCORD_KEY);
|
||||
return val === null ? true : val === 'true';
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
setShowDiscord(enabled) {
|
||||
localStorage.setItem(this.SHOW_DISCORD_KEY, enabled ? 'true' : 'false');
|
||||
},
|
||||
|
||||
applySidebarVisibility() {
|
||||
const items = [
|
||||
{ id: 'sidebar-nav-home', check: this.shouldShowHome() },
|
||||
{ id: 'sidebar-nav-library', check: this.shouldShowLibrary() },
|
||||
{ id: 'sidebar-nav-recent', check: this.shouldShowRecent() },
|
||||
{ id: 'sidebar-nav-unreleased', check: this.shouldShowUnreleased() },
|
||||
{ id: 'sidebar-nav-donate', check: this.shouldShowDonate() },
|
||||
{ id: 'sidebar-nav-settings', check: this.shouldShowSettings() },
|
||||
{ id: 'sidebar-nav-account', check: this.shouldShowAccount() },
|
||||
{ id: 'sidebar-nav-about', check: this.shouldShowAbout() },
|
||||
{ id: 'sidebar-nav-download', check: this.shouldShowDownload() },
|
||||
{ id: 'sidebar-nav-discord', check: this.shouldShowDiscord() },
|
||||
];
|
||||
|
||||
items.forEach(({ id, check }) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
el.style.display = check ? '' : 'none';
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
// System theme listener
|
||||
if (typeof window !== 'undefined' && window.matchMedia) {
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
||||
|
|
|
|||
67
js/ui.js
67
js/ui.js
|
|
@ -19,7 +19,13 @@ import {
|
|||
escapeHtml,
|
||||
} from './utils.js';
|
||||
import { openLyricsPanel } from './lyrics.js';
|
||||
import { recentActivityManager, backgroundSettings, cardSettings, visualizerSettings } from './storage.js';
|
||||
import {
|
||||
recentActivityManager,
|
||||
backgroundSettings,
|
||||
cardSettings,
|
||||
visualizerSettings,
|
||||
homePageSettings,
|
||||
} from './storage.js';
|
||||
import { db } from './db.js';
|
||||
import { getVibrantColorFromImage } from './vibrant-color.js';
|
||||
import { syncManager } from './accounts/pocketbase.js';
|
||||
|
|
@ -1254,6 +1260,15 @@ export class UIRenderer {
|
|||
|
||||
async renderHomeSongs(forceRefresh = false) {
|
||||
const songsContainer = document.getElementById('home-recommended-songs');
|
||||
const section = songsContainer?.closest('.content-section');
|
||||
|
||||
if (!homePageSettings.shouldShowRecommendedSongs()) {
|
||||
if (section) section.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
if (section) section.style.display = '';
|
||||
|
||||
if (songsContainer) {
|
||||
if (forceRefresh) songsContainer.innerHTML = this.createSkeletonTracks(5, true);
|
||||
else if (songsContainer.children.length > 0 && !songsContainer.querySelector('.skeleton')) return; // Already loaded
|
||||
|
|
@ -1279,6 +1294,15 @@ export class UIRenderer {
|
|||
|
||||
async renderHomeAlbums(forceRefresh = false) {
|
||||
const albumsContainer = document.getElementById('home-recommended-albums');
|
||||
const section = albumsContainer?.closest('.content-section');
|
||||
|
||||
if (!homePageSettings.shouldShowRecommendedAlbums()) {
|
||||
if (section) section.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
if (section) section.style.display = '';
|
||||
|
||||
if (albumsContainer) {
|
||||
if (forceRefresh) albumsContainer.innerHTML = this.createSkeletonCards(6);
|
||||
else if (albumsContainer.children.length > 0 && !albumsContainer.querySelector('.skeleton')) return;
|
||||
|
|
@ -1317,6 +1341,15 @@ export class UIRenderer {
|
|||
|
||||
async renderHomeArtists(forceRefresh = false) {
|
||||
const artistsContainer = document.getElementById('home-recommended-artists');
|
||||
const section = artistsContainer?.closest('.content-section');
|
||||
|
||||
if (!homePageSettings.shouldShowRecommendedArtists()) {
|
||||
if (section) section.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
if (section) section.style.display = '';
|
||||
|
||||
if (artistsContainer) {
|
||||
if (forceRefresh) artistsContainer.innerHTML = this.createSkeletonCards(6, true);
|
||||
else if (artistsContainer.children.length > 0 && !artistsContainer.querySelector('.skeleton')) return;
|
||||
|
|
@ -1359,6 +1392,15 @@ export class UIRenderer {
|
|||
|
||||
renderHomeRecent() {
|
||||
const recentContainer = document.getElementById('home-recent-mixed');
|
||||
const section = recentContainer?.closest('.content-section');
|
||||
|
||||
if (!homePageSettings.shouldShowJumpBackIn()) {
|
||||
if (section) section.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
if (section) section.style.display = '';
|
||||
|
||||
if (recentContainer) {
|
||||
const recents = recentActivityManager.getRecents();
|
||||
const items = [];
|
||||
|
|
@ -2513,11 +2555,17 @@ export class UIRenderer {
|
|||
async renderRecentPage() {
|
||||
this.showPage('recent');
|
||||
const container = document.getElementById('recent-tracks-container');
|
||||
const clearBtn = document.getElementById('clear-history-btn');
|
||||
container.innerHTML = this.createSkeletonTracks(10, true);
|
||||
|
||||
try {
|
||||
const history = await db.getHistory();
|
||||
|
||||
// Show/hide clear button based on whether there's history
|
||||
if (clearBtn) {
|
||||
clearBtn.style.display = history.length > 0 ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
if (history.length === 0) {
|
||||
container.innerHTML = createPlaceholder("You haven't played any tracks yet.");
|
||||
return;
|
||||
|
|
@ -2570,9 +2618,26 @@ export class UIRenderer {
|
|||
container.appendChild(tempContainer.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
// Setup clear button handler
|
||||
if (clearBtn) {
|
||||
clearBtn.onclick = async () => {
|
||||
if (confirm('Clear all recently played tracks? This cannot be undone.')) {
|
||||
try {
|
||||
await db.clearHistory();
|
||||
container.innerHTML = createPlaceholder("You haven't played any tracks yet.");
|
||||
clearBtn.style.display = 'none';
|
||||
} catch (err) {
|
||||
console.error('Failed to clear history:', err);
|
||||
alert('Failed to clear history');
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load history:', error);
|
||||
container.innerHTML = createPlaceholder('Failed to load history.');
|
||||
if (clearBtn) clearBtn.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
15
package-lock.json
generated
15
package-lock.json
generated
|
|
@ -74,6 +74,7 @@
|
|||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
|
|
@ -1603,6 +1604,7 @@
|
|||
"integrity": "sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@keyv/serialize": "^1.1.1"
|
||||
}
|
||||
|
|
@ -1644,6 +1646,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
|
|
@ -1687,6 +1690,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -3138,6 +3142,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@svta/cml-xml/-/cml-xml-1.0.1.tgz",
|
||||
"integrity": "sha512-11LkJa5kDEcsRMWkVI1ABH3KLCxGoiSVe4kQ293ItVj8ncTTQ7htmCGiJDjS+Cmy35UgF3e/vc0ysJIiWRTx2g==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
|
|
@ -3186,6 +3191,7 @@
|
|||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
|
|
@ -3209,6 +3215,7 @@
|
|||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
|
|
@ -3496,6 +3503,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
|
|
@ -4280,6 +4288,7 @@
|
|||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
|
|
@ -6636,6 +6645,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
|
|
@ -6719,6 +6729,7 @@
|
|||
"integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
|
|
@ -7668,6 +7679,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@csstools/css-parser-algorithms": "^3.0.5",
|
||||
"@csstools/css-syntax-patches-for-csstree": "^1.0.19",
|
||||
|
|
@ -8082,6 +8094,7 @@
|
|||
"integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
"acorn": "^8.15.0",
|
||||
|
|
@ -8406,6 +8419,7 @@
|
|||
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
|
|
@ -8793,6 +8807,7 @@
|
|||
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
|
|
|
|||
32
styles.css
32
styles.css
|
|
@ -3305,6 +3305,38 @@ input:checked + .slider::before {
|
|||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Lyrics timing adjustment controls */
|
||||
.lyrics-timing-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
margin-right: auto;
|
||||
padding-right: 0.5rem;
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.lyrics-timing-display {
|
||||
font-family: monospace;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--foreground);
|
||||
min-width: 3.5rem;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.lyrics-timing-controls .btn-icon {
|
||||
padding: 0.4rem;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.lyrics-timing-controls .btn-icon:hover {
|
||||
background: var(--secondary);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
|
|
|||
Loading…
Reference in a new issue