diff --git a/js/app.js b/js/app.js
index 0f790ec..f96d057 100644
--- a/js/app.js
+++ b/js/app.js
@@ -120,11 +120,11 @@ function initializeKeyboardShortcuts(player, audioPlayer) {
break;
case 'arrowup':
e.preventDefault();
- audioPlayer.volume = Math.min(1, audioPlayer.volume + 0.1);
+ player.setVolume(player.userVolume + 0.1);
break;
case 'arrowdown':
e.preventDefault();
- audioPlayer.volume = Math.max(0, audioPlayer.volume - 0.1);
+ player.setVolume(player.userVolume - 0.1);
break;
case 'm':
audioPlayer.muted = !audioPlayer.muted;
@@ -421,7 +421,7 @@ document.addEventListener('DOMContentLoaded', async () => {
if (publicToggle) publicToggle.checked = false;
if (shareBtn) shareBtn.style.display = 'none';
- modal.style.display = 'flex';
+ modal.classList.add('active');
document.getElementById('playlist-name-input').focus();
}
@@ -465,7 +465,7 @@ document.addEventListener('DOMContentLoaded', async () => {
if (window.location.hash === `#userplaylist/${editingId}`) {
ui.renderPlaylistPage(editingId, 'user');
}
- modal.style.display = 'none';
+ modal.classList.remove('active');
delete modal.dataset.editingId;
}
});
@@ -482,6 +482,7 @@ document.addEventListener('DOMContentLoaded', async () => {
const progressCurrent = document.getElementById('csv-progress-current');
const progressTotal = document.getElementById('csv-progress-total');
const currentTrackElement = progressElement.querySelector('.current-track');
+ const currentArtistElement = progressElement.querySelector('.current-artist');
try {
// Show progress bar
@@ -489,6 +490,7 @@ document.addEventListener('DOMContentLoaded', async () => {
progressFill.style.width = '0%';
progressCurrent.textContent = '0';
currentTrackElement.textContent = 'Reading CSV file...';
+ if (currentArtistElement) currentArtistElement.textContent = '';
const csvText = await file.text();
const lines = csvText.trim().split('\n');
@@ -500,6 +502,7 @@ document.addEventListener('DOMContentLoaded', async () => {
progressFill.style.width = `${Math.min(percentage, 100)}%`;
progressCurrent.textContent = progress.current.toString();
currentTrackElement.textContent = progress.currentTrack;
+ if (currentArtistElement) currentArtistElement.textContent = progress.currentArtist || '';
});
tracks = result.tracks;
@@ -537,14 +540,14 @@ document.addEventListener('DOMContentLoaded', async () => {
await db.performTransaction('user_playlists', 'readwrite', (store) => store.put(playlist));
syncManager.syncUserPlaylist(playlist, 'create');
ui.renderLibraryPage();
- modal.style.display = 'none';
+ modal.classList.remove('active');
});
}
}
}
if (e.target.closest('#playlist-modal-cancel')) {
- document.getElementById('playlist-modal').style.display = 'none';
+ document.getElementById('playlist-modal').classList.remove('active');
}
if (e.target.closest('.edit-playlist-btn')) {
@@ -574,7 +577,7 @@ document.addEventListener('DOMContentLoaded', async () => {
modal.dataset.editingId = playlistId;
document.getElementById('csv-import-section').style.display = 'none';
- modal.style.display = 'flex';
+ modal.classList.add('active');
document.getElementById('playlist-name-input').focus();
}
});
@@ -613,7 +616,7 @@ document.addEventListener('DOMContentLoaded', async () => {
modal.dataset.editingId = playlistId;
document.getElementById('csv-import-section').style.display = 'none';
- modal.style.display = 'flex';
+ modal.classList.add('active');
document.getElementById('playlist-name-input').focus();
}
});
@@ -970,37 +973,24 @@ function showInstallPrompt(deferredPrompt) {
}
function showMissingTracksNotification(missingTracks) {
- const modal = document.createElement('div');
- modal.className = 'missing-tracks-modal-overlay';
- modal.innerHTML = `
-
-
-
-
Unfortunately some songs weren't able to be added. This could be an issue with our import system, try searching for the song and adding it. But it could also be due to Monochrome not having it sadly :(
-
-
Missing Tracks:
-
- ${missingTracks.map(track => `${track} `).join('')}
-
-
-
-
- OK
-
-
- `;
- document.body.appendChild(modal);
+ const modal = document.getElementById('missing-tracks-modal');
+ const listUl = document.getElementById('missing-tracks-list-ul');
+
+ listUl.innerHTML = missingTracks.map(track => `
${track} `).join('');
+
+ const closeModal = () => modal.classList.remove('active');
- const closeModal = () => modal.remove();
-
- modal.addEventListener('click', (e) => {
- if (e.target === modal || e.target.classList.contains('close-missing-tracks') || e.target.id === 'close-missing-tracks-btn') {
+ // Remove old listeners if any (though usually these functions are called once per instance,
+ // but since we reuse the same modal element we should be careful or use a one-time listener)
+ const handleClose = (e) => {
+ if (e.target === modal || e.target.closest('.close-missing-tracks') || e.target.id === 'close-missing-tracks-btn' || e.target.classList.contains('modal-overlay')) {
closeModal();
+ modal.removeEventListener('click', handleClose);
}
- });
+ };
+
+ modal.addEventListener('click', handleClose);
+ modal.classList.add('active');
}
async function parseCSV(csvText, api, onProgress) {
@@ -1070,7 +1060,8 @@ async function parseCSV(csvText, api, onProgress) {
onProgress({
current: i,
total: totalTracks,
- currentTrack: trackTitle || 'Unknown track'
+ currentTrack: trackTitle || 'Unknown track',
+ currentArtist: artistNames || ''
});
}
@@ -1080,12 +1071,45 @@ async function parseCSV(csvText, api, onProgress) {
await new Promise(resolve => setTimeout(resolve, 300));
try {
- const searchQuery = `${trackTitle} ${artistNames}`;
- const searchResults = await api.searchTracks(searchQuery);
+ let foundTrack = null;
+
+ // 1. Initial Search: Title + All Artists
+ let searchQuery = `${trackTitle} ${artistNames}`;
+ let searchResults = await api.searchTracks(searchQuery);
if (searchResults.items && searchResults.items.length > 0) {
- // Use the first result
- const foundTrack = searchResults.items[0];
+ foundTrack = searchResults.items[0];
+ }
+
+ // 2. Retry with Main Artist only
+ if (!foundTrack) {
+ const mainArtist = artistNames.split(',')[0].trim();
+ // Only retry if mainArtist is actually different from artistNames (e.g. multiple artists)
+ if (mainArtist && mainArtist !== artistNames) {
+ searchQuery = `${trackTitle} ${mainArtist}`;
+ console.log(`Retry 1 (Main Artist): ${searchQuery}`);
+ searchResults = await api.searchTracks(searchQuery);
+ if (searchResults.items && searchResults.items.length > 0) {
+ foundTrack = searchResults.items[0];
+ }
+ }
+ }
+
+ // 3. Retry with Cleaned Title (if " - " exists) + Main Artist
+ if (!foundTrack && trackTitle.includes(' - ')) {
+ const mainArtist = artistNames.split(',')[0].trim();
+ const cleanedTitle = trackTitle.split(' - ')[0].trim();
+ if (cleanedTitle) {
+ searchQuery = `${cleanedTitle} ${mainArtist}`;
+ console.log(`Retry 2 (Cleaned Title): ${searchQuery}`);
+ searchResults = await api.searchTracks(searchQuery);
+ if (searchResults.items && searchResults.items.length > 0) {
+ foundTrack = searchResults.items[0];
+ }
+ }
+ }
+
+ if (foundTrack) {
tracks.push(foundTrack);
console.log(`Found track: "${trackTitle}" by ${artistNames}`);
} else {
@@ -1113,79 +1137,35 @@ async function parseCSV(csvText, api, onProgress) {
}
function showKeyboardShortcuts() {
- const modal = document.createElement('div');
- modal.className = 'shortcuts-modal-overlay';
- modal.innerHTML = `
-
-
-
-
- Space
- Play / Pause
-
-
- →
- Seek forward 10s
-
-
- ←
- Seek backward 10s
-
-
- Shift + →
- Next track
-
-
- Shift + ←
- Previous track
-
-
- ↑
- Volume up
-
-
- ↓
- Volume down
-
-
- M
- Mute / Unmute
-
-
- S
- Toggle shuffle
-
-
- R
- Toggle repeat
-
-
- Q
- Open queue
-
-
- L
- Toggle lyrics
-
-
- /
- Focus search
-
-
- Esc
- Close modals
-
-
-
- `;
- document.body.appendChild(modal);
- modal.addEventListener('click', (e) => {
- if (e.target === modal || e.target.classList.contains('close-shortcuts')) {
- modal.remove();
+ const modal = document.getElementById('shortcuts-modal');
+
+
+
+ const closeModal = () => {
+
+ modal.classList.remove('active');
+
+ modal.removeEventListener('click', handleClose);
+
+ };
+
+
+
+ const handleClose = (e) => {
+
+ if (e.target === modal || e.target.classList.contains('close-shortcuts') || e.target.classList.contains('modal-overlay')) {
+
+ closeModal();
+
}
- });
+
+ };
+
+
+
+ modal.addEventListener('click', handleClose);
+
+ modal.classList.add('active');
+
}
diff --git a/js/events.js b/js/events.js
index be71b44..f13af19 100644
--- a/js/events.js
+++ b/js/events.js
@@ -250,7 +250,8 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) {
});
const updateVolumeUI = () => {
- const { volume, muted } = audioPlayer;
+ const { muted } = audioPlayer;
+ const volume = player.userVolume;
volumeBtn.innerHTML = (muted || volume === 0) ? SVG_MUTE : SVG_VOLUME;
const effectiveVolume = muted ? 0 : volume * 100;
volumeFill.style.setProperty('--volume-level', `${effectiveVolume}%`);
@@ -268,7 +269,7 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) {
const savedVolume = parseFloat(localStorage.getItem('volume') || '0.7');
const savedMuted = localStorage.getItem('muted') === 'true';
- audioPlayer.volume = savedVolume;
+ player.setVolume(savedVolume);
audioPlayer.muted = savedMuted;
volumeFill.style.width = `${savedVolume * 100}%`;
@@ -337,10 +338,9 @@ function initializeSmoothSliders(audioPlayer, player) {
if (isAdjustingVolume) {
seek(volumeBar, e, position => {
- audioPlayer.volume = position;
+ player.setVolume(position);
volumeFill.style.width = `${position * 100}%`;
volumeBar.style.setProperty('--volume-level', `${position * 100}%`);
- localStorage.setItem('volume', position);
});
}
});
@@ -360,10 +360,9 @@ function initializeSmoothSliders(audioPlayer, player) {
const touch = e.touches[0];
const rect = volumeBar.getBoundingClientRect();
const position = Math.max(0, Math.min(1, (touch.clientX - rect.left) / rect.width));
- audioPlayer.volume = position;
+ player.setVolume(position);
volumeFill.style.width = `${position * 100}%`;
volumeBar.style.setProperty('--volume-level', `${position * 100}%`);
- localStorage.setItem('volume', position);
}
});
@@ -417,10 +416,9 @@ function initializeSmoothSliders(audioPlayer, player) {
volumeBar.addEventListener('mousedown', (e) => {
isAdjustingVolume = true;
seek(volumeBar, e, position => {
- audioPlayer.volume = position;
+ player.setVolume(position);
volumeFill.style.width = `${position * 100}%`;
volumeBar.style.setProperty('--volume-level', `${position * 100}%`);
- localStorage.setItem('volume', position);
});
});
@@ -430,52 +428,48 @@ function initializeSmoothSliders(audioPlayer, player) {
const touch = e.touches[0];
const rect = volumeBar.getBoundingClientRect();
const position = Math.max(0, Math.min(1, (touch.clientX - rect.left) / rect.width));
- audioPlayer.volume = position;
+ player.setVolume(position);
volumeFill.style.width = `${position * 100}%`;
volumeBar.style.setProperty('--volume-level', `${position * 100}%`);
- localStorage.setItem('volume', position);
});
volumeBar.addEventListener('click', e => {
if (!isAdjustingVolume) {
seek(volumeBar, e, position => {
- audioPlayer.volume = position;
+ player.setVolume(position);
volumeFill.style.width = `${position * 100}%`;
volumeBar.style.setProperty('--volume-level', `${position * 100}%`);
- localStorage.setItem('volume', position);
});
}
});
volumeBar.addEventListener('wheel', e => {
e.preventDefault();
const delta = e.deltaY > 0 ? -0.05 : 0.05;
- const newVolume = Math.max(0, Math.min(1, audioPlayer.volume + delta));
+ const newVolume = Math.max(0, Math.min(1, player.userVolume + delta));
if (delta > 0 && audioPlayer.muted) {
audioPlayer.muted = false;
localStorage.setItem('muted', false);
}
- audioPlayer.volume = newVolume;
+ player.setVolume(newVolume);
volumeFill.style.width = `${newVolume * 100}%`;
volumeBar.style.setProperty('--volume-level', `${newVolume * 100}%`);
- localStorage.setItem('volume', newVolume);
}, { passive: false });
volumeBtn?.addEventListener('wheel', e => {
e.preventDefault();
const delta = e.deltaY > 0 ? -0.05 : 0.05;
- const newVolume = Math.max(0, Math.min(1, audioPlayer.volume + delta));
+ const newVolume = Math.max(0, Math.min(1, player.userVolume + delta));
if (delta > 0 && audioPlayer.muted) {
audioPlayer.muted = false;
localStorage.setItem('muted', false);
}
- audioPlayer.volume = newVolume;
+ player.setVolume(newVolume);
volumeFill.style.width = `${newVolume * 100}%`;
volumeBar.style.setProperty('--volume-level', `${newVolume * 100}%`);
- localStorage.setItem('volume', newVolume);
}, { passive: false });
}
@@ -615,39 +609,43 @@ export async function handleTrackAction(action, item, player, api, lyricsManager
return;
}
- const modal = document.createElement('div');
- modal.className = 'playlist-select-modal';
- modal.innerHTML = `
-
-
-
Add to Playlist
-
- ${playlists.map(p => `
${p.name}
`).join('')}
-
-
- Cancel
-
-
-
- `;
- document.body.appendChild(modal);
+ const modal = document.getElementById('playlist-select-modal');
+ const list = document.getElementById('playlist-select-list');
+ const cancelBtn = document.getElementById('playlist-select-cancel');
+ const overlay = modal.querySelector('.modal-overlay');
- modal.addEventListener('click', async (e) => {
- if (e.target.id === 'cancel-add-playlist') {
- modal.remove();
- return;
- }
+ list.innerHTML = playlists.map(p => `
+
${p.name}
+ `).join('');
- const option = e.target.closest('.playlist-option');
+ const closeModal = () => {
+ modal.classList.remove('active');
+ cleanup();
+ };
+
+ const handleOptionClick = async (e) => {
+ const option = e.target.closest('.modal-option');
if (option) {
const playlistId = option.dataset.id;
await db.addTrackToPlaylist(playlistId, item);
const updatedPlaylist = await db.getPlaylist(playlistId);
syncManager.syncUserPlaylist(updatedPlaylist, 'update');
showNotification(`Added to playlist: ${option.textContent}`);
- modal.remove();
+ closeModal();
}
- });
+ };
+
+ const cleanup = () => {
+ cancelBtn.removeEventListener('click', closeModal);
+ overlay.removeEventListener('click', closeModal);
+ list.removeEventListener('click', handleOptionClick);
+ };
+
+ cancelBtn.addEventListener('click', closeModal);
+ overlay.addEventListener('click', closeModal);
+ list.addEventListener('click', handleOptionClick);
+
+ modal.classList.add('active');
}
}
@@ -853,34 +851,6 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
}
});
}
- // cast is back working woo :P
- const castBtn = document.getElementById('cast-btn');
- if (castBtn) {
- castBtn.addEventListener('click', async (e) => {
- e.stopPropagation();
-
- const audioPlayer = document.getElementById('audio-player');
- if (!audioPlayer.src) {
- alert('Please play a track first to enable casting.');
- return;
- }
-
- if ('remote' in audioPlayer) {
- audioPlayer.remote.prompt().catch(err => {
- if (err.name === 'NotAllowedError') return;
- if (err.name === 'NotFoundError') {
- alert('No remote playback devices (Chromecast/AirPlay) were found on your network.');
- return;
- }
- console.log('Cast prompt error:', err);
- });
- } else if (audioPlayer.webkitShowPlaybackTargetPicker) {
- audioPlayer.webkitShowPlaybackTargetPicker();
- } else {
- alert('Casting is not supported in this browser. Try Chrome for Chromecast or Safari for AirPlay.');
- }
- });
- }
@@ -900,61 +870,52 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
}
function showSleepTimerModal(player) {
- if (document.querySelector('.sleep-timer-modal')) return;
+ const modal = document.getElementById('sleep-timer-modal');
+ if (!modal) return;
- const modal = document.createElement('div');
- modal.className = 'sleep-timer-modal';
- modal.innerHTML = `
-
-
-
Sleep Timer
-
-
5 minutes
-
15 minutes
-
30 minutes
-
1 hour
-
2 hours
-
-
- Set
-
-
-
- Cancel
-
-
-
- `;
- document.body.appendChild(modal);
-
- modal.addEventListener('click', (e) => {
- if (e.target.id === 'cancel-sleep-timer' || e.target.classList.contains('modal-overlay')) {
- modal.remove();
- return;
- }
+ const closeModal = () => {
+ modal.classList.remove('active');
+ cleanup();
+ };
+ const handleOptionClick = (e) => {
const timerOption = e.target.closest('.timer-option');
if (timerOption) {
- const minutes = parseInt(timerOption.dataset.minutes);
+ let minutes;
+ if (timerOption.id === 'custom-timer-btn') {
+ const customInput = document.getElementById('custom-minutes');
+ minutes = parseInt(customInput.value);
+ if (!minutes || minutes < 1) {
+ showNotification('Please enter a valid number of minutes');
+ return;
+ }
+ } else {
+ minutes = parseInt(timerOption.dataset.minutes);
+ }
+
if (minutes) {
player.setSleepTimer(minutes);
showNotification(`Sleep timer set for ${minutes} minute${minutes === 1 ? '' : 's'}`);
- modal.remove();
+ closeModal();
}
}
+ };
- if (e.target.id === 'custom-timer-btn') {
- const customInput = document.getElementById('custom-minutes');
- const minutes = parseInt(customInput.value);
- if (minutes && minutes > 0 && minutes <= 480) {
- player.setSleepTimer(minutes);
- showNotification(`Sleep timer set for ${minutes} minute${minutes === 1 ? '' : 's'}`);
- modal.remove();
- } else {
- showNotification('Please enter a valid number of minutes (1-480)');
- }
+ const handleCancel = (e) => {
+ if (e.target.id === 'cancel-sleep-timer' || e.target.classList.contains('modal-overlay')) {
+ closeModal();
}
- });
+ };
+
+ const cleanup = () => {
+ modal.removeEventListener('click', handleOptionClick);
+ modal.removeEventListener('click', handleCancel);
+ };
+
+ modal.addEventListener('click', handleOptionClick);
+ modal.addEventListener('click', handleCancel);
+
+ modal.classList.add('active');
}
function positionMenu(menu, x, y, anchorRect = null) {
diff --git a/js/player.js b/js/player.js
index 50ca852..249d804 100644
--- a/js/player.js
+++ b/js/player.js
@@ -1,6 +1,6 @@
//js/player.js
import { REPEAT_MODE, formatTime, getTrackArtists, getTrackTitle, getTrackArtistsHTML } from './utils.js';
-import { queueManager } from './storage.js';
+import { queueManager, replayGainSettings } from './storage.js';
export class Player {
constructor(audioElement, api, quality = 'LOSSLESS') {
@@ -16,6 +16,8 @@ export class Player {
this.preloadCache = new Map();
this.preloadAbortController = null;
this.currentTrack = null;
+ this.currentRgValues = null;
+ this.userVolume = parseFloat(localStorage.getItem('volume') || '0.7');
// Sleep timer properties
this.sleepTimer = null;
@@ -30,6 +32,47 @@ export class Player {
});
}
+ setVolume(value) {
+ this.userVolume = Math.max(0, Math.min(1, value));
+ localStorage.setItem('volume', this.userVolume);
+ this.applyReplayGain();
+ }
+
+ applyReplayGain() {
+ const mode = replayGainSettings.getMode(); // 'off', 'track', 'album'
+ let gainDb = 0;
+ let peak = 1.0;
+
+ if (mode !== 'off' && this.currentRgValues) {
+ const { trackReplayGain, trackPeakAmplitude, albumReplayGain, albumPeakAmplitude } = this.currentRgValues;
+
+ if (mode === 'album' && albumReplayGain !== undefined) {
+ gainDb = albumReplayGain;
+ peak = albumPeakAmplitude || 1.0;
+ } else if (trackReplayGain !== undefined) {
+ gainDb = trackReplayGain;
+ peak = trackPeakAmplitude || 1.0;
+ }
+
+ // Apply Pre-Amp
+ gainDb += replayGainSettings.getPreamp();
+ }
+
+ // Convert dB to linear scale: 10^(dB/20)
+ let scale = Math.pow(10, gainDb / 20);
+
+ // Peak protection (prevent clipping)
+ if (scale * peak > 1.0) {
+ scale = 1.0 / peak;
+ }
+
+ // Calculate effective volume
+ const effectiveVolume = this.userVolume * scale;
+
+ // Apply to audio element
+ this.audio.volume = Math.max(0, Math.min(1, effectiveVolume));
+ }
+
loadQueueState() {
const savedState = queueManager.getQueue();
if (savedState) {
@@ -212,18 +255,29 @@ export class Player {
this.updatePlayingTrackIndicator();
try {
+ // Get track data for ReplayGain (should be cached by API)
+ const trackData = await this.api.getTrack(track.id, this.quality);
+
+ if (trackData && trackData.info) {
+ this.currentRgValues = {
+ trackReplayGain: trackData.info.trackReplayGain,
+ trackPeakAmplitude: trackData.info.trackPeakAmplitude,
+ albumReplayGain: trackData.info.albumReplayGain,
+ albumPeakAmplitude: trackData.info.albumPeakAmplitude
+ };
+ } else {
+ this.currentRgValues = null;
+ }
+ this.applyReplayGain();
+
let streamUrl;
if (this.preloadCache.has(track.id)) {
streamUrl = this.preloadCache.get(track.id);
+ } else if (trackData.originalTrackUrl) {
+ streamUrl = trackData.originalTrackUrl;
} else {
- const trackData = await this.api.getTrack(track.id, this.quality);
-
- if (trackData.originalTrackUrl) {
- streamUrl = trackData.originalTrackUrl;
- } else {
- streamUrl = this.api.extractStreamUrlFromManifest(trackData.info.manifest);
- }
+ streamUrl = this.api.extractStreamUrlFromManifest(trackData.info.manifest);
}
this.audio.src = streamUrl;
diff --git a/js/settings.js b/js/settings.js
index 4f18b49..279c707 100644
--- a/js/settings.js
+++ b/js/settings.js
@@ -8,7 +8,8 @@ import {
trackListSettings,
cardSettings,
waveformSettings,
- smoothScrollingSettings,
+ replayGainSettings,
+ smoothScrollingSettings
} from "./storage.js";
import { db } from "./db.js";
import { authManager } from "./firebase/auth.js";
@@ -273,6 +274,25 @@ export function initializeSettings(scrobbler, player, api, ui) {
});
}
+ // ReplayGain Settings
+ const replayGainMode = document.getElementById("replay-gain-mode");
+ if (replayGainMode) {
+ replayGainMode.value = replayGainSettings.getMode();
+ replayGainMode.addEventListener("change", (e) => {
+ replayGainSettings.setMode(e.target.value);
+ player.applyReplayGain();
+ });
+ }
+
+ const replayGainPreamp = document.getElementById("replay-gain-preamp");
+ if (replayGainPreamp) {
+ replayGainPreamp.value = replayGainSettings.getPreamp();
+ replayGainPreamp.addEventListener("change", (e) => {
+ replayGainSettings.setPreamp(parseFloat(e.target.value) || 3);
+ player.applyReplayGain();
+ });
+ }
+
// Now Playing Mode
const nowPlayingMode = document.getElementById("now-playing-mode");
if (nowPlayingMode) {
diff --git a/js/storage.js b/js/storage.js
index 8279c24..53021de 100644
--- a/js/storage.js
+++ b/js/storage.js
@@ -493,6 +493,24 @@ export const cardSettings = {
}
};
+export const replayGainSettings = {
+ STORAGE_KEY_MODE: 'replay-gain-mode', // 'off', 'track', 'album'
+ STORAGE_KEY_PREAMP: 'replay-gain-preamp',
+ getMode() {
+ return localStorage.getItem(this.STORAGE_KEY_MODE) || 'track';
+ },
+ setMode(mode) {
+ localStorage.setItem(this.STORAGE_KEY_MODE, mode);
+ },
+ getPreamp() {
+ const val = parseFloat(localStorage.getItem(this.STORAGE_KEY_PREAMP));
+ return isNaN(val) ? 3 : val;
+ },
+ setPreamp(db) {
+ localStorage.setItem(this.STORAGE_KEY_PREAMP, db);
+ }
+};
+
export const waveformSettings = {
STORAGE_KEY: 'waveform-seekbar-enabled',
diff --git a/js/vibrant-color.js b/js/vibrant-color.js
index b9250e1..73a2f54 100644
--- a/js/vibrant-color.js
+++ b/js/vibrant-color.js
@@ -67,29 +67,26 @@ export function getVibrantColorFromImage(imgElement) {
const ctx = canvas.getContext('2d');
if (!ctx) return null;
- canvas.width = imgElement.naturalWidth || imgElement.width;
- canvas.height = imgElement.naturalHeight || imgElement.height;
-
try {
- ctx.drawImage(imgElement, 0, 0);
-
- // Downscale for performance if image is large
- const maxDimension = 64;
- if (canvas.width > maxDimension || canvas.height > maxDimension) {
- const scale = Math.min(maxDimension / canvas.width, maxDimension / canvas.height);
- const w = Math.floor(canvas.width * scale);
- const h = Math.floor(canvas.height * scale);
- const smallCanvas = document.createElement('canvas');
- smallCanvas.width = w;
- smallCanvas.height = h;
- smallCanvas.getContext('2d').drawImage(imgElement, 0, 0, w, h);
- ctx.drawImage(smallCanvas, 0, 0, canvas.width, canvas.height);
- // Actually, better to just use the small canvas data
- var imageData = smallCanvas.getContext('2d').getImageData(0, 0, w, h);
- } else {
- var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+ const maxDimension = 64;
+ let w = imgElement.naturalWidth || imgElement.width;
+ let h = imgElement.naturalHeight || imgElement.height;
+
+ if (w > maxDimension || h > maxDimension) {
+ const scale = Math.min(maxDimension / w, maxDimension / h);
+ w = Math.floor(w * scale);
+ h = Math.floor(h * scale);
}
+ canvas.width = w;
+ canvas.height = h;
+
+ // Draw image directly at small size
+ // Note: For best quality downscaling, one might step down, but for color extraction,
+ // direct browser downscaling is sufficient and much faster/lighter.
+ ctx.drawImage(imgElement, 0, 0, w, h);
+
+ const imageData = ctx.getImageData(0, 0, w, h);
const pixels = imageData.data;
const candidates = [];
@@ -144,4 +141,4 @@ export function getVibrantColorFromImage(imgElement) {
} catch (e) {
throw e; // Re-throw to allow UI to handle CORS retry
}
-}
\ No newline at end of file
+}
diff --git a/styles.css b/styles.css
index e5a335a..e456eb6 100644
--- a/styles.css
+++ b/styles.css
@@ -2408,41 +2408,6 @@ input:checked + .slider::before {
align-items: stretch;
}
-.shortcuts-modal-overlay {
- position: fixed;
- inset: 0;
- background: rgba(0, 0, 0, 0.7);
- backdrop-filter: blur(4px);
- z-index: 10000;
- display: flex;
- justify-content: center;
- align-items: center;
- animation: fadeIn 0.2s ease;
-}
-
-.shortcuts-modal {
- background: var(--card);
- border-radius: var(--radius);
- width: 90%;
- max-width: 500px;
- max-height: 80vh;
- overflow-y: auto;
- box-shadow: var(--shadow-xl);
- animation: scaleIn 0.2s ease;
-}
-
-.shortcuts-header {
- padding: 1rem;
- border-bottom: 1px solid var(--border);
- display: flex;
- justify-content: space-between;
- align-items: center;
-}
-
-.shortcuts-header h3 {
- margin: 0;
-}
-
.close-shortcuts {
background: transparent;
border: none;
@@ -2943,7 +2908,91 @@ img:not([src]), img[src=''] {
background: var(--primary);
}
+/* Modals */
+.modal {
+ position: fixed;
+ inset: 0;
+ z-index: 10000;
+ display: none;
+ align-items: center;
+ justify-content: center;
+}
+.modal.active {
+ display: flex;
+}
+
+.modal-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.7);
+ backdrop-filter: blur(4px);
+ z-index: -1;
+}
+
+.modal-content {
+ background: var(--card);
+ padding: 2rem;
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ max-width: 400px;
+ width: 90%;
+ box-shadow: var(--shadow-xl);
+ animation: scaleIn 0.2s ease;
+ max-height: 90vh;
+ overflow-y: auto;
+}
+
+.modal-content.wide {
+ max-width: 600px;
+}
+
+.modal-content.medium {
+ max-width: 500px;
+}
+
+.modal-list {
+ margin: 1rem 0;
+ max-height: 200px;
+ overflow-y: auto;
+}
+
+.modal-option {
+ padding: 0.75rem;
+ cursor: pointer;
+ border-bottom: 1px solid var(--border);
+ transition: background 0.2s ease;
+}
+
+.modal-option:hover {
+ background: var(--secondary);
+}
+
+.modal-option:last-child {
+ border-bottom: none;
+}
+
+.modal-actions {
+ display: flex;
+ gap: 0.5rem;
+ justify-content: flex-end;
+ margin-top: 1.5rem;
+}
+
+.timer-options {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.timer-option {
+ text-align: center;
+}
+
+@keyframes scaleIn {
+ from { transform: scale(0.95); opacity: 0; }
+ to { transform: scale(1); opacity: 1; }
+}
#playlist-modal {
opacity: 1;
@@ -3060,33 +3109,6 @@ img:not([src]), img[src=''] {
-/* OH NO SOME SONGS WERENT FOUND FUCK ME FUCK ME */
-.missing-tracks-modal-overlay {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(0, 0, 0, 0.7);
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 10000;
- animation: fadeIn 0.2s ease;
-}
-
-.missing-tracks-modal {
- background: var(--card);
- border: 1px solid var(--border);
- border-radius: var(--radius);
- max-width: 600px;
- width: 90%;
- max-height: 85vh;
- overflow-y: auto;
- box-shadow: var(--shadow-xl);
- animation: scaleIn 0.2s ease;
-}
-
.missing-tracks-header {
display: flex;
justify-content: space-between;
@@ -3668,15 +3690,11 @@ img:not([src]), img[src=''] {
font-size: 1.75rem;
}
-.fullscreen-cover-content {
+ .fullscreen-cover-content {
flex-direction: column;
}
- .fullscreen-lyrics-toggle {
- right: 3.5rem;
- }
-
-.csv-import-progress {
+ .csv-import-progress {
bottom: 10px;
right: 10px;
left: 10px;
@@ -3684,7 +3702,7 @@ img:not([src]), img[src=''] {
min-width: 0;
}
-.missing-tracks-modal {
+ .missing-tracks-modal {
width: 95%;
max-height: 90vh;
margin: 1rem;
@@ -3729,7 +3747,7 @@ img:not([src]), img[src=''] {
font-size: 0.9rem;
}
-.mobile-only {
+ .mobile-only {
display: flex !important;
}