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>
|
</div>
|
||||||
<nav class="sidebar-nav main">
|
<nav class="sidebar-nav main">
|
||||||
<ul>
|
<ul>
|
||||||
<li class="nav-item">
|
<li class="nav-item" id="sidebar-nav-home">
|
||||||
<a href="/">
|
<a href="/">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|
@ -777,7 +777,7 @@
|
||||||
<span>Home</span>
|
<span>Home</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item" id="sidebar-nav-library">
|
||||||
<a href="/library">
|
<a href="/library">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|
@ -798,7 +798,7 @@
|
||||||
<span>Library</span>
|
<span>Library</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item" id="sidebar-nav-recent">
|
||||||
<a href="/recent">
|
<a href="/recent">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|
@ -818,7 +818,7 @@
|
||||||
<span>Recent</span>
|
<span>Recent</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item" id="sidebar-nav-unreleased">
|
||||||
<a href="/unreleased">
|
<a href="/unreleased">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|
@ -839,7 +839,32 @@
|
||||||
<span>Unreleased</span>
|
<span>Unreleased</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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">
|
<a href="/settings">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|
@ -860,7 +885,7 @@
|
||||||
<span>Settings</span>
|
<span>Settings</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item" id="sidebar-nav-account">
|
||||||
<a href="/account">
|
<a href="/account">
|
||||||
<svg
|
<svg
|
||||||
width="24"
|
width="24"
|
||||||
|
|
@ -898,7 +923,7 @@
|
||||||
</nav>
|
</nav>
|
||||||
<nav class="sidebar-nav">
|
<nav class="sidebar-nav">
|
||||||
<ul>
|
<ul>
|
||||||
<li class="nav-item">
|
<li class="nav-item" id="sidebar-nav-about">
|
||||||
<a href="/about">
|
<a href="/about">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|
@ -919,7 +944,7 @@
|
||||||
<span>About</span>
|
<span>About</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item" id="sidebar-nav-download">
|
||||||
<a href="/download">
|
<a href="/download">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|
@ -939,7 +964,7 @@
|
||||||
<span>Download</span>
|
<span>Download</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item" id="sidebar-nav-discord">
|
||||||
<a href="https://monochrome.samidy.com/discord" target="_blank">
|
<a href="https://monochrome.samidy.com/discord" target="_blank">
|
||||||
<svg
|
<svg
|
||||||
width="64px"
|
width="64px"
|
||||||
|
|
@ -1378,7 +1403,31 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="page-recent" class="page">
|
<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 class="track-list" id="recent-tracks-container"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -2083,6 +2132,164 @@
|
||||||
<input type="checkbox" id="listenbrainz-enabled-toggle" />
|
<input type="checkbox" id="listenbrainz-enabled-toggle" />
|
||||||
<span class="slider"></span>
|
<span class="slider"></span>
|
||||||
</label>
|
</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>
|
||||||
<div class="setting-item" id="listenbrainz-token-setting" style="display: none">
|
<div class="setting-item" id="listenbrainz-token-setting" style="display: none">
|
||||||
<div class="info">
|
<div class="info">
|
||||||
|
|
@ -2809,7 +3016,7 @@
|
||||||
flex-wrap: wrap;
|
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>
|
<button id="donate-btn" class="btn-secondary">Donate to Monochrome</button>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,26 @@ const syncManager = {
|
||||||
});
|
});
|
||||||
return JSON.parse(recovered);
|
return JSON.parse(recovered);
|
||||||
} catch {
|
} 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;
|
return fallback;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -361,7 +381,7 @@ const syncManager = {
|
||||||
image: playlist.cover,
|
image: playlist.cover,
|
||||||
cover: playlist.cover,
|
cover: playlist.cover,
|
||||||
playlist_cover: playlist.cover,
|
playlist_cover: playlist.cover,
|
||||||
tracks: playlist.tracks,
|
tracks: JSON.stringify(playlist.tracks || []),
|
||||||
isPublic: true,
|
isPublic: true,
|
||||||
data: {
|
data: {
|
||||||
title: playlist.name,
|
title: playlist.name,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
//js/app.js
|
//js/app.js
|
||||||
import { LosslessAPI } from './api.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 { UIRenderer } from './ui.js';
|
||||||
import { Player } from './player.js';
|
import { Player } from './player.js';
|
||||||
import { MultiScrobbler } from './multi-scrobbler.js';
|
import { MultiScrobbler } from './multi-scrobbler.js';
|
||||||
|
|
@ -313,6 +313,9 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
const currentTheme = themeManager.getTheme();
|
const currentTheme = themeManager.getTheme();
|
||||||
themeManager.setTheme(currentTheme);
|
themeManager.setTheme(currentTheme);
|
||||||
|
|
||||||
|
// Restore sidebar state
|
||||||
|
sidebarSettings.restoreState();
|
||||||
|
|
||||||
initializeSettings(scrobbler, player, api, ui);
|
initializeSettings(scrobbler, player, api, ui);
|
||||||
initializePlayerEvents(player, audioPlayer, scrobbler, ui);
|
initializePlayerEvents(player, audioPlayer, scrobbler, ui);
|
||||||
initializeTrackInteractions(
|
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="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>';
|
: '<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', () => {
|
document.getElementById('nav-back')?.addEventListener('click', () => {
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,17 @@ class AudioContextManager {
|
||||||
if (this.isInitialized) return;
|
if (this.isInitialized) return;
|
||||||
if (!audioElement) 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 {
|
try {
|
||||||
this.audio = audioElement;
|
this.audio = audioElement;
|
||||||
|
|
||||||
|
|
@ -179,11 +190,28 @@ class AudioContextManager {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resume audio context (required after user interaction)
|
* Resume audio context (required after user interaction)
|
||||||
|
* @returns {Promise<boolean>} - Returns true if context is running
|
||||||
*/
|
*/
|
||||||
resume() {
|
async resume() {
|
||||||
if (this.audioContext && this.audioContext.state === 'suspended') {
|
if (!this.audioContext) return false;
|
||||||
this.audioContext.resume();
|
|
||||||
|
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
|
// Favorites API
|
||||||
async toggleFavorite(type, item) {
|
async toggleFavorite(type, item) {
|
||||||
const plural = type === 'mix' ? 'mixes' : `${type}s`;
|
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.geniusManager = new GeniusManager();
|
||||||
this.isGeniusMode = false;
|
this.isGeniusMode = false;
|
||||||
this.currentGeniusData = null;
|
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)
|
// 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 renderControls = (container) => {
|
||||||
const isRomajiMode = manager.getRomajiMode();
|
const isRomajiMode = manager.getRomajiMode();
|
||||||
manager.isRomajiMode = isRomajiMode;
|
manager.isRomajiMode = isRomajiMode;
|
||||||
const isGeniusMode = manager.isGeniusMode;
|
const isGeniusMode = manager.isGeniusMode;
|
||||||
|
const offsetDisplay = manager.getOffsetDisplayString(manager.timingOffset);
|
||||||
|
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<button id="close-side-panel-btn" class="btn-icon" title="Close">
|
<button id="close-side-panel-btn" class="btn-icon" title="Close">
|
||||||
${SVG_CLOSE}
|
${SVG_CLOSE}
|
||||||
</button>
|
</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)' : ''}">
|
<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">
|
<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>
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
|
|
@ -740,6 +797,32 @@ export function openLyricsPanel(track, audioPlayer, lyricsManager, forceOpen = f
|
||||||
clearLyricsPanelSync(audioPlayer, sidePanelManager.panel);
|
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
|
// Romaji toggle button handler
|
||||||
const romajiBtn = container.querySelector('#romaji-toggle-btn');
|
const romajiBtn = container.querySelector('#romaji-toggle-btn');
|
||||||
if (romajiBtn) {
|
if (romajiBtn) {
|
||||||
|
|
@ -945,11 +1028,17 @@ function setupSync(track, audioPlayer, amLyrics, lyricsManager) {
|
||||||
let lastTimestamp = performance.now();
|
let lastTimestamp = performance.now();
|
||||||
let animationFrameId = null;
|
let animationFrameId = null;
|
||||||
|
|
||||||
|
// Get timing offset from lyrics manager (in milliseconds)
|
||||||
|
const getTimingOffset = () => {
|
||||||
|
return lyricsManager?.timingOffset || 0;
|
||||||
|
};
|
||||||
|
|
||||||
const updateTime = () => {
|
const updateTime = () => {
|
||||||
const currentMs = audioPlayer.currentTime * 1000;
|
const currentMs = audioPlayer.currentTime * 1000;
|
||||||
baseTimeMs = currentMs;
|
baseTimeMs = currentMs;
|
||||||
lastTimestamp = performance.now();
|
lastTimestamp = performance.now();
|
||||||
amLyrics.currentTime = currentMs;
|
// Apply timing offset: positive offset delays lyrics, negative advances them
|
||||||
|
amLyrics.currentTime = currentMs - getTimingOffset();
|
||||||
};
|
};
|
||||||
|
|
||||||
const tick = () => {
|
const tick = () => {
|
||||||
|
|
@ -957,7 +1046,8 @@ function setupSync(track, audioPlayer, amLyrics, lyricsManager) {
|
||||||
const now = performance.now();
|
const now = performance.now();
|
||||||
const elapsed = now - lastTimestamp;
|
const elapsed = now - lastTimestamp;
|
||||||
const nextMs = baseTimeMs + elapsed;
|
const nextMs = baseTimeMs + elapsed;
|
||||||
amLyrics.currentTime = nextMs;
|
// Apply timing offset: positive offset delays lyrics, negative advances them
|
||||||
|
amLyrics.currentTime = nextMs - getTimingOffset();
|
||||||
animationFrameId = requestAnimationFrame(tick);
|
animationFrameId = requestAnimationFrame(tick);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
43
js/player.js
43
js/player.js
|
|
@ -9,6 +9,7 @@ import {
|
||||||
createQualityBadgeHTML,
|
createQualityBadgeHTML,
|
||||||
} from './utils.js';
|
} from './utils.js';
|
||||||
import { queueManager, replayGainSettings } from './storage.js';
|
import { queueManager, replayGainSettings } from './storage.js';
|
||||||
|
import { audioContextManager } from './audio-context.js';
|
||||||
|
|
||||||
export class Player {
|
export class Player {
|
||||||
constructor(audioElement, api, quality = 'HI_RES_LOSSLESS') {
|
constructor(audioElement, api, quality = 'HI_RES_LOSSLESS') {
|
||||||
|
|
@ -50,6 +51,17 @@ export class Player {
|
||||||
window.addEventListener('beforeunload', () => {
|
window.addEventListener('beforeunload', () => {
|
||||||
this.saveQueueState();
|
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) {
|
setVolume(value) {
|
||||||
|
|
@ -176,19 +188,42 @@ export class Player {
|
||||||
setupMediaSession() {
|
setupMediaSession() {
|
||||||
if (!('mediaSession' in navigator)) return;
|
if (!('mediaSession' in navigator)) return;
|
||||||
|
|
||||||
navigator.mediaSession.setActionHandler('play', () => {
|
navigator.mediaSession.setActionHandler('play', async () => {
|
||||||
this.audio.play().catch(console.error);
|
// 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', () => {
|
navigator.mediaSession.setActionHandler('pause', () => {
|
||||||
this.audio.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();
|
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();
|
this.playNext();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
129
js/settings.js
129
js/settings.js
|
|
@ -17,6 +17,8 @@ import {
|
||||||
playlistSettings,
|
playlistSettings,
|
||||||
equalizerSettings,
|
equalizerSettings,
|
||||||
listenBrainzSettings,
|
listenBrainzSettings,
|
||||||
|
homePageSettings,
|
||||||
|
sidebarSectionSettings,
|
||||||
} from './storage.js';
|
} from './storage.js';
|
||||||
import { audioContextManager, EQ_PRESETS } from './audio-context.js';
|
import { audioContextManager, EQ_PRESETS } from './audio-context.js';
|
||||||
import { db } from './db.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
|
// Filename template setting
|
||||||
const filenameTemplate = document.getElementById('filename-template');
|
const filenameTemplate = document.getElementById('filename-template');
|
||||||
if (filenameTemplate) {
|
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 = {
|
export const queueManager = {
|
||||||
STORAGE_KEY: 'monochrome-queue',
|
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
|
// System theme listener
|
||||||
if (typeof window !== 'undefined' && window.matchMedia) {
|
if (typeof window !== 'undefined' && window.matchMedia) {
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
||||||
|
|
|
||||||
67
js/ui.js
67
js/ui.js
|
|
@ -19,7 +19,13 @@ import {
|
||||||
escapeHtml,
|
escapeHtml,
|
||||||
} from './utils.js';
|
} from './utils.js';
|
||||||
import { openLyricsPanel } from './lyrics.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 { db } from './db.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';
|
||||||
|
|
@ -1254,6 +1260,15 @@ export class UIRenderer {
|
||||||
|
|
||||||
async renderHomeSongs(forceRefresh = false) {
|
async renderHomeSongs(forceRefresh = false) {
|
||||||
const songsContainer = document.getElementById('home-recommended-songs');
|
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 (songsContainer) {
|
||||||
if (forceRefresh) songsContainer.innerHTML = this.createSkeletonTracks(5, true);
|
if (forceRefresh) songsContainer.innerHTML = this.createSkeletonTracks(5, true);
|
||||||
else if (songsContainer.children.length > 0 && !songsContainer.querySelector('.skeleton')) return; // Already loaded
|
else if (songsContainer.children.length > 0 && !songsContainer.querySelector('.skeleton')) return; // Already loaded
|
||||||
|
|
@ -1279,6 +1294,15 @@ export class UIRenderer {
|
||||||
|
|
||||||
async renderHomeAlbums(forceRefresh = false) {
|
async renderHomeAlbums(forceRefresh = false) {
|
||||||
const albumsContainer = document.getElementById('home-recommended-albums');
|
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 (albumsContainer) {
|
||||||
if (forceRefresh) albumsContainer.innerHTML = this.createSkeletonCards(6);
|
if (forceRefresh) albumsContainer.innerHTML = this.createSkeletonCards(6);
|
||||||
else if (albumsContainer.children.length > 0 && !albumsContainer.querySelector('.skeleton')) return;
|
else if (albumsContainer.children.length > 0 && !albumsContainer.querySelector('.skeleton')) return;
|
||||||
|
|
@ -1317,6 +1341,15 @@ export class UIRenderer {
|
||||||
|
|
||||||
async renderHomeArtists(forceRefresh = false) {
|
async renderHomeArtists(forceRefresh = false) {
|
||||||
const artistsContainer = document.getElementById('home-recommended-artists');
|
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 (artistsContainer) {
|
||||||
if (forceRefresh) artistsContainer.innerHTML = this.createSkeletonCards(6, true);
|
if (forceRefresh) artistsContainer.innerHTML = this.createSkeletonCards(6, true);
|
||||||
else if (artistsContainer.children.length > 0 && !artistsContainer.querySelector('.skeleton')) return;
|
else if (artistsContainer.children.length > 0 && !artistsContainer.querySelector('.skeleton')) return;
|
||||||
|
|
@ -1359,6 +1392,15 @@ export class UIRenderer {
|
||||||
|
|
||||||
renderHomeRecent() {
|
renderHomeRecent() {
|
||||||
const recentContainer = document.getElementById('home-recent-mixed');
|
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) {
|
if (recentContainer) {
|
||||||
const recents = recentActivityManager.getRecents();
|
const recents = recentActivityManager.getRecents();
|
||||||
const items = [];
|
const items = [];
|
||||||
|
|
@ -2513,11 +2555,17 @@ export class UIRenderer {
|
||||||
async renderRecentPage() {
|
async renderRecentPage() {
|
||||||
this.showPage('recent');
|
this.showPage('recent');
|
||||||
const container = document.getElementById('recent-tracks-container');
|
const container = document.getElementById('recent-tracks-container');
|
||||||
|
const clearBtn = document.getElementById('clear-history-btn');
|
||||||
container.innerHTML = this.createSkeletonTracks(10, true);
|
container.innerHTML = this.createSkeletonTracks(10, true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const history = await db.getHistory();
|
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) {
|
if (history.length === 0) {
|
||||||
container.innerHTML = createPlaceholder("You haven't played any tracks yet.");
|
container.innerHTML = createPlaceholder("You haven't played any tracks yet.");
|
||||||
return;
|
return;
|
||||||
|
|
@ -2570,9 +2618,26 @@ export class UIRenderer {
|
||||||
container.appendChild(tempContainer.firstChild);
|
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) {
|
} catch (error) {
|
||||||
console.error('Failed to load history:', error);
|
console.error('Failed to load history:', error);
|
||||||
container.innerHTML = createPlaceholder('Failed to load history.');
|
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==",
|
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.5",
|
"@babel/generator": "^7.28.5",
|
||||||
|
|
@ -1603,6 +1604,7 @@
|
||||||
"integrity": "sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ==",
|
"integrity": "sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@keyv/serialize": "^1.1.1"
|
"@keyv/serialize": "^1.1.1"
|
||||||
}
|
}
|
||||||
|
|
@ -1644,6 +1646,7 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
|
|
@ -1687,6 +1690,7 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
|
|
@ -3138,6 +3142,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/@svta/cml-xml/-/cml-xml-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@svta/cml-xml/-/cml-xml-1.0.1.tgz",
|
||||||
"integrity": "sha512-11LkJa5kDEcsRMWkVI1ABH3KLCxGoiSVe4kQ293ItVj8ncTTQ7htmCGiJDjS+Cmy35UgF3e/vc0ysJIiWRTx2g==",
|
"integrity": "sha512-11LkJa5kDEcsRMWkVI1ABH3KLCxGoiSVe4kQ293ItVj8ncTTQ7htmCGiJDjS+Cmy35UgF3e/vc0ysJIiWRTx2g==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
},
|
},
|
||||||
|
|
@ -3186,6 +3191,7 @@
|
||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
|
|
@ -3209,6 +3215,7 @@
|
||||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"fast-uri": "^3.0.1",
|
"fast-uri": "^3.0.1",
|
||||||
|
|
@ -3496,6 +3503,7 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
|
|
@ -4280,6 +4288,7 @@
|
||||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
|
|
@ -6636,6 +6645,7 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
|
|
@ -6719,6 +6729,7 @@
|
||||||
"integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
|
"integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cssesc": "^3.0.0",
|
"cssesc": "^3.0.0",
|
||||||
"util-deprecate": "^1.0.2"
|
"util-deprecate": "^1.0.2"
|
||||||
|
|
@ -7668,6 +7679,7 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@csstools/css-parser-algorithms": "^3.0.5",
|
"@csstools/css-parser-algorithms": "^3.0.5",
|
||||||
"@csstools/css-syntax-patches-for-csstree": "^1.0.19",
|
"@csstools/css-syntax-patches-for-csstree": "^1.0.19",
|
||||||
|
|
@ -8082,6 +8094,7 @@
|
||||||
"integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
|
"integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/source-map": "^0.3.3",
|
"@jridgewell/source-map": "^0.3.3",
|
||||||
"acorn": "^8.15.0",
|
"acorn": "^8.15.0",
|
||||||
|
|
@ -8406,6 +8419,7 @@
|
||||||
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
|
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.27.0",
|
"esbuild": "^0.27.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
|
|
@ -8793,6 +8807,7 @@
|
||||||
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
|
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"rollup": "dist/bin/rollup"
|
"rollup": "dist/bin/rollup"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
32
styles.css
32
styles.css
|
|
@ -3305,6 +3305,38 @@ input:checked + .slider::before {
|
||||||
gap: 0.5rem;
|
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 {
|
.panel-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue