From ae7fae9b3dbd988adf456891063ed471b803a655 Mon Sep 17 00:00:00 2001 From: eduardprigoana Date: Mon, 17 Nov 2025 21:43:33 +0200 Subject: [PATCH] naming --- js/api.js | 97 +++++++++---------- js/app.js | 134 +++++++++++++------------- js/cache.js | 12 +-- js/downloads.js | 143 ++++++++++++++-------------- js/events.js | 107 ++++++++++----------- js/lastfm.js | 44 ++++----- js/lyrics.js | 71 +++++++------- js/player.js | 61 ++++++------ js/router.js | 7 +- js/settings.js | 77 +++++++-------- js/storage.js | 125 ++++++++++++------------ js/ui-interactions.js | 73 +++++++------- js/ui.js | 123 ++++++++++++------------ js/utils.js | 30 +++--- styles.css | 215 +++++++++++++++++++++--------------------- 15 files changed, 666 insertions(+), 653 deletions(-) diff --git a/js/api.js b/js/api.js index 7286bb3..2a126c4 100644 --- a/js/api.js +++ b/js/api.js @@ -1,3 +1,4 @@ +//js/api.js import { RATE_LIMIT_ERROR_MESSAGE, deriveTrackQuality, delay } from './utils.js'; import { APICache } from './cache.js'; @@ -11,7 +12,7 @@ export class LosslessAPI { ttl: 1000 * 60 * 30 }); this.streamCache = new Map(); - + setInterval(() => { this.cache.clearExpired(); this.pruneStreamCache(); @@ -36,8 +37,8 @@ export class LosslessAPI { let lastError = null; for (const baseUrl of instances) { - const url = baseUrl.endsWith('/') - ? `${baseUrl}${relativePath.substring(1)}` + const url = baseUrl.endsWith('/') + ? `${baseUrl}${relativePath.substring(1)}` : `${baseUrl}${relativePath}`; for (let attempt = 1; attempt <= maxRetries; attempt++) { @@ -79,9 +80,9 @@ export class LosslessAPI { if (error.name === 'AbortError') { throw error; } - + lastError = error; - + if (attempt < maxRetries) { await delay(200 * attempt); } @@ -94,7 +95,7 @@ export class LosslessAPI { findSearchSection(source, key, visited) { if (!source || typeof source !== 'object') return; - + if (Array.isArray(source)) { for (const e of source) { const f = this.findSearchSection(e, key, visited); @@ -102,17 +103,17 @@ export class LosslessAPI { } return; } - + if (visited.has(source)) return; visited.add(source); - + if ('items' in source && Array.isArray(source.items)) return source; - + if (key in source) { const f = this.findSearchSection(source[key], key, visited); if (f) return f; } - + for (const v of Object.values(source)) { const f = this.findSearchSection(v, key, visited); if (f) return f; @@ -136,7 +137,7 @@ export class LosslessAPI { prepareTrack(track) { let normalized = track; - + if (!track.artist && Array.isArray(track.artists) && track.artists.length > 0) { normalized = { ...track, artist: track.artists[0] }; } @@ -169,17 +170,17 @@ export class LosslessAPI { for (const entry of entries) { if (!entry || typeof entry !== 'object') continue; - + if (!track && 'duration' in entry) { track = entry; continue; } - + if (!info && 'manifest' in entry) { info = entry; continue; } - + if (!originalTrackUrl && 'OriginalTrackUrl' in entry) { const candidate = entry.OriginalTrackUrl; if (typeof candidate === 'string') { @@ -198,7 +199,7 @@ export class LosslessAPI { extractStreamUrlFromManifest(manifest) { try { const decoded = atob(manifest); - + try { const parsed = JSON.parse(decoded); if (parsed?.urls?.[0]) { @@ -286,14 +287,14 @@ export class LosslessAPI { const entries = Array.isArray(data) ? data : [data]; let album, tracksSection; - + for (const entry of entries) { if (!entry || typeof entry !== 'object') continue; - + if (!album && 'numberOfTracks' in entry) { album = this.prepareAlbum(entry); } - + if (!tracksSection && 'items' in entry) { tracksSection = entry; } @@ -317,14 +318,14 @@ export class LosslessAPI { const entries = Array.isArray(data) ? data : [data]; let playlist, tracksSection; - + for (const entry of entries) { if (!entry || typeof entry !== 'object') continue; - + if (!playlist && ('uuid' in entry || 'numberOfTracks' in entry)) { playlist = entry; } - + if (!tracksSection && 'items' in entry) { tracksSection = entry; } @@ -347,53 +348,53 @@ export class LosslessAPI { this.fetchWithRetry(`/artist/?id=${artistId}`), this.fetchWithRetry(`/artist/?f=${artistId}`) ]); - + const primaryData = await primaryResponse.json(); const rawArtist = Array.isArray(primaryData) ? primaryData[0] : primaryData; - + if (!rawArtist) throw new Error('Primary artist details not found.'); - + const artist = { ...this.prepareArtist(rawArtist), picture: rawArtist.picture || null, name: rawArtist.name || 'Unknown Artist' }; - + const contentData = await contentResponse.json(); const entries = Array.isArray(contentData) ? contentData : [contentData]; - + const albumMap = new Map(); const trackMap = new Map(); - + const isTrack = v => v?.id && v.duration && v.album; const isAlbum = v => v?.id && 'numberOfTracks' in v; - + const scan = (value, visited = new Set()) => { if (!value || typeof value !== 'object' || visited.has(value)) return; visited.add(value); - + if (Array.isArray(value)) { value.forEach(item => scan(item, visited)); return; } - + const item = value.item || value; if (isAlbum(item)) albumMap.set(item.id, this.prepareAlbum(item)); if (isTrack(item)) trackMap.set(item.id, this.prepareTrack(item)); - + Object.values(value).forEach(nested => scan(nested, visited)); }; - + entries.forEach(entry => scan(entry)); - - const albums = Array.from(albumMap.values()).sort((a, b) => + + const albums = Array.from(albumMap.values()).sort((a, b) => new Date(b.releaseDate || 0) - new Date(a.releaseDate || 0) ); - + const tracks = Array.from(trackMap.values()) .sort((a, b) => (b.popularity || 0) - (a.popularity || 0)) .slice(0, 10); - + const result = { ...artist, albums, tracks }; await this.cache.set('artist', artistId, result); @@ -414,13 +415,13 @@ export class LosslessAPI { async getStreamUrl(id, quality = 'LOSSLESS') { const cacheKey = `stream_${id}_${quality}`; - + if (this.streamCache.has(cacheKey)) { return this.streamCache.get(cacheKey); } const lookup = await this.getTrack(id, quality); - + let streamUrl; if (lookup.originalTrackUrl) { streamUrl = lookup.originalTrackUrl; @@ -437,7 +438,7 @@ export class LosslessAPI { async downloadTrack(id, quality = 'LOSSLESS', filename, options = {}) { const { onProgress } = options; - + try { const lookup = await this.getTrack(id, quality); let streamUrl; @@ -451,18 +452,18 @@ export class LosslessAPI { } } - const response = await fetch(streamUrl, { + const response = await fetch(streamUrl, { cache: 'no-store', - signal: options.signal + signal: options.signal }); - + if (!response.ok) { throw new Error(`Fetch failed: ${response.status}`); } const contentLength = response.headers.get('Content-Length'); const totalBytes = contentLength ? parseInt(contentLength, 10) : 0; - + let receivedBytes = 0; if (response.body && onProgress) { @@ -472,11 +473,11 @@ export class LosslessAPI { while (true) { const { done, value } = await reader.read(); if (done) break; - + if (value) { chunks.push(value); receivedBytes += value.byteLength; - + onProgress({ stage: 'downloading', receivedBytes, @@ -525,7 +526,7 @@ export class LosslessAPI { if (!id) { return `https://picsum.photos/seed/${Math.random()}/${size}`; } - + const formattedId = id.replace(/-/g, '/'); return `https://resources.tidal.com/images/${formattedId}/${size}x${size}.jpg`; } @@ -534,7 +535,7 @@ export class LosslessAPI { if (!id) { return `https://picsum.photos/seed/${Math.random()}/${size}`; } - + const formattedId = id.replace(/-/g, '/'); return `https://resources.tidal.com/images/${formattedId}/${size}x${size}.jpg`; } @@ -550,4 +551,4 @@ export class LosslessAPI { streamUrls: this.streamCache.size }; } -} \ No newline at end of file +} diff --git a/js/app.js b/js/app.js index bfd5076..bc97c54 100644 --- a/js/app.js +++ b/js/app.js @@ -1,3 +1,5 @@ + +//js/app.js import { LosslessAPI } from './api.js'; import { apiSettings, themeManager, nowPlayingSettings } from './storage.js'; import { UIRenderer } from './ui.js'; @@ -13,7 +15,7 @@ import { debounce, SVG_PLAY } from './utils.js'; function initializeCasting(audioPlayer, castBtn) { if (!castBtn) return; - + if ('remote' in audioPlayer) { audioPlayer.remote.watchAvailability((available) => { if (available) { @@ -26,39 +28,39 @@ function initializeCasting(audioPlayer, castBtn) { castBtn.style.display = 'flex'; } }); - + castBtn.addEventListener('click', () => { audioPlayer.remote.prompt().catch(err => { console.log('Cast prompt error:', err); }); }); - + audioPlayer.addEventListener('playing', () => { if (audioPlayer.remote && audioPlayer.remote.state === 'connected') { castBtn.classList.add('connected'); } }); - + audioPlayer.addEventListener('pause', () => { if (audioPlayer.remote && audioPlayer.remote.state === 'disconnected') { castBtn.classList.remove('connected'); } }); - } + } else if (audioPlayer.webkitShowPlaybackTargetPicker) { castBtn.style.display = 'flex'; castBtn.classList.add('available'); - + castBtn.addEventListener('click', () => { audioPlayer.webkitShowPlaybackTargetPicker(); }); - + audioPlayer.addEventListener('webkitplaybacktargetavailabilitychanged', (e) => { if (e.availability === 'available') { castBtn.classList.add('available'); } }); - + audioPlayer.addEventListener('webkitcurrentplaybacktargetiswirelesschanged', () => { if (audioPlayer.webkitCurrentPlaybackTargetIsWireless) { castBtn.classList.add('connected'); @@ -78,7 +80,7 @@ function initializeCasting(audioPlayer, castBtn) { function initializeKeyboardShortcuts(player, audioPlayer, lyricsPanel) { document.addEventListener('keydown', (e) => { if (e.target.matches('input, textarea')) return; - + switch(e.key.toLowerCase()) { case ' ': e.preventDefault(); @@ -89,7 +91,7 @@ function initializeKeyboardShortcuts(player, audioPlayer, lyricsPanel) { player.playNext(); } else { audioPlayer.currentTime = Math.min( - audioPlayer.duration, + audioPlayer.duration, audioPlayer.currentTime + 10 ); } @@ -146,7 +148,7 @@ function initializeKeyboardShortcuts(player, audioPlayer, lyricsPanel) { function initializeMediaSessionHandlers(player) { if (!('mediaSession' in navigator)) return; - + try { navigator.mediaSession.setActionHandler('seekto', (details) => { if (details.seekTime !== undefined && details.fastSeek !== undefined && details.fastSeek) { @@ -171,7 +173,7 @@ function showOfflineNotification() { You are offline. Some features may not work. `; document.body.appendChild(notification); - + setTimeout(() => { notification.style.animation = 'slideOut 0.3s ease forwards'; setTimeout(() => notification.remove(), 300); @@ -189,40 +191,40 @@ function hideOfflineNotification() { document.addEventListener('DOMContentLoaded', async () => { const api = new LosslessAPI(apiSettings); const ui = new UIRenderer(api); - + const audioPlayer = document.getElementById('audio-player'); const currentQuality = localStorage.getItem('playback-quality') || 'LOSSLESS'; const player = new Player(audioPlayer, api, currentQuality); - + const scrobbler = new LastFMScrobbler(); const lyricsManager = new LyricsManager(api); const lyricsPanel = createLyricsPanel(); - + const currentTheme = themeManager.getTheme(); themeManager.setTheme(currentTheme); - + initializeSettings(scrobbler, player, api, ui); initializePlayerEvents(player, audioPlayer, scrobbler); initializeTrackInteractions(player, api, document.querySelector('.main-content'), document.getElementById('context-menu')); initializeUIInteractions(player, api); initializeKeyboardShortcuts(player, audioPlayer, lyricsPanel); initializeMediaSessionHandlers(player); - + const castBtn = document.getElementById('cast-btn'); initializeCasting(audioPlayer, castBtn); - + document.querySelector('.now-playing-bar .cover').addEventListener('click', async () => { if (!player.currentTrack) { alert('No track is currently playing'); return; } - + const mode = nowPlayingSettings.getMode(); - + if (mode === 'karaoke') { lyricsPanel.classList.add('hidden'); clearLyricsPanelSync(audioPlayer, lyricsPanel); - + const lyricsData = await lyricsManager.fetchLyrics(player.currentTrack.id); if (lyricsData) { showKaraokeView(player.currentTrack, lyricsData, audioPlayer); @@ -232,13 +234,13 @@ document.addEventListener('DOMContentLoaded', async () => { } else if (mode === 'lyrics') { const isHidden = lyricsPanel.classList.contains('hidden'); lyricsPanel.classList.toggle('hidden'); - + if (isHidden) { const content = lyricsPanel.querySelector('.lyrics-content'); content.innerHTML = '
Loading lyrics...
'; - + const lyricsData = await lyricsManager.fetchLyrics(player.currentTrack.id); - + if (lyricsData) { lyricsManager.currentLyrics = lyricsData; showSyncedLyricsPanel(lyricsData, audioPlayer, lyricsPanel); @@ -251,42 +253,42 @@ document.addEventListener('DOMContentLoaded', async () => { } } }); - + document.getElementById('close-lyrics-btn')?.addEventListener('click', (e) => { e.stopPropagation(); lyricsPanel.classList.add('hidden'); clearLyricsPanelSync(audioPlayer, lyricsPanel); }); - + document.getElementById('download-lrc-btn')?.addEventListener('click', (e) => { e.stopPropagation(); if (lyricsManager.currentLyrics && player.currentTrack) { lyricsManager.downloadLRC(lyricsManager.currentLyrics, player.currentTrack); } }); - + document.getElementById('download-current-btn')?.addEventListener('click', () => { downloadCurrentTrack(player.currentTrack, player.quality, api, lyricsManager); }); - + // Auto-update lyrics when track changes let previousTrackId = null; audioPlayer.addEventListener('play', async () => { if (!player.currentTrack) return; - + const currentTrackId = player.currentTrack.id; if (currentTrackId === previousTrackId) return; previousTrackId = currentTrackId; - + // Update lyrics panel if it's open if (!lyricsPanel.classList.contains('hidden')) { const mode = nowPlayingSettings.getMode(); if (mode === 'lyrics') { const content = lyricsPanel.querySelector('.lyrics-content'); content.innerHTML = '
Loading lyrics...
'; - + const lyricsData = await lyricsManager.fetchLyrics(player.currentTrack.id); - + if (lyricsData) { lyricsManager.currentLyrics = lyricsData; // Clear old sync before showing new @@ -298,15 +300,15 @@ document.addEventListener('DOMContentLoaded', async () => { } } }); - + document.addEventListener('click', async (e) => { if (e.target.closest('#play-album-btn')) { const btn = e.target.closest('#play-album-btn'); if (btn.disabled) return; - + const albumId = window.location.hash.split('/')[1]; if (!albumId) return; - + try { const { tracks } = await api.getAlbum(albumId); if (tracks.length > 0) { @@ -322,14 +324,14 @@ document.addEventListener('DOMContentLoaded', async () => { if (e.target.closest('#download-playlist-btn')) { const btn = e.target.closest('#download-playlist-btn'); if (btn.disabled) return; - + const playlistId = window.location.hash.split('/')[1]; if (!playlistId) return; - + btn.disabled = true; const originalHTML = btn.innerHTML; btn.innerHTML = 'Downloading...'; - + try { const { playlist, tracks } = await api.getPlaylist(playlistId); await downloadPlaylistAsZip(playlist, tracks, api, player.quality, lyricsManager); @@ -344,10 +346,10 @@ document.addEventListener('DOMContentLoaded', async () => { if (e.target.closest('#play-playlist-btn')) { const btn = e.target.closest('#play-playlist-btn'); if (btn.disabled) return; - + const playlistId = window.location.hash.split('/')[1]; if (!playlistId) return; - + try { const { tracks } = await api.getPlaylist(playlistId); if (tracks.length > 0) { @@ -360,18 +362,18 @@ document.addEventListener('DOMContentLoaded', async () => { alert('Failed to play playlist: ' + error.message); } } - + if (e.target.closest('#download-album-btn')) { const btn = e.target.closest('#download-album-btn'); if (btn.disabled) return; - + const albumId = window.location.hash.split('/')[1]; if (!albumId) return; - + btn.disabled = true; const originalHTML = btn.innerHTML; btn.innerHTML = 'Downloading...'; - + try { const { album, tracks } = await api.getAlbum(albumId); await downloadAlbumAsZip(album, tracks, api, player.quality, lyricsManager); @@ -383,18 +385,18 @@ document.addEventListener('DOMContentLoaded', async () => { btn.innerHTML = originalHTML; } } - + if (e.target.closest('#download-discography-btn')) { const btn = e.target.closest('#download-discography-btn'); if (btn.disabled) return; - + const artistId = window.location.hash.split('/')[1]; if (!artistId) return; - + btn.disabled = true; const originalHTML = btn.innerHTML; btn.innerHTML = 'Downloading...'; - + try { const artist = await api.getArtist(artistId); await downloadDiscography(artist, api, player.quality, lyricsManager); @@ -407,23 +409,23 @@ document.addEventListener('DOMContentLoaded', async () => { } } }); - + const searchForm = document.getElementById('search-form'); const searchInput = document.getElementById('search-input'); - + const performSearch = debounce((query) => { if (query) { window.location.hash = `#search/${encodeURIComponent(query)}`; } }, 300); - + searchInput.addEventListener('input', (e) => { const query = e.target.value.trim(); if (query.length > 2) { performSearch(query); } }); - + searchForm.addEventListener('submit', e => { e.preventDefault(); const query = searchInput.value.trim(); @@ -431,33 +433,33 @@ document.addEventListener('DOMContentLoaded', async () => { window.location.hash = `#search/${encodeURIComponent(query)}`; } }); - + window.addEventListener('online', () => { hideOfflineNotification(); console.log('Back online'); }); - + window.addEventListener('offline', () => { showOfflineNotification(); console.log('Gone offline'); }); - + document.querySelector('.play-pause-btn').innerHTML = SVG_PLAY; - + const router = createRouter(ui); router(); window.addEventListener('hashchange', router); - + audioPlayer.addEventListener('play', () => { updateTabTitle(player); }); - + if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('./sw.js') .then(reg => { console.log('Service worker registered'); - + reg.addEventListener('updatefound', () => { const newWorker = reg.installing; newWorker.addEventListener('statechange', () => { @@ -470,14 +472,14 @@ document.addEventListener('DOMContentLoaded', async () => { .catch(err => console.log('Service worker not registered', err)); }); } - + let deferredPrompt; window.addEventListener('beforeinstallprompt', (e) => { e.preventDefault(); deferredPrompt = e; showInstallPrompt(deferredPrompt); }); - + if (!localStorage.getItem('shortcuts-shown')) { setTimeout(() => { showKeyboardShortcuts(); @@ -501,7 +503,7 @@ function showUpdateNotification() { function showInstallPrompt(deferredPrompt) { if (!deferredPrompt) return; - + const notification = document.createElement('div'); notification.className = 'install-prompt'; notification.innerHTML = ` @@ -515,7 +517,7 @@ function showInstallPrompt(deferredPrompt) { `; document.body.appendChild(notification); - + document.getElementById('install-btn').addEventListener('click', async () => { notification.remove(); deferredPrompt.prompt(); @@ -523,7 +525,7 @@ function showInstallPrompt(deferredPrompt) { console.log(`User response to install prompt: ${outcome}`); deferredPrompt = null; }); - + document.getElementById('dismiss-install').addEventListener('click', () => { notification.remove(); }); @@ -599,10 +601,10 @@ function showKeyboardShortcuts() { `; document.body.appendChild(modal); - + modal.addEventListener('click', (e) => { if (e.target === modal || e.target.classList.contains('close-shortcuts')) { modal.remove(); } }); -} \ No newline at end of file +} diff --git a/js/cache.js b/js/cache.js index 61f3341..85f535d 100644 --- a/js/cache.js +++ b/js/cache.js @@ -1,4 +1,4 @@ -//cache.js +//js/cache.js export class APICache { constructor(options = {}) { this.memoryCache = new Map(); @@ -24,7 +24,7 @@ export class APICache { request.onupgradeneeded = (event) => { const db = event.target.result; - + if (!db.objectStoreNames.contains('responses')) { const store = db.createObjectStore('responses', { keyPath: 'key' }); store.createIndex('timestamp', 'timestamp', { unique: false }); @@ -34,15 +34,15 @@ export class APICache { } generateKey(type, params) { - const paramString = typeof params === 'object' - ? JSON.stringify(params) + const paramString = typeof params === 'object' + ? JSON.stringify(params) : String(params); return `${type}:${paramString}`; } async get(type, params) { const key = this.generateKey(type, params); - + if (this.memoryCache.has(key)) { const cached = this.memoryCache.get(key); if (Date.now() - cached.timestamp < this.ttl) { @@ -177,4 +177,4 @@ export class APICache { ttl: this.ttl }; } -} \ No newline at end of file +} diff --git a/js/downloads.js b/js/downloads.js index 9904e7b..f42ae68 100644 --- a/js/downloads.js +++ b/js/downloads.js @@ -1,3 +1,4 @@ +//js/downloads.js import { buildTrackFilename, sanitizeForFilename, RATE_LIMIT_ERROR_MESSAGE, getTrackArtists, getTrackTitle, formatTemplate } from './utils.js'; import { lyricsSettings } from './storage.js'; @@ -25,14 +26,14 @@ function createDownloadNotification() { export function addDownloadTask(trackId, track, filename, api) { const container = createDownloadNotification(); - + const taskEl = document.createElement('div'); taskEl.className = 'download-task'; taskEl.dataset.trackId = trackId; const trackTitle = getTrackTitle(track); taskEl.innerHTML = `
-
${trackTitle}
@@ -50,40 +51,40 @@ export function addDownloadTask(trackId, track, filename, api) {
`; - + container.appendChild(taskEl); - + const abortController = new AbortController(); downloadTasks.set(trackId, { taskEl, abortController }); - + taskEl.querySelector('.download-cancel').addEventListener('click', () => { abortController.abort(); removeDownloadTask(trackId); }); - + return { taskEl, abortController }; } export function updateDownloadProgress(trackId, progress) { const task = downloadTasks.get(trackId); if (!task) return; - + const { taskEl } = task; const progressFill = taskEl.querySelector('.download-progress-fill'); const statusEl = taskEl.querySelector('.download-status'); - + if (progress.stage === 'downloading') { - const percent = progress.totalBytes + const percent = progress.totalBytes ? Math.round((progress.receivedBytes / progress.totalBytes) * 100) : 0; - + progressFill.style.width = `${percent}%`; - + const receivedMB = (progress.receivedBytes / (1024 * 1024)).toFixed(1); - const totalMB = progress.totalBytes + const totalMB = progress.totalBytes ? (progress.totalBytes / (1024 * 1024)).toFixed(1) : '?'; - + statusEl.textContent = `Downloading: ${receivedMB}MB / ${totalMB}MB (${percent}%)`; } } @@ -91,19 +92,19 @@ export function updateDownloadProgress(trackId, progress) { export function completeDownloadTask(trackId, success = true, message = null) { const task = downloadTasks.get(trackId); if (!task) return; - + const { taskEl } = task; const progressFill = taskEl.querySelector('.download-progress-fill'); const statusEl = taskEl.querySelector('.download-status'); const cancelBtn = taskEl.querySelector('.download-cancel'); - + if (success) { progressFill.style.width = '100%'; progressFill.style.background = '#10b981'; statusEl.textContent = '✓ Downloaded'; statusEl.style.color = '#10b981'; cancelBtn.remove(); - + setTimeout(() => removeDownloadTask(trackId), 3000); } else { progressFill.style.background = '#ef4444'; @@ -116,7 +117,7 @@ export function completeDownloadTask(trackId, success = true, message = null) { `; cancelBtn.onclick = () => removeDownloadTask(trackId); - + setTimeout(() => removeDownloadTask(trackId), 5000); } } @@ -124,14 +125,14 @@ export function completeDownloadTask(trackId, success = true, message = null) { function removeDownloadTask(trackId) { const task = downloadTasks.get(trackId); if (!task) return; - + const { taskEl } = task; taskEl.style.animation = 'slideOut 0.3s ease'; - + setTimeout(() => { taskEl.remove(); downloadTasks.delete(trackId); - + if (downloadNotificationContainer && downloadNotificationContainer.children.length === 0) { downloadNotificationContainer.remove(); downloadNotificationContainer = null; @@ -156,7 +157,7 @@ async function downloadTrackBlob(track, quality, api, lyricsManager = null) { if (!response.ok) { throw new Error(`Failed to fetch track: ${response.status}`); } - + const blob = await response.blob(); return blob; } @@ -164,27 +165,27 @@ async function downloadTrackBlob(track, quality, api, lyricsManager = null) { export async function downloadAlbumAsZip(album, tracks, api, quality, lyricsManager = null) { const JSZip = await loadJSZip(); const zip = new JSZip(); - + const template = localStorage.getItem('zip-folder-template') || '{albumTitle} - {albumArtist} - monochrome.tf'; const folderName = formatTemplate(template, { albumTitle: album.title, albumArtist: album.artist?.name, year: new Date(album.releaseDate).getFullYear() }); - + const notification = createBulkDownloadNotification('album', album.title, tracks.length); - + try { for (let i = 0; i < tracks.length; i++) { const track = tracks[i]; const filename = buildTrackFilename(track, quality); const trackTitle = getTrackTitle(track); - + updateBulkDownloadProgress(notification, i, tracks.length, trackTitle); - + const blob = await downloadTrackBlob(track, quality, api); zip.file(`${folderName}/${filename}`, blob); - + if (lyricsManager && lyricsSettings.shouldDownloadLyrics()) { try { const lyricsData = await lyricsManager.fetchLyrics(track.id); @@ -200,15 +201,15 @@ export async function downloadAlbumAsZip(album, tracks, api, quality, lyricsMana } } } - + updateBulkDownloadProgress(notification, tracks.length, tracks.length, 'Creating ZIP...'); - - const zipBlob = await zip.generateAsync({ + + const zipBlob = await zip.generateAsync({ type: 'blob', compression: 'DEFLATE', compressionOptions: { level: 6 } }); - + const url = URL.createObjectURL(zipBlob); const a = document.createElement('a'); a.href = url; @@ -217,7 +218,7 @@ export async function downloadAlbumAsZip(album, tracks, api, quality, lyricsMana a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); - + completeBulkDownload(notification, true); } catch (error) { completeBulkDownload(notification, false, error.message); @@ -228,27 +229,27 @@ export async function downloadAlbumAsZip(album, tracks, api, quality, lyricsMana export async function downloadPlaylistAsZip(playlist, tracks, api, quality, lyricsManager = null) { const JSZip = await loadJSZip(); const zip = new JSZip(); - + const template = localStorage.getItem('zip-folder-template') || '{albumTitle} - {albumArtist} - monochrome.tf'; const folderName = formatTemplate(template, { albumTitle: playlist.title, albumArtist: 'Playlist', year: new Date().getFullYear() }); - + const notification = createBulkDownloadNotification('playlist', playlist.title, tracks.length); - + try { for (let i = 0; i < tracks.length; i++) { const track = tracks[i]; const filename = buildTrackFilename(track, quality); const trackTitle = getTrackTitle(track); - + updateBulkDownloadProgress(notification, i, tracks.length, trackTitle); - + const blob = await downloadTrackBlob(track, quality, api); zip.file(`${folderName}/${filename}`, blob); - + if (lyricsManager && lyricsSettings.shouldDownloadLyrics()) { try { const lyricsData = await lyricsManager.fetchLyrics(track.id); @@ -264,15 +265,15 @@ export async function downloadPlaylistAsZip(playlist, tracks, api, quality, lyri } } } - + updateBulkDownloadProgress(notification, tracks.length, tracks.length, 'Creating ZIP...'); - - const zipBlob = await zip.generateAsync({ + + const zipBlob = await zip.generateAsync({ type: 'blob', compression: 'DEFLATE', compressionOptions: { level: 6 } }); - + const url = URL.createObjectURL(zipBlob); const a = document.createElement('a'); a.href = url; @@ -281,7 +282,7 @@ export async function downloadPlaylistAsZip(playlist, tracks, api, quality, lyri a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); - + completeBulkDownload(notification, true); } catch (error) { completeBulkDownload(notification, false, error.message); @@ -292,19 +293,19 @@ export async function downloadPlaylistAsZip(playlist, tracks, api, quality, lyri export async function downloadDiscography(artist, api, quality, lyricsManager = null) { const JSZip = await loadJSZip(); const zip = new JSZip(); - + const template = localStorage.getItem('zip-folder-template') || '{albumTitle} - {albumArtist} - monochrome.tf'; const rootFolder = `${sanitizeForFilename(artist.name)} discography - monochrome.tf`; - + const totalAlbums = artist.albums.length; const notification = createBulkDownloadNotification('discography', artist.name, totalAlbums); - + try { for (let albumIndex = 0; albumIndex < artist.albums.length; albumIndex++) { const album = artist.albums[albumIndex]; - + updateBulkDownloadProgress(notification, albumIndex, totalAlbums, album.title); - + try { const { album: fullAlbum, tracks } = await api.getAlbum(album.id); const albumFolder = formatTemplate(template, { @@ -312,12 +313,12 @@ export async function downloadDiscography(artist, api, quality, lyricsManager = albumArtist: fullAlbum.artist?.name, year: new Date(fullAlbum.releaseDate).getFullYear() }); - + for (const track of tracks) { const filename = buildTrackFilename(track, quality); const blob = await downloadTrackBlob(track, quality, api); zip.file(`${rootFolder}/${albumFolder}/${filename}`, blob); - + if (lyricsManager && lyricsSettings.shouldDownloadLyrics()) { try { const lyricsData = await lyricsManager.fetchLyrics(track.id); @@ -337,15 +338,15 @@ export async function downloadDiscography(artist, api, quality, lyricsManager = console.error(`Failed to download album ${album.title}:`, error); } } - + updateBulkDownloadProgress(notification, totalAlbums, totalAlbums, 'Creating ZIP...'); - - const zipBlob = await zip.generateAsync({ + + const zipBlob = await zip.generateAsync({ type: 'blob', compression: 'DEFLATE', compressionOptions: { level: 6 } }); - + const url = URL.createObjectURL(zipBlob); const a = document.createElement('a'); a.href = url; @@ -354,7 +355,7 @@ export async function downloadDiscography(artist, api, quality, lyricsManager = a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); - + completeBulkDownload(notification, true); } catch (error) { completeBulkDownload(notification, false, error.message); @@ -364,12 +365,12 @@ export async function downloadDiscography(artist, api, quality, lyricsManager = function createBulkDownloadNotification(type, name, totalItems) { const container = createDownloadNotification(); - + const notifEl = document.createElement('div'); notifEl.className = 'download-task bulk-download'; - + const typeLabel = type === 'album' ? 'Album' : type === 'playlist' ? 'Playlist' : 'Discography'; - + notifEl.innerHTML = `
@@ -384,7 +385,7 @@ function createBulkDownloadNotification(type, name, totalItems) {
`; - + container.appendChild(notifEl); return notifEl; } @@ -392,7 +393,7 @@ function createBulkDownloadNotification(type, name, totalItems) { function updateBulkDownloadProgress(notifEl, current, total, currentItem) { const progressFill = notifEl.querySelector('.download-progress-fill'); const statusEl = notifEl.querySelector('.download-status'); - + const percent = total > 0 ? Math.round((current / total) * 100) : 0; progressFill.style.width = `${percent}%`; statusEl.textContent = `${current}/${total} - ${currentItem}`; @@ -401,13 +402,13 @@ function updateBulkDownloadProgress(notifEl, current, total, currentItem) { function completeBulkDownload(notifEl, success = true, message = null) { const progressFill = notifEl.querySelector('.download-progress-fill'); const statusEl = notifEl.querySelector('.download-status'); - + if (success) { progressFill.style.width = '100%'; progressFill.style.background = '#10b981'; statusEl.textContent = '✓ Download complete'; statusEl.style.color = '#10b981'; - + setTimeout(() => { notifEl.style.animation = 'slideOut 0.3s ease'; setTimeout(() => notifEl.remove(), 300); @@ -416,7 +417,7 @@ function completeBulkDownload(notifEl, success = true, message = null) { progressFill.style.background = '#ef4444'; statusEl.textContent = message || '✗ Download failed'; statusEl.style.color = '#ef4444'; - + setTimeout(() => { notifEl.style.animation = 'slideOut 0.3s ease'; setTimeout(() => notifEl.remove(), 300); @@ -429,9 +430,9 @@ export async function downloadCurrentTrack(track, quality, api, lyricsManager = alert('No track is currently playing'); return; } - + const filename = buildTrackFilename(track, quality); - + try { const { taskEl, abortController } = addDownloadTask( track.id, @@ -439,16 +440,16 @@ export async function downloadCurrentTrack(track, quality, api, lyricsManager = filename, api ); - + await api.downloadTrack(track.id, quality, filename, { signal: abortController.signal, onProgress: (progress) => { updateDownloadProgress(track.id, progress); } }); - + completeDownloadTask(track.id, true); - + if (lyricsManager && lyricsSettings.shouldDownloadLyrics()) { try { const lyricsData = await lyricsManager.fetchLyrics(track.id); @@ -461,10 +462,10 @@ export async function downloadCurrentTrack(track, quality, api, lyricsManager = } } catch (error) { if (error.name !== 'AbortError') { - const errorMsg = error.message === RATE_LIMIT_ERROR_MESSAGE - ? error.message + const errorMsg = error.message === RATE_LIMIT_ERROR_MESSAGE + ? error.message : 'Download failed. Please try again.'; completeDownloadTask(track.id, false, errorMsg); } } -} \ No newline at end of file +} diff --git a/js/events.js b/js/events.js index c86176a..39c239b 100644 --- a/js/events.js +++ b/js/events.js @@ -1,3 +1,4 @@ +//js/events.js import { SVG_PLAY, SVG_PAUSE, SVG_VOLUME, SVG_MUTE, REPEAT_MODE, trackDataStore, RATE_LIMIT_ERROR_MESSAGE, buildTrackFilename } from './utils.js'; import { lastFMStorage } from './storage.js'; import { addDownloadTask, updateDownloadProgress, completeDownloadTask } from './downloads.js'; @@ -9,7 +10,7 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler) { const prevBtn = document.getElementById('prev-btn'); const shuffleBtn = document.getElementById('shuffle-btn'); const repeatBtn = document.getElementById('repeat-btn'); - + audioPlayer.addEventListener('play', () => { if (scrobbler.isAuthenticated() && lastFMStorage.isEnabled() && player.currentTrack) { scrobbler.updateNowPlaying(player.currentTrack); @@ -18,16 +19,16 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler) { player.updateMediaSessionPlaybackState(); updateTabTitle(player); }); - + audioPlayer.addEventListener('pause', () => { playPauseBtn.innerHTML = SVG_PLAY; player.updateMediaSessionPlaybackState(); }); - + audioPlayer.addEventListener('ended', () => { player.playNext(); }); - + audioPlayer.addEventListener('timeupdate', () => { const { currentTime, duration } = audioPlayer; if (duration) { @@ -38,63 +39,63 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler) { player.updateMediaSessionPositionState(); } }); - + audioPlayer.addEventListener('loadedmetadata', () => { const totalDurationEl = document.getElementById('total-duration'); totalDurationEl.textContent = formatTime(audioPlayer.duration); player.updateMediaSessionPositionState(); }); - + audioPlayer.addEventListener('error', (e) => { console.error('Audio playback error:', e); document.querySelector('.now-playing-bar .artist').textContent = 'Playback error. Try another track.'; playPauseBtn.innerHTML = SVG_PLAY; }); - + playPauseBtn.addEventListener('click', () => player.handlePlayPause()); nextBtn.addEventListener('click', () => player.playNext()); prevBtn.addEventListener('click', () => player.playPrev()); - + shuffleBtn.addEventListener('click', () => { player.toggleShuffle(); shuffleBtn.classList.toggle('active', player.shuffleActive); renderQueue(player); }); - + repeatBtn.addEventListener('click', () => { const mode = player.toggleRepeat(); repeatBtn.classList.toggle('active', mode !== REPEAT_MODE.OFF); repeatBtn.classList.toggle('repeat-one', mode === REPEAT_MODE.ONE); - repeatBtn.title = mode === REPEAT_MODE.OFF - ? 'Repeat' + repeatBtn.title = mode === REPEAT_MODE.OFF + ? 'Repeat' : (mode === REPEAT_MODE.ALL ? 'Repeat Queue' : 'Repeat One'); }); - + // Volume controls const volumeBar = document.getElementById('volume-bar'); const volumeFill = document.getElementById('volume-fill'); const volumeBtn = document.getElementById('volume-btn'); - + const updateVolumeUI = () => { const { volume, muted } = audioPlayer; volumeBtn.innerHTML = (muted || volume === 0) ? SVG_MUTE : SVG_VOLUME; const effectiveVolume = muted ? 0 : volume * 100; volumeFill.style.setProperty('--volume-level', `${effectiveVolume}%`); }; - + volumeBtn.addEventListener('click', () => { audioPlayer.muted = !audioPlayer.muted; }); - + audioPlayer.addEventListener('volumechange', updateVolumeUI); - + // Initialize volume from localStorage const savedVolume = parseFloat(localStorage.getItem('volume') || '0.7'); audioPlayer.volume = savedVolume; volumeFill.style.width = `${savedVolume * 100}%`; volumeBar.style.setProperty('--volume-level', `${savedVolume * 100}%`); updateVolumeUI(); - + initializeSmoothSliders(audioPlayer, player); } @@ -103,23 +104,23 @@ function initializeSmoothSliders(audioPlayer, player) { const progressFill = document.getElementById('progress-fill'); const volumeBar = document.getElementById('volume-bar'); const volumeFill = document.getElementById('volume-fill'); - + let isSeeking = false; let wasPlaying = false; let isAdjustingVolume = false; - + const seek = (bar, event, setter) => { const rect = bar.getBoundingClientRect(); const position = Math.max(0, Math.min(1, (event.clientX - rect.left) / rect.width)); setter(position); }; - + // Progress bar with smooth dragging progressBar.addEventListener('mousedown', (e) => { isSeeking = true; wasPlaying = !audioPlayer.paused; if (wasPlaying) audioPlayer.pause(); - + seek(progressBar, e, position => { if (!isNaN(audioPlayer.duration)) { audioPlayer.currentTime = position * audioPlayer.duration; @@ -127,14 +128,14 @@ function initializeSmoothSliders(audioPlayer, player) { } }); }); - + // Touch events for mobile progressBar.addEventListener('touchstart', (e) => { e.preventDefault(); isSeeking = true; wasPlaying = !audioPlayer.paused; if (wasPlaying) audioPlayer.pause(); - + const touch = e.touches[0]; const rect = progressBar.getBoundingClientRect(); const position = Math.max(0, Math.min(1, (touch.clientX - rect.left) / rect.width)); @@ -143,7 +144,7 @@ function initializeSmoothSliders(audioPlayer, player) { progressFill.style.width = `${position * 100}%`; } }); - + document.addEventListener('mousemove', (e) => { if (isSeeking) { seek(progressBar, e, position => { @@ -153,7 +154,7 @@ function initializeSmoothSliders(audioPlayer, player) { } }); } - + if (isAdjustingVolume) { seek(volumeBar, e, position => { audioPlayer.volume = position; @@ -163,7 +164,7 @@ function initializeSmoothSliders(audioPlayer, player) { }); } }); - + document.addEventListener('touchmove', (e) => { if (isSeeking) { const touch = e.touches[0]; @@ -174,7 +175,7 @@ function initializeSmoothSliders(audioPlayer, player) { progressFill.style.width = `${position * 100}%`; } } - + if (isAdjustingVolume) { const touch = e.touches[0]; const rect = volumeBar.getBoundingClientRect(); @@ -185,7 +186,7 @@ function initializeSmoothSliders(audioPlayer, player) { localStorage.setItem('volume', position); } }); - + document.addEventListener('mouseup', (e) => { if (isSeeking) { seek(progressBar, e, position => { @@ -197,12 +198,12 @@ function initializeSmoothSliders(audioPlayer, player) { }); isSeeking = false; } - + if (isAdjustingVolume) { isAdjustingVolume = false; } }); - + document.addEventListener('touchend', (e) => { if (isSeeking) { if (!isNaN(audioPlayer.duration)) { @@ -211,12 +212,12 @@ function initializeSmoothSliders(audioPlayer, player) { } isSeeking = false; } - + if (isAdjustingVolume) { isAdjustingVolume = false; } }); - + progressBar.addEventListener('click', e => { if (!isSeeking) { seek(progressBar, e, position => { @@ -227,7 +228,7 @@ function initializeSmoothSliders(audioPlayer, player) { }); } }); - + volumeBar.addEventListener('mousedown', (e) => { isAdjustingVolume = true; seek(volumeBar, e, position => { @@ -237,7 +238,7 @@ function initializeSmoothSliders(audioPlayer, player) { localStorage.setItem('volume', position); }); }); - + volumeBar.addEventListener('touchstart', (e) => { e.preventDefault(); isAdjustingVolume = true; @@ -249,7 +250,7 @@ function initializeSmoothSliders(audioPlayer, player) { volumeBar.style.setProperty('--volume-level', `${position * 100}%`); localStorage.setItem('volume', position); }); - + volumeBar.addEventListener('click', e => { if (!isAdjustingVolume) { seek(volumeBar, e, position => { @@ -264,7 +265,7 @@ function initializeSmoothSliders(audioPlayer, player) { export function initializeTrackInteractions(player, api, mainContent, contextMenu) { let contextTrack = null; - + mainContent.addEventListener('click', e => { const menuBtn = e.target.closest('.track-menu-btn'); if (menuBtn) { @@ -281,30 +282,30 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen } return; } - + const trackItem = e.target.closest('.track-item'); if (trackItem && !trackItem.dataset.queueIndex) { const parentList = trackItem.closest('.track-list'); const allTrackElements = Array.from(parentList.querySelectorAll('.track-item')); const trackList = allTrackElements.map(el => trackDataStore.get(el)).filter(Boolean); - + if (trackList.length > 0) { const clickedTrackId = trackItem.dataset.trackId; const startIndex = trackList.findIndex(t => t.id == clickedTrackId); - + player.setQueue(trackList, startIndex); document.getElementById('shuffle-btn').classList.remove('active'); player.playTrackFromQueue(); } } }); - + mainContent.addEventListener('contextmenu', e => { const trackItem = e.target.closest('.track-item'); if (trackItem && !trackItem.dataset.queueIndex) { e.preventDefault(); contextTrack = trackDataStore.get(trackItem); - + if (contextTrack) { contextMenu.style.top = `${e.pageY}px`; contextMenu.style.left = `${e.pageX}px`; @@ -312,22 +313,22 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen } } }); - + document.addEventListener('click', () => { contextMenu.style.display = 'none'; }); - + contextMenu.addEventListener('click', async e => { e.stopPropagation(); const action = e.target.dataset.action; - + if (action === 'add-to-queue' && contextTrack) { player.addToQueue(contextTrack); renderQueue(player); } else if (action === 'download' && contextTrack) { const quality = player.quality; const filename = buildTrackFilename(contextTrack, quality); - + try { const { taskEl, abortController } = addDownloadTask( contextTrack.id, @@ -335,28 +336,28 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen filename, api ); - + await api.downloadTrack(contextTrack.id, quality, filename, { signal: abortController.signal, onProgress: (progress) => { updateDownloadProgress(contextTrack.id, progress); } }); - + completeDownloadTask(contextTrack.id, true); } catch (error) { if (error.name !== 'AbortError') { - const errorMsg = error.message === RATE_LIMIT_ERROR_MESSAGE - ? error.message + const errorMsg = error.message === RATE_LIMIT_ERROR_MESSAGE + ? error.message : 'Download failed. Please try again.'; completeDownloadTask(contextTrack.id, false, errorMsg); } } } - + contextMenu.style.display = 'none'; }); - + // Now playing bar interactions document.querySelector('.now-playing-bar .title').addEventListener('click', () => { const track = player.currentTrack; @@ -364,7 +365,7 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen window.location.hash = `#album/${track.album.id}`; } }); - + document.querySelector('.now-playing-bar .artist').addEventListener('click', () => { const track = player.currentTrack; if (track?.artist?.id) { @@ -385,4 +386,4 @@ function formatTime(seconds) { const m = Math.floor(seconds / 60); const s = Math.floor(seconds % 60); return `${m}:${String(s).padStart(2, '0')}`; -} \ No newline at end of file +} diff --git a/js/lastfm.js b/js/lastfm.js index eeb1241..1583f98 100644 --- a/js/lastfm.js +++ b/js/lastfm.js @@ -1,4 +1,4 @@ -//lastfm.js +//js/lastfm.js import { delay } from './utils.js'; export class LastFMScrobbler { @@ -6,14 +6,14 @@ export class LastFMScrobbler { this.API_KEY = '0fc32c426d943d34a662977b31b98b67'; this.API_SECRET = '53acf2466be726db021e7fdfd0ad1084'; this.API_URL = 'https://ws.audioscrobbler.com/2.0/'; - + this.sessionKey = null; this.username = null; this.currentTrack = null; this.scrobbleTimer = null; this.scrobbleThreshold = 0; this.hasScrobbled = false; - + this.loadSession(); } @@ -53,15 +53,15 @@ export class LastFMScrobbler { const filteredParams = { ...params }; delete filteredParams.format; delete filteredParams.callback; - + const sortedKeys = Object.keys(filteredParams).sort(); - + const signatureString = sortedKeys .map(key => `${key}${filteredParams[key]}`) .join('') + this.API_SECRET; - + console.log('Signature string:', signatureString); - + try { const { default: md5 } = await import('https://cdn.jsdelivr.net/npm/md5@2.3.0/+esm'); return md5(signatureString); @@ -83,7 +83,7 @@ export class LastFMScrobbler { } const signature = await this.generateSignature(requestParams); - + const formData = new URLSearchParams({ ...requestParams, api_sig: signature, @@ -116,7 +116,7 @@ export class LastFMScrobbler { try { const data = await this.makeRequest('auth.getToken'); const token = data.token; - + return { token, url: `https://www.last.fm/api/auth/?api_key=${this.API_KEY}&token=${token}` @@ -130,7 +130,7 @@ export class LastFMScrobbler { async completeAuthentication(token) { try { const data = await this.makeRequest('auth.getSession', { token }); - + if (data.session) { this.saveSession(data.session.key, data.session.name); return { @@ -138,7 +138,7 @@ export class LastFMScrobbler { username: data.session.name }; } - + throw new Error('No session returned'); } catch (error) { console.error('Authentication failed:', error); @@ -158,19 +158,19 @@ export class LastFMScrobbler { artist: track.artist?.name || 'Unknown Artist', track: track.title }; - + if (track.album?.title) { params.album = track.album.title; } - + if (track.duration) { params.duration = Math.floor(track.duration); } - + if (track.trackNumber) { params.trackNumber = track.trackNumber; } - + await this.makeRequest('track.updateNowPlaying', params, true); console.log('Now playing updated:', track.title); @@ -185,7 +185,7 @@ export class LastFMScrobbler { scheduleScrobble(delay) { this.clearScrobbleTimer(); - + this.scrobbleTimer = setTimeout(() => { this.scrobbleCurrentTrack(); }, delay); @@ -203,25 +203,25 @@ export class LastFMScrobbler { try { const timestamp = Math.floor(Date.now() / 1000); - + const params = { artist: this.currentTrack.artist?.name || 'Unknown Artist', track: this.currentTrack.title, timestamp: timestamp }; - + if (this.currentTrack.album?.title) { params.album = this.currentTrack.album.title; } - + if (this.currentTrack.duration) { params.duration = Math.floor(this.currentTrack.duration); } - + if (this.currentTrack.trackNumber) { params.trackNumber = this.currentTrack.trackNumber; } - + await this.makeRequest('track.scrobble', params, true); this.hasScrobbled = true; @@ -246,4 +246,4 @@ export class LastFMScrobbler { this.clearScrobbleTimer(); this.currentTrack = null; } -} \ No newline at end of file +} diff --git a/js/lyrics.js b/js/lyrics.js index 31d0308..b810f27 100644 --- a/js/lyrics.js +++ b/js/lyrics.js @@ -1,3 +1,4 @@ +//js/lyrics.js import { getTrackTitle, getTrackArtists } from './utils.js'; export class LyricsManager { @@ -16,13 +17,13 @@ export class LyricsManager { try { const response = await this.api.fetchWithRetry(`/lyrics/?id=${trackId}`); const data = await response.json(); - + if (Array.isArray(data) && data.length > 0) { const lyricsData = data[0]; this.lyricsCache.set(trackId, lyricsData); return lyricsData; } - + return null; } catch (error) { console.error('Failed to fetch lyrics:', error); @@ -32,7 +33,7 @@ export class LyricsManager { parseSyncedLyrics(subtitles) { if (!subtitles) return []; - + const lines = subtitles.split('\n').filter(line => line.trim()); return lines.map(line => { const match = line.match(/\[(\d+):(\d+)\.(\d+)\]\s*(.+)/); @@ -47,17 +48,17 @@ export class LyricsManager { generateLRCContent(lyricsData, track) { if (!lyricsData || !lyricsData.subtitles) return null; - + const trackTitle = getTrackTitle(track); const trackArtist = getTrackArtists(track); - + let lrc = `[ti:${trackTitle}]\n`; lrc += `[ar:${trackArtist}]\n`; lrc += `[al:${track.album?.title || 'Unknown Album'}]\n`; lrc += `[by:${lyricsData.lyricsProvider || 'Unknown'}]\n`; lrc += '\n'; lrc += lyricsData.subtitles; - + return lrc; } @@ -67,7 +68,7 @@ export class LyricsManager { alert('No synced lyrics available for this track'); return; } - + const blob = new Blob([lrcContent], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); @@ -81,7 +82,7 @@ export class LyricsManager { getCurrentLine(currentTime) { if (!this.syncedLyrics || this.syncedLyrics.length === 0) return -1; - + let currentIndex = -1; for (let i = 0; i < this.syncedLyrics.length; i++) { if (currentTime >= this.syncedLyrics[i].time) { @@ -127,11 +128,11 @@ export function createLyricsPanel() { export function showSyncedLyricsPanel(lyricsData, audioPlayer, panel) { const content = panel.querySelector('.lyrics-content'); - - const syncedLyrics = lyricsData.subtitles + + const syncedLyrics = lyricsData.subtitles ? parseSyncedLyricsSimple(lyricsData.subtitles) : null; - + if (syncedLyrics && syncedLyrics.length > 0) { // Render synced lyrics content.innerHTML = ''; @@ -143,19 +144,19 @@ export function showSyncedLyricsPanel(lyricsData, audioPlayer, panel) { lineEl.dataset.time = line.time; content.appendChild(lineEl); }); - + let currentLineIndex = -1; - + const updateLyrics = () => { const currentTime = audioPlayer.currentTime; const newIndex = getCurrentLineIndex(syncedLyrics, currentTime); - + if (newIndex !== currentLineIndex) { currentLineIndex = newIndex; - + content.querySelectorAll('.synced-line').forEach((line, index) => { line.classList.remove('active', 'upcoming', 'past'); - + if (index === currentLineIndex) { line.classList.add('active'); // Smooth scroll to active line @@ -168,17 +169,17 @@ export function showSyncedLyricsPanel(lyricsData, audioPlayer, panel) { }); } }; - + // Store the update function so we can remove it later panel.lyricsUpdateHandler = updateLyrics; audioPlayer.addEventListener('timeupdate', updateLyrics); - + // Initial update updateLyrics(); } else if (lyricsData.lyrics) { // Fallback to static lyrics const lines = lyricsData.lyrics.split('\n'); - content.innerHTML = lines.map(line => + content.innerHTML = lines.map(line => `

${line || ' '}

` ).join(''); } else { @@ -197,11 +198,11 @@ export function showKaraokeView(track, lyricsData, audioPlayer) { const view = document.createElement('div'); view.id = 'karaoke-view'; view.className = 'karaoke-view'; - - const syncedLyrics = lyricsData.subtitles + + const syncedLyrics = lyricsData.subtitles ? parseSyncedLyricsSimple(lyricsData.subtitles) : []; - + view.innerHTML = `
`; - + document.body.appendChild(view); - + const lyricsContainer = view.querySelector('#karaoke-lyrics'); syncedLyrics.forEach((line, index) => { const lineEl = document.createElement('div'); @@ -229,19 +230,19 @@ export function showKaraokeView(track, lyricsData, audioPlayer) { lineEl.dataset.time = line.time; lyricsContainer.appendChild(lineEl); }); - + let currentLineIndex = -1; - + const updateLyrics = () => { const currentTime = audioPlayer.currentTime; const newIndex = getCurrentLineIndex(syncedLyrics, currentTime); - + if (newIndex !== currentLineIndex) { currentLineIndex = newIndex; - + document.querySelectorAll('.karaoke-line').forEach((line, index) => { line.classList.remove('active', 'upcoming', 'past'); - + if (index === currentLineIndex) { line.classList.add('active'); } else if (index === currentLineIndex + 1) { @@ -250,7 +251,7 @@ export function showKaraokeView(track, lyricsData, audioPlayer) { line.classList.add('past'); } }); - + if (currentLineIndex >= 0) { const activeLine = lyricsContainer.children[currentLineIndex]; if (activeLine) { @@ -259,18 +260,18 @@ export function showKaraokeView(track, lyricsData, audioPlayer) { } } }; - + // Use timeupdate event for better sync audioPlayer.addEventListener('timeupdate', updateLyrics); - + // Initial update updateLyrics(); - + view.querySelector('#close-karaoke-btn').addEventListener('click', () => { audioPlayer.removeEventListener('timeupdate', updateLyrics); view.remove(); }); - + return view; } @@ -297,4 +298,4 @@ function getCurrentLineIndex(syncedLyrics, currentTime) { } } return currentIndex; -} \ No newline at end of file +} diff --git a/js/player.js b/js/player.js index a526dbd..35d6c40 100644 --- a/js/player.js +++ b/js/player.js @@ -1,3 +1,4 @@ +//js/player.js import { REPEAT_MODE, formatTime, getTrackArtists, getTrackTitle} from './utils.js'; export class Player { @@ -69,26 +70,26 @@ export class Player { if (this.preloadAbortController) { this.preloadAbortController.abort(); } - + this.preloadAbortController = new AbortController(); const currentQueue = this.shuffleActive ? this.shuffledQueue : this.queue; const tracksToPreload = []; - + for (let i = 1; i <= 2; i++) { const nextIndex = this.currentQueueIndex + i; if (nextIndex < currentQueue.length) { tracksToPreload.push({ track: currentQueue[nextIndex], index: nextIndex }); } } - + for (const { track } of tracksToPreload) { if (this.preloadCache.has(track.id)) continue; const trackTitle = getTrackTitle(track); try { const streamUrl = await this.api.getStreamUrl(track.id, this.quality); - + if (this.preloadAbortController.signal.aborted) break; - + this.preloadCache.set(track.id, streamUrl); } catch (error) { if (error.name !== 'AbortError') { @@ -107,36 +108,36 @@ export class Player { const track = currentQueue[this.currentQueueIndex]; this.currentTrack = track; - const trackTitle = getTrackTitle(track); + const trackTitle = getTrackTitle(track); const trackArtists = getTrackArtists(track); - - document.querySelector('.now-playing-bar .cover').src = + + document.querySelector('.now-playing-bar .cover').src = this.api.getCoverUrl(track.album?.cover, '1280'); document.querySelector('.now-playing-bar .title').textContent = trackTitle; document.querySelector('.now-playing-bar .artist').textContent = trackArtists; document.title = `${trackTitle} • ${track.artist?.name || 'Unknown'}`; - + this.updatePlayingTrackIndicator(); this.updateMediaSession(track); try { let streamUrl; - + if (this.preloadCache.has(track.id)) { streamUrl = this.preloadCache.get(track.id); } 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); } } - + this.audio.src = streamUrl; await this.audio.play(); - + this.updateMediaSessionPlaybackState(); this.preloadNextTracks(); } catch (error) { @@ -187,7 +188,7 @@ export class Player { handlePlayPause() { if (!this.audio.src) return; - + if (this.audio.paused) { this.audio.play().catch(console.error); } else { @@ -216,7 +217,7 @@ export class Player { const currentTrack = this.queue[this.currentQueueIndex]; this.shuffledQueue = [...this.queue].sort(() => Math.random() - 0.5); this.currentQueueIndex = this.shuffledQueue.findIndex(t => t.id === currentTrack?.id); - + if (this.currentQueueIndex === -1 && currentTrack) { this.shuffledQueue.unshift(currentTrack); this.currentQueueIndex = 0; @@ -226,7 +227,7 @@ export class Player { this.queue = [...this.originalQueueBeforeShuffle]; this.currentQueueIndex = this.queue.findIndex(t => t.id === currentTrack?.id); } - + this.preloadCache.clear(); this.preloadNextTracks(); } @@ -245,7 +246,7 @@ export class Player { addToQueue(track) { this.queue.push(track); - + if (!this.currentTrack || this.currentQueueIndex === -1) { this.currentQueueIndex = this.queue.length - 1; this.playTrackFromQueue(); @@ -254,15 +255,15 @@ export class Player { removeFromQueue(index) { const currentQueue = this.shuffleActive ? this.shuffledQueue : this.queue; - + if (index < 0 || index >= currentQueue.length) return; - + if (this.shuffleActive) { this.shuffledQueue.splice(index, 1); } else { this.queue.splice(index, 1); } - + if (index < this.currentQueueIndex) { this.currentQueueIndex--; } else if (index === this.currentQueueIndex) { @@ -274,13 +275,13 @@ export class Player { moveInQueue(fromIndex, toIndex) { const currentQueue = this.shuffleActive ? this.shuffledQueue : this.queue; - + if (fromIndex < 0 || fromIndex >= currentQueue.length) return; if (toIndex < 0 || toIndex >= currentQueue.length) return; - + const [track] = currentQueue.splice(fromIndex, 1); currentQueue.splice(toIndex, 0, track); - + if (this.currentQueueIndex === fromIndex) { this.currentQueueIndex = toIndex; } else if (fromIndex < this.currentQueueIndex && toIndex >= this.currentQueueIndex) { @@ -297,7 +298,7 @@ export class Player { updatePlayingTrackIndicator() { const currentTrack = this.getCurrentQueue()[this.currentQueueIndex]; document.querySelectorAll('.track-item').forEach(item => { - item.classList.toggle('playing', + item.classList.toggle('playing', currentTrack && item.dataset.trackId == currentTrack.id ); }); @@ -305,12 +306,12 @@ export class Player { updateMediaSession(track) { if (!('mediaSession' in navigator)) return; - + const artwork = []; const sizes = ['1280']; const coverId = track.album?.cover; const trackTitle = getTrackTitle(track); - + if (coverId) { sizes.forEach(size => { artwork.push({ @@ -320,7 +321,7 @@ export class Player { }); }); } - + navigator.mediaSession.metadata = new MediaMetadata({ title: trackTitle || 'Unknown Title', artist: track.artist?.name || 'Unknown Artist', @@ -340,9 +341,9 @@ export class Player { updateMediaSessionPositionState() { if (!('mediaSession' in navigator)) return; if (!('setPositionState' in navigator.mediaSession)) return; - + const duration = this.audio.duration; - + if (!duration || isNaN(duration) || !isFinite(duration)) { return; } @@ -357,4 +358,4 @@ export class Player { console.debug('Failed to update Media Session position:', error); } } -} \ No newline at end of file +} diff --git a/js/router.js b/js/router.js index e7d4d71..5e7f05b 100644 --- a/js/router.js +++ b/js/router.js @@ -1,8 +1,9 @@ +//router.js export function createRouter(ui) { const router = () => { const path = window.location.hash.substring(1) || "home"; const [page, param] = path.split('/'); - + switch (page) { case 'search': ui.renderSearchPage(decodeURIComponent(param)); @@ -24,7 +25,7 @@ export function createRouter(ui) { break; } }; - + return router; } @@ -39,4 +40,4 @@ export function updateTabTitle(player) { } document.title = 'Monochrome Music'; } -} \ No newline at end of file +} diff --git a/js/settings.js b/js/settings.js index 994906b..9204c21 100644 --- a/js/settings.js +++ b/js/settings.js @@ -1,3 +1,4 @@ +//js/settings import { themeManager, lastFMStorage, nowPlayingSettings, lyricsSettings } from './storage.js'; export function initializeSettings(scrobbler, player, api, ui) { @@ -5,7 +6,7 @@ export function initializeSettings(scrobbler, player, api, ui) { const lastfmStatus = document.getElementById('lastfm-status'); const lastfmToggle = document.getElementById('lastfm-toggle'); const lastfmToggleSetting = document.getElementById('lastfm-toggle-setting'); - + function updateLastFMUI() { if (scrobbler.isAuthenticated()) { lastfmStatus.textContent = `Connected as ${scrobbler.username}`; @@ -20,9 +21,9 @@ export function initializeSettings(scrobbler, player, api, ui) { lastfmToggleSetting.style.display = 'none'; } } - + updateLastFMUI(); - + lastfmConnectBtn?.addEventListener('click', async () => { if (scrobbler.isAuthenticated()) { if (confirm('Disconnect from Last.fm?')) { @@ -31,14 +32,14 @@ export function initializeSettings(scrobbler, player, api, ui) { } return; } - + const authWindow = window.open('', '_blank'); lastfmConnectBtn.disabled = true; lastfmConnectBtn.textContent = 'Opening Last.fm...'; - + try { const { token, url } = await scrobbler.getAuthUrl(); - + if (authWindow) { authWindow.location.href = url; } else { @@ -47,15 +48,15 @@ export function initializeSettings(scrobbler, player, api, ui) { lastfmConnectBtn.disabled = false; return; } - + lastfmConnectBtn.textContent = 'Waiting for authorization...'; - + let attempts = 0; const maxAttempts = 30; - + const checkAuth = setInterval(async () => { attempts++; - + if (attempts > maxAttempts) { clearInterval(checkAuth); lastfmConnectBtn.textContent = 'Connect Last.fm'; @@ -64,10 +65,10 @@ export function initializeSettings(scrobbler, player, api, ui) { alert('Authorization timed out. Please try again.'); return; } - + try { const result = await scrobbler.completeAuthentication(token); - + if (result.success) { clearInterval(checkAuth); if (authWindow && !authWindow.closed) authWindow.close(); @@ -81,7 +82,7 @@ export function initializeSettings(scrobbler, player, api, ui) { // Still waiting } }, 2000); - + } catch (error) { console.error('Last.fm connection failed:', error); alert('Failed to connect to Last.fm: ' + error.message); @@ -90,26 +91,26 @@ export function initializeSettings(scrobbler, player, api, ui) { if (authWindow && !authWindow.closed) authWindow.close(); } }); - + lastfmToggle?.addEventListener('change', (e) => { lastFMStorage.setEnabled(e.target.checked); }); - + // Theme picker const themePicker = document.getElementById('theme-picker'); const currentTheme = themeManager.getTheme(); - + themePicker.querySelectorAll('.theme-option').forEach(option => { if (option.dataset.theme === currentTheme) { option.classList.add('active'); } - + option.addEventListener('click', () => { const theme = option.dataset.theme; - + themePicker.querySelectorAll('.theme-option').forEach(opt => opt.classList.remove('active')); option.classList.add('active'); - + if (theme === 'custom') { document.getElementById('custom-theme-editor').classList.add('show'); renderCustomThemeEditor(); @@ -119,7 +120,7 @@ export function initializeSettings(scrobbler, player, api, ui) { } }); }); - + function renderCustomThemeEditor() { const grid = document.getElementById('theme-color-grid'); const customTheme = themeManager.getCustomTheme() || { @@ -131,7 +132,7 @@ export function initializeSettings(scrobbler, player, api, ui) { border: '#27272a', highlight: '#ffffff' }; - + grid.innerHTML = Object.entries(customTheme).map(([key, value]) => `
@@ -139,7 +140,7 @@ export function initializeSettings(scrobbler, player, api, ui) {
`).join(''); } - + document.getElementById('apply-custom-theme')?.addEventListener('click', () => { const colors = {}; document.querySelectorAll('#theme-color-grid input[type="color"]').forEach(input => { @@ -147,25 +148,25 @@ export function initializeSettings(scrobbler, player, api, ui) { }); themeManager.setCustomTheme(colors); }); - + document.getElementById('reset-custom-theme')?.addEventListener('click', () => { renderCustomThemeEditor(); }); - + // Quality setting const qualitySetting = document.getElementById('quality-setting'); if (qualitySetting) { const savedQuality = localStorage.getItem('playback-quality') || 'LOSSLESS'; qualitySetting.value = savedQuality; player.setQuality(savedQuality); - + qualitySetting.addEventListener('change', (e) => { const newQuality = e.target.value; player.setQuality(newQuality); localStorage.setItem('playback-quality', newQuality); }); } - + // Now Playing Mode const nowPlayingMode = document.getElementById('now-playing-mode'); if (nowPlayingMode) { @@ -174,7 +175,7 @@ export function initializeSettings(scrobbler, player, api, ui) { nowPlayingSettings.setMode(e.target.value); }); } - + // Download Lyrics Toggle const downloadLyricsToggle = document.getElementById('download-lyrics-toggle'); if (downloadLyricsToggle) { @@ -183,7 +184,7 @@ export function initializeSettings(scrobbler, player, api, ui) { lyricsSettings.setDownloadLyrics(e.target.checked); }); } - + // Filename template setting const filenameTemplate = document.getElementById('filename-template'); if (filenameTemplate) { @@ -192,7 +193,7 @@ export function initializeSettings(scrobbler, player, api, ui) { localStorage.setItem('filename-template', e.target.value); }); } - + // ZIP folder template const zipFolderTemplate = document.getElementById('zip-folder-template'); if (zipFolderTemplate) { @@ -201,14 +202,14 @@ export function initializeSettings(scrobbler, player, api, ui) { localStorage.setItem('zip-folder-template', e.target.value); }); } - + // API settings document.getElementById('refresh-speed-test-btn')?.addEventListener('click', async () => { const btn = document.getElementById('refresh-speed-test-btn'); const originalText = btn.textContent; btn.textContent = 'Testing...'; btn.disabled = true; - + try { await api.settings.refreshSpeedTests(); ui.renderApiSettings(); @@ -226,31 +227,31 @@ export function initializeSettings(scrobbler, player, api, ui) { }, 1500); } }); - + document.getElementById('api-instance-list')?.addEventListener('click', async (e) => { const button = e.target.closest('button'); if (!button) return; - + const li = button.closest('li'); const index = parseInt(li.dataset.index, 10); const instances = await api.settings.getInstances(); - + if (button.classList.contains('move-up') && index > 0) { [instances[index], instances[index - 1]] = [instances[index - 1], instances[index]]; } else if (button.classList.contains('move-down') && index < instances.length - 1) { [instances[index], instances[index + 1]] = [instances[index + 1], instances[index]]; } - + api.settings.saveInstances(instances); ui.renderApiSettings(); }); - + document.getElementById('clear-cache-btn')?.addEventListener('click', async () => { const btn = document.getElementById('clear-cache-btn'); const originalText = btn.textContent; btn.textContent = 'Clearing...'; btn.disabled = true; - + try { await api.clearCache(); btn.textContent = 'Cleared!'; @@ -270,4 +271,4 @@ export function initializeSettings(scrobbler, player, api, ui) { }, 1500); } }); -} \ No newline at end of file +} diff --git a/js/storage.js b/js/storage.js index 2989942..51c5794 100644 --- a/js/storage.js +++ b/js/storage.js @@ -1,3 +1,4 @@ +//storage.js export const apiSettings = { STORAGE_KEY: 'monochrome-api-instances', INSTANCES_URL: 'https://raw.githubusercontent.com/EduardPrigoana/hifi-instances/refs/heads/main/instances.json', @@ -5,28 +6,28 @@ export const apiSettings = { SPEED_TEST_CACHE_DURATION: 1000 * 60 * 60, defaultInstances: [], instancesLoaded: false, - + async loadInstancesFromGitHub() { if (this.instancesLoaded) { return this.defaultInstances; } - + try { const response = await fetch(this.INSTANCES_URL); if (!response.ok) throw new Error('Failed to fetch instances'); - + const data = await response.json(); const allInstances = []; - + for (const [provider, config] of Object.entries(data.api)) { if (config.cors === false && Array.isArray(config.urls)) { allInstances.push(...config.urls); } } - + this.defaultInstances = allInstances; this.instancesLoaded = true; - + return allInstances; } catch (error) { console.error('Failed to load instances from GitHub:', error); @@ -50,61 +51,61 @@ export const apiSettings = { return this.defaultInstances; } }, - + async speedTestInstance(url) { - const testUrl = url.endsWith('/') - ? `${url}track/?id=204567804&quality=HIGH` + const testUrl = url.endsWith('/') + ? `${url}track/?id=204567804&quality=HIGH` : `${url}/track/?id=204567804&quality=HIGH`; - + const startTime = performance.now(); - + try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); - + const response = await fetch(testUrl, { signal: controller.signal, cache: 'no-store' }); - + clearTimeout(timeout); - + if (!response.ok) { return { url, speed: Infinity, error: `HTTP ${response.status}` }; } - + const endTime = performance.now(); const speed = endTime - startTime; - + return { url, speed, error: null }; } catch (error) { return { url, speed: Infinity, error: error.message }; } }, - + async runSpeedTests(instances) { console.log('[SpeedTest] Testing', instances.length, 'instances...'); - + const results = await Promise.all( instances.map(url => this.speedTestInstance(url)) ); - + const validResults = results.filter(r => r.speed !== Infinity); const failedResults = results.filter(r => r.speed === Infinity); - + if (failedResults.length > 0) { console.log('[SpeedTest] Failed instances:', failedResults.map(r => `${r.url} (${r.error})`)); } - + validResults.sort((a, b) => a.speed - b.speed); - + console.log('[SpeedTest] Results:', validResults.map(r => `${r.url}: ${r.speed.toFixed(0)}ms`)); - + const sortedInstances = [ ...validResults.map(r => r.url), ...failedResults.map(r => r.url) ]; - + const cacheData = { timestamp: Date.now(), speeds: results.reduce((acc, r) => { @@ -112,82 +113,82 @@ export const apiSettings = { return acc; }, {}) }; - + try { localStorage.setItem(this.SPEED_TEST_CACHE_KEY, JSON.stringify(cacheData)); } catch (e) { console.warn('[SpeedTest] Failed to cache results'); } - + return sortedInstances; }, - + getCachedSpeedTests() { try { const cached = localStorage.getItem(this.SPEED_TEST_CACHE_KEY); if (!cached) return null; - + const data = JSON.parse(cached); - + if (Date.now() - data.timestamp > this.SPEED_TEST_CACHE_DURATION) { return null; } - + return data; } catch (e) { return null; } }, - + sortInstancesByCache(instances, cachedData) { const speeds = cachedData.speeds; - + const sorted = [...instances].sort((a, b) => { const speedA = speeds[a]?.speed ?? Infinity; const speedB = speeds[b]?.speed ?? Infinity; return speedA - speedB; }); - - console.log('[SpeedTest] Using cached results (age:', + + console.log('[SpeedTest] Using cached results (age:', Math.round((Date.now() - cachedData.timestamp) / 1000 / 60), 'minutes)'); - + return sorted; }, - + async getInstances() { try { const stored = localStorage.getItem(this.STORAGE_KEY); if (stored) { return JSON.parse(stored); } - + const instances = await this.loadInstancesFromGitHub(); - + const cachedSpeedTests = this.getCachedSpeedTests(); - + let sortedInstances; if (cachedSpeedTests) { sortedInstances = this.sortInstancesByCache(instances, cachedSpeedTests); } else { sortedInstances = await this.runSpeedTests(instances); } - + this.saveInstances(sortedInstances); - + return sortedInstances; } catch (e) { const instances = await this.loadInstancesFromGitHub(); return instances; } }, - + async refreshSpeedTests() { const instances = await this.loadInstancesFromGitHub(); const sortedInstances = await this.runSpeedTests(instances); this.saveInstances(sortedInstances); return sortedInstances; }, - + saveInstances(instances) { localStorage.setItem(this.STORAGE_KEY, JSON.stringify(instances)); } @@ -196,7 +197,7 @@ export const apiSettings = { export const recentActivityManager = { STORAGE_KEY: 'monochrome-recent-activity', LIMIT: 10, - + _get() { try { const data = localStorage.getItem(this.STORAGE_KEY); @@ -205,15 +206,15 @@ export const recentActivityManager = { return { artists: [], albums: [] }; } }, - + _save(data) { localStorage.setItem(this.STORAGE_KEY, JSON.stringify(data)); }, - + getRecents() { return this._get(); }, - + _add(type, item) { const data = this._get(); data[type] = data[type].filter(i => i.id !== item.id); @@ -221,11 +222,11 @@ export const recentActivityManager = { data[type] = data[type].slice(0, this.LIMIT); this._save(data); }, - + addArtist(artist) { this._add('artists', artist); }, - + addAlbum(album) { this._add('albums', album); } @@ -234,7 +235,7 @@ export const recentActivityManager = { export const themeManager = { STORAGE_KEY: 'monochrome-theme', CUSTOM_THEME_KEY: 'monochrome-custom-theme', - + defaultThemes: { monochrome: {}, dark: {}, @@ -242,7 +243,7 @@ export const themeManager = { purple: {}, forest: {} }, - + getTheme() { try { return localStorage.getItem(this.STORAGE_KEY) || 'monochrome'; @@ -250,12 +251,12 @@ export const themeManager = { return 'monochrome'; } }, - + setTheme(theme) { localStorage.setItem(this.STORAGE_KEY, theme); document.documentElement.setAttribute('data-theme', theme); }, - + getCustomTheme() { try { const stored = localStorage.getItem(this.CUSTOM_THEME_KEY); @@ -264,12 +265,12 @@ export const themeManager = { return null; } }, - + setCustomTheme(colors) { localStorage.setItem(this.CUSTOM_THEME_KEY, JSON.stringify(colors)); this.applyCustomTheme(colors); }, - + applyCustomTheme(colors) { const root = document.documentElement; for (const [key, value] of Object.entries(colors)) { @@ -280,7 +281,7 @@ export const themeManager = { export const lastFMStorage = { STORAGE_KEY: 'lastfm-enabled', - + isEnabled() { try { return localStorage.getItem(this.STORAGE_KEY) === 'true'; @@ -288,7 +289,7 @@ export const lastFMStorage = { return false; } }, - + setEnabled(enabled) { localStorage.setItem(this.STORAGE_KEY, enabled ? 'true' : 'false'); } @@ -296,7 +297,7 @@ export const lastFMStorage = { export const nowPlayingSettings = { STORAGE_KEY: 'now-playing-mode', - + getMode() { try { return localStorage.getItem(this.STORAGE_KEY) || 'cover'; @@ -304,7 +305,7 @@ export const nowPlayingSettings = { return 'cover'; } }, - + setMode(mode) { localStorage.setItem(this.STORAGE_KEY, mode); } @@ -312,7 +313,7 @@ export const nowPlayingSettings = { export const lyricsSettings = { DOWNLOAD_WITH_TRACKS: 'lyrics-download-with-tracks', - + shouldDownloadLyrics() { try { return localStorage.getItem(this.DOWNLOAD_WITH_TRACKS) === 'true'; @@ -320,8 +321,8 @@ export const lyricsSettings = { return false; } }, - + setDownloadLyrics(enabled) { localStorage.setItem(this.DOWNLOAD_WITH_TRACKS, enabled ? 'true' : 'false'); } -}; \ No newline at end of file +}; diff --git a/js/ui-interactions.js b/js/ui-interactions.js index c27a2c4..e85965e 100644 --- a/js/ui-interactions.js +++ b/js/ui-interactions.js @@ -1,3 +1,4 @@ +//js/ui-interactions.js import { formatTime, trackDataStore, getTrackTitle, getTrackArtists } from './utils.js'; export function initializeUIInteractions(player, api) { @@ -8,57 +9,57 @@ export function initializeUIInteractions(player, api) { const queueModalOverlay = document.getElementById('queue-modal-overlay'); const closeQueueBtn = document.getElementById('close-queue-btn'); const queueList = document.getElementById('queue-list'); - + let draggedQueueIndex = null; - + // Sidebar mobile hamburgerBtn.addEventListener('click', () => { sidebar.classList.add('is-open'); sidebarOverlay.classList.add('is-visible'); }); - + const closeSidebar = () => { sidebar.classList.remove('is-open'); sidebarOverlay.classList.remove('is-visible'); }; - + sidebarOverlay.addEventListener('click', closeSidebar); - + sidebar.addEventListener('click', e => { if (e.target.closest('a')) { closeSidebar(); } }); - + // Queue modal queueBtn.addEventListener('click', () => { renderQueue(); queueModalOverlay.style.display = 'flex'; }); - + closeQueueBtn.addEventListener('click', () => { queueModalOverlay.style.display = 'none'; }); - + queueModalOverlay.addEventListener('click', e => { if (e.target === queueModalOverlay) { queueModalOverlay.style.display = 'none'; } }); - + function renderQueue() { const currentQueue = player.getCurrentQueue(); - + if (currentQueue.length === 0) { queueList.innerHTML = '
Queue is empty.
'; return; } - + const html = currentQueue.map((track, index) => { const isPlaying = index === player.currentQueueIndex; const trackTitle = getTrackTitle(track); const trackArtists = getTrackArtists(track, { fallback: "Unknown" }); - + return `
@@ -68,7 +69,7 @@ export function initializeUIInteractions(player, api) {
-
${trackTitle}
@@ -86,31 +87,31 @@ export function initializeUIInteractions(player, api) {
`; }).join(''); - + queueList.innerHTML = html; - + queueList.querySelectorAll('.queue-track-item').forEach((item) => { const index = parseInt(item.dataset.queueIndex); - + item.addEventListener('click', (e) => { if (e.target.closest('.track-menu-btn')) return; player.playAtIndex(index); renderQueue(); }); - + item.addEventListener('dragstart', (e) => { draggedQueueIndex = index; item.style.opacity = '0.5'; }); - + item.addEventListener('dragend', () => { item.style.opacity = '1'; }); - + item.addEventListener('dragover', (e) => { e.preventDefault(); }); - + item.addEventListener('drop', (e) => { e.preventDefault(); if (draggedQueueIndex !== null && draggedQueueIndex !== index) { @@ -119,7 +120,7 @@ export function initializeUIInteractions(player, api) { } }); }); - + queueList.querySelectorAll('.track-menu-btn').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); @@ -128,7 +129,7 @@ export function initializeUIInteractions(player, api) { }); }); } - + function showQueueTrackMenu(e, trackIndex) { const menu = document.getElementById('queue-track-menu'); menu.style.top = `${e.pageY}px`; @@ -138,45 +139,45 @@ export function initializeUIInteractions(player, api) { positionContextMenu(menu, e.pageX, e.pageY, true); document.addEventListener('click', hideQueueTrackMenu); } - + function hideQueueTrackMenu() { const menu = document.getElementById('queue-track-menu'); menu.classList.remove('show'); document.removeEventListener('click', hideQueueTrackMenu); } - + document.getElementById('queue-track-menu').addEventListener('click', (e) => { e.stopPropagation(); const action = e.target.dataset.action; const menu = document.getElementById('queue-track-menu'); const trackIndex = parseInt(menu.dataset.trackIndex); - + if (action === 'remove') { player.removeFromQueue(trackIndex); renderQueue(); } - + hideQueueTrackMenu(); }); - + function positionContextMenu(menu, x, y, preferLeft = false) { menu.style.display = 'block'; menu.style.visibility = 'hidden'; - + const menuRect = menu.getBoundingClientRect(); const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; - + let finalX = x; let finalY = y; - + if (preferLeft || (x + menuRect.width > viewportWidth)) { finalX = x - menuRect.width; if (finalX < 0) { finalX = Math.min(x, viewportWidth - menuRect.width - 10); } } - + if (finalX < 10) finalX = 10; if (finalX + menuRect.width > viewportWidth - 10) { finalX = viewportWidth - menuRect.width - 10; @@ -188,23 +189,23 @@ export function initializeUIInteractions(player, api) { finalY = viewportHeight - menuRect.height - 10; } if (finalY < 10) finalY = 10; - + menu.style.left = `${finalX}px`; menu.style.top = `${finalY}px`; menu.style.visibility = 'visible'; } - + // Make renderQueue available globally for other modules window.renderQueueFunction = renderQueue; - + // Search tabs document.querySelectorAll('.search-tab').forEach(tab => { tab.addEventListener('click', () => { document.querySelectorAll('.search-tab').forEach(t => t.classList.remove('active')); document.querySelectorAll('.search-tab-content').forEach(c => c.classList.remove('active')); - + tab.classList.add('active'); document.getElementById(`search-tab-${tab.dataset.tab}`).classList.add('active'); }); }); -} \ No newline at end of file +} diff --git a/js/ui.js b/js/ui.js index 530be10..a364960 100644 --- a/js/ui.js +++ b/js/ui.js @@ -1,3 +1,4 @@ +//js/ui.js import { formatTime, createPlaceholder, trackDataStore, hasExplicitContent, getTrackArtists, getTrackTitle, calculateTotalDuration, formatDuration } from './utils.js'; import { recentActivityManager } from './storage.js'; @@ -29,7 +30,7 @@ export class UIRenderer { const explicitBadge = hasExplicitContent(track) ? this.createExplicitBadge() : ''; const trackArtists = getTrackArtists(track); const trackTitle = getTrackTitle(track); - + return `
${trackNumberHTML} @@ -116,17 +117,17 @@ export class UIRenderer { const fragment = document.createDocumentFragment(); const tempDiv = document.createElement('div'); - tempDiv.innerHTML = tracks.map((track, i) => + tempDiv.innerHTML = tracks.map((track, i) => this.createTrackItemHTML(track, i, showCover) ).join(''); - + while (tempDiv.firstChild) { fragment.appendChild(tempDiv.firstChild); } - + container.innerHTML = ''; container.appendChild(fragment); - + tracks.forEach(track => { const element = container.querySelector(`[data-track-id="${track.id}"]`); if (element) trackDataStore.set(element, track); @@ -137,13 +138,13 @@ export class UIRenderer { document.querySelectorAll('.page').forEach(page => { page.classList.toggle('active', page.id === `page-${pageId}`); }); - + document.querySelectorAll('.sidebar-nav a').forEach(link => { link.classList.toggle('active', link.hash === `#${pageId}`); }); - + document.querySelector('.main-content').scrollTop = 0; - + if (pageId === 'settings') { this.renderApiSettings(); } @@ -152,14 +153,14 @@ export class UIRenderer { async renderHomePage() { this.showPage('home'); const recents = recentActivityManager.getRecents(); - + const albumsContainer = document.getElementById('home-recent-albums'); const artistsContainer = document.getElementById('home-recent-artists'); - + albumsContainer.innerHTML = recents.albums.length ? recents.albums.map(album => this.createAlbumCardHTML(album)).join('') : createPlaceholder("You haven't viewed any albums yet. Search for music to get started!"); - + artistsContainer.innerHTML = recents.artists.length ? recents.artists.map(artist => this.createArtistCardHTML(artist)).join('') : createPlaceholder("You haven't viewed any artists yet. Search for music to get started!"); @@ -168,26 +169,26 @@ export class UIRenderer { async renderSearchPage(query) { this.showPage('search'); document.getElementById('search-results-title').textContent = `Search Results for "${query}"`; - + const tracksContainer = document.getElementById('search-tracks-container'); const artistsContainer = document.getElementById('search-artists-container'); const albumsContainer = document.getElementById('search-albums-container'); - + tracksContainer.innerHTML = this.createSkeletonTracks(8, true); artistsContainer.innerHTML = this.createSkeletonCards(6, true); albumsContainer.innerHTML = this.createSkeletonCards(6, false); - + try { const [tracksResult, artistsResult, albumsResult] = await Promise.all([ this.api.searchTracks(query), this.api.searchArtists(query), this.api.searchAlbums(query) ]); - + let finalTracks = tracksResult.items; let finalArtists = artistsResult.items; let finalAlbums = albumsResult.items; - + if (finalArtists.length === 0 && finalTracks.length > 0) { const artistMap = new Map(); finalTracks.forEach(track => { @@ -204,7 +205,7 @@ export class UIRenderer { }); finalArtists = Array.from(artistMap.values()); } - + if (finalAlbums.length === 0 && finalTracks.length > 0) { const albumMap = new Map(); finalTracks.forEach(track => { @@ -214,21 +215,21 @@ export class UIRenderer { }); finalAlbums = Array.from(albumMap.values()); } - + if (finalTracks.length) { this.renderListWithTracks(tracksContainer, finalTracks, true); } else { tracksContainer.innerHTML = createPlaceholder('No tracks found.'); } - + artistsContainer.innerHTML = finalArtists.length ? finalArtists.map(artist => this.createArtistCardHTML(artist)).join('') : createPlaceholder('No artists found.'); - + albumsContainer.innerHTML = finalAlbums.length ? finalAlbums.map(album => this.createAlbumCardHTML(album)).join('') : createPlaceholder('No albums found.'); - + } catch (error) { console.error("Search failed:", error); const errorMsg = createPlaceholder(`Error during search. ${error.message}`); @@ -240,12 +241,12 @@ export class UIRenderer { async renderAlbumPage(albumId) { this.showPage('album'); - + const imageEl = document.getElementById('album-detail-image'); const titleEl = document.getElementById('album-detail-title'); const metaEl = document.getElementById('album-detail-meta'); const tracklistContainer = document.getElementById('album-detail-tracklist'); - + imageEl.src = ''; imageEl.style.backgroundColor = 'var(--muted)'; titleEl.innerHTML = '
'; @@ -258,27 +259,27 @@ export class UIRenderer {
${this.createSkeletonTracks(10, false)} `; - + try { const { album, tracks } = await this.api.getAlbum(albumId); - + imageEl.src = this.api.getCoverUrl(album.cover, '1280'); imageEl.style.backgroundColor = ''; - + const explicitBadge = hasExplicitContent(album) ? this.createExplicitBadge() : ''; titleEl.innerHTML = `${album.title} ${explicitBadge}`; - + const totalDuration = calculateTotalDuration(tracks); const releaseDate = new Date(album.releaseDate); const year = releaseDate.getFullYear(); - - const dateDisplay = window.innerWidth > 768 + + const dateDisplay = window.innerWidth > 768 ? releaseDate.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) : year; - - metaEl.innerHTML = + + metaEl.innerHTML = `By ${album.artist.name} • ${dateDisplay} • ${tracks.length} tracks • ${formatDuration(totalDuration)}`; - + tracklistContainer.innerHTML = `
# @@ -286,12 +287,12 @@ export class UIRenderer { Duration
`; - + tracks.sort((a, b) => a.trackNumber - b.trackNumber); this.renderListWithTracks(tracklistContainer, tracks, false); - + recentActivityManager.addAlbum(album); - + document.title = `${album.title} - ${album.artist.name} - Monochrome`; } catch (error) { console.error("Failed to load album:", error); @@ -301,13 +302,13 @@ export class UIRenderer { async renderPlaylistPage(playlistId) { this.showPage('playlist'); - + const imageEl = document.getElementById('playlist-detail-image'); const titleEl = document.getElementById('playlist-detail-title'); const metaEl = document.getElementById('playlist-detail-meta'); const descEl = document.getElementById('playlist-detail-description'); const tracklistContainer = document.getElementById('playlist-detail-tracklist'); - + imageEl.src = ''; imageEl.style.backgroundColor = 'var(--muted)'; titleEl.innerHTML = '
'; @@ -321,22 +322,22 @@ async renderPlaylistPage(playlistId) {
${this.createSkeletonTracks(10, true)} `; - + try { const { playlist, tracks } = await this.api.getPlaylist(playlistId); - + const imageId = playlist.squareImage || playlist.image; imageEl.src = this.api.getCoverUrl(imageId, '1080'); imageEl.style.backgroundColor = ''; - + titleEl.textContent = playlist.title; - + const totalDuration = calculateTotalDuration(tracks); - + metaEl.textContent = `${playlist.numberOfTracks} tracks • ${formatDuration(totalDuration)}`; - + descEl.textContent = playlist.description || ''; - + tracklistContainer.innerHTML = `
# @@ -344,9 +345,9 @@ async renderPlaylistPage(playlistId) { Duration
`; - + this.renderListWithTracks(tracklistContainer, tracks, true); - + document.title = `${playlist.title} - Monochrome`; } catch (error) { console.error("Failed to load playlist:", error); @@ -356,39 +357,39 @@ async renderPlaylistPage(playlistId) { async renderArtistPage(artistId) { this.showPage('artist'); - + const imageEl = document.getElementById('artist-detail-image'); const nameEl = document.getElementById('artist-detail-name'); const metaEl = document.getElementById('artist-detail-meta'); const tracksContainer = document.getElementById('artist-detail-tracks'); const albumsContainer = document.getElementById('artist-detail-albums'); - + imageEl.src = ''; imageEl.style.backgroundColor = 'var(--muted)'; nameEl.innerHTML = '
'; metaEl.innerHTML = '
'; tracksContainer.innerHTML = this.createSkeletonTracks(5, true); albumsContainer.innerHTML = this.createSkeletonCards(6, false); - + try { const artist = await this.api.getArtist(artistId); - + imageEl.src = this.api.getArtistPictureUrl(artist.picture, '750'); imageEl.style.backgroundColor = ''; nameEl.textContent = artist.name; metaEl.textContent = `${artist.popularity} popularity`; - + this.renderListWithTracks(tracksContainer, artist.tracks, true); - albumsContainer.innerHTML = artist.albums.map(album => + albumsContainer.innerHTML = artist.albums.map(album => this.createAlbumCardHTML(album) ).join(''); - + recentActivityManager.addArtist(artist); - + document.title = `${artist.name} - Monochrome`; } catch (error) { console.error("Failed to load artist:", error); - tracksContainer.innerHTML = albumsContainer.innerHTML = + tracksContainer.innerHTML = albumsContainer.innerHTML = createPlaceholder(`Could not load artist details. ${error.message}`); } } @@ -398,15 +399,15 @@ async renderPlaylistPage(playlistId) { this.api.settings.getInstances().then(instances => { const cachedData = this.api.settings.getCachedSpeedTests(); const speeds = cachedData?.speeds || {}; - + container.innerHTML = instances.map((url, index) => { const speedInfo = speeds[url]; - const speedText = speedInfo - ? (speedInfo.speed === Infinity - ? `Failed` + const speedText = speedInfo + ? (speedInfo.speed === Infinity + ? `Failed` : `${speedInfo.speed.toFixed(0)}ms`) : ''; - + return `
  • @@ -436,4 +437,4 @@ async renderPlaylistPage(playlistId) { } }); } -} \ No newline at end of file +} diff --git a/js/utils.js b/js/utils.js index 97c3826..12aa916 100644 --- a/js/utils.js +++ b/js/utils.js @@ -1,4 +1,4 @@ -// utils.js +//js/utils.js export const QUALITY = 'LOSSLESS'; @@ -65,14 +65,14 @@ export const getExtensionForQuality = (quality) => { export const buildTrackFilename = (track, quality) => { const template = localStorage.getItem('filename-template') || '{trackNumber} - {artist} - {title}'; const extension = getExtensionForQuality(quality); - + const data = { trackNumber: track.trackNumber, artist: track.artist?.name, title: getTrackTitle(track), album: track.album?.title }; - + return formatTemplate(template, data) + '.' + extension; }; @@ -83,21 +83,21 @@ const sanitizeToken = (value) => { export const normalizeQualityToken = (value) => { if (!value) return null; - + const token = sanitizeToken(value); - + for (const [quality, aliases] of Object.entries(QUALITY_TOKENS)) { if (aliases.includes(token)) { return quality; } } - + return null; }; export const deriveQualityFromTags = (rawTags) => { if (!Array.isArray(rawTags)) return null; - + const candidates = []; for (const tag of rawTags) { if (typeof tag !== 'string') continue; @@ -106,37 +106,37 @@ export const deriveQualityFromTags = (rawTags) => { candidates.push(normalized); } } - + return pickBestQuality(candidates); }; export const pickBestQuality = (candidates) => { let best = null; let bestRank = Infinity; - + for (const candidate of candidates) { if (!candidate) continue; const rank = QUALITY_PRIORITY.indexOf(candidate); const currentRank = rank === -1 ? Infinity : rank; - + if (currentRank < bestRank) { best = candidate; bestRank = currentRank; } } - + return best; }; export const deriveTrackQuality = (track) => { if (!track) return null; - + const candidates = [ deriveQualityFromTags(track.mediaMetadata?.tags), deriveQualityFromTags(track.album?.mediaMetadata?.tags), normalizeQualityToken(track.audioQuality) ]; - + return pickBestQuality(candidates); }; @@ -190,10 +190,10 @@ export const calculateTotalDuration = (tracks) => { export const formatDuration = (seconds) => { if (!seconds || isNaN(seconds)) return '0 min'; - + const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); - + if (hours > 0) { return `${hours} hr ${minutes} min`; } diff --git a/styles.css b/styles.css index df9f4a9..830bbdd 100644 --- a/styles.css +++ b/styles.css @@ -1,3 +1,4 @@ + :root { --spacing-xs: 0.5rem; --spacing-sm: 0.75rem; @@ -163,7 +164,7 @@ kbd { display: grid; height: 100vh; height: 100dvh; - grid-template: + grid-template: "sidebar main" 1fr "player player" auto / 280px 1fr; } @@ -318,11 +319,11 @@ kbd { } @keyframes fadeIn { - from { + from { opacity: 0; transform: translateY(4px); } - to { + to { opacity: 1; transform: translateY(0); } @@ -1806,16 +1807,16 @@ input:checked + .slider::before { .app-container { grid-template-columns: 240px 1fr; } - + .card-grid { grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: var(--spacing-md); } - + .detail-header-info .title { font-size: 3rem; } - + .main-content { padding: var(--spacing-lg); } @@ -1823,25 +1824,25 @@ input:checked + .slider::before { @media (max-width: 768px) { .app-container { - grid-template: + grid-template: "header" auto "main" 1fr "player" auto / 1fr; height: 100vh; height: 100dvh; } - + .main-content { padding: var(--spacing-md); grid-area: main; } - + .main-header { grid-area: header; padding: var(--spacing-md) var(--spacing-md) 0; margin-bottom: var(--spacing-md); } - + .sidebar { position: fixed; top: 0; @@ -1850,37 +1851,37 @@ input:checked + .slider::before { transform: translateX(-100%); box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); } - + .sidebar.is-open { transform: translateX(0); } - + .hamburger-menu { display: block; } - + #sidebar-overlay.is-visible { display: block; } - + .search-bar { max-width: none; } - + .content-section { margin-bottom: var(--spacing-xl); } - + .section-title { font-size: 1.5rem; margin-bottom: var(--spacing-md); } - + .card-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: var(--spacing-md); } - + .detail-header { flex-direction: column; align-items: flex-start; @@ -1888,229 +1889,229 @@ input:checked + .slider::before { padding-bottom: var(--spacing-md); margin-bottom: var(--spacing-lg); } - + .detail-header-image { width: 150px; height: 150px; } - + .detail-header-info .title { font-size: 2rem; line-height: 1.2; } - + .detail-header-info .meta { font-size: 0.85rem; gap: 0.35rem; } - + .detail-header-actions, .btn-primary { width: 100%; } - + .now-playing-bar { - grid-template: + grid-template: "track controls" auto "progress progress" auto / 1fr auto; gap: var(--spacing-sm); padding: var(--spacing-sm) var(--spacing-md); height: auto; } - + .now-playing-bar .track-info { grid-area: track; min-width: 0; } - + .track-info { gap: var(--spacing-sm); } - + .track-info .cover { width: 48px; height: 48px; } - + .track-info .details { min-width: 0; flex: 1; } - + .track-info .details .title { font-size: 0.9rem; } - + .track-info .details .artist { font-size: 0.75rem; } - + .now-playing-bar .volume-controls { grid-area: controls; display: flex; gap: 0.25rem; justify-content: flex-end; } - + .now-playing-bar .player-controls { grid-area: progress; width: 100%; } - + .player-controls .buttons { gap: var(--spacing-sm); margin-bottom: var(--spacing-xs); } - + .player-controls .buttons button { width: 28px; height: 28px; } - + .player-controls .buttons .play-pause-btn { width: 32px; height: 32px; } - + .player-controls .progress-container { max-width: none; font-size: 0.75rem; gap: 0.5rem; } - + .desktop-only { display: none !important; } - + .volume-controls button:not(.desktop-only) { display: flex; } - + .volume-controls button { padding: 0.375rem; min-width: 32px; min-height: 32px; } - + .volume-controls button svg { width: 18px; height: 18px; } - + #download-notifications { bottom: 10px; right: 10px; left: 10px; max-width: none; } - + .track-menu-btn { opacity: 1; } - + .about-links { flex-direction: column; } - + .github-link { width: 100%; justify-content: center; } - + .setting-item { flex-direction: column; align-items: flex-start; gap: var(--spacing-md); } - + .setting-item .info { width: 100%; } - + .template-input { max-width: none; font-size: 0.85rem; } - + .track-item { grid-template-columns: 28px 1fr 45px 32px; gap: var(--spacing-sm); padding: var(--spacing-sm); } - + .track-number { font-size: 0.8rem; width: 28px; } - + .track-item-info { gap: var(--spacing-sm); min-width: 0; overflow: hidden; } - + .track-item-cover { width: 36px; height: 36px; } - + .track-item-details { min-width: 0; overflow: hidden; } - + .track-item-details .title { font-size: 0.85rem; } - + .track-item-details .artist { font-size: 0.75rem; } - + .track-item-duration { font-size: 0.75rem; text-align: right; white-space: nowrap; } - + .track-menu-btn { padding: 0.5rem; margin: 0; } - + .track-menu-btn svg { width: 18px; height: 18px; } - + .queue-track-item { grid-template-columns: 24px 1fr 40px 28px; gap: var(--spacing-sm); padding: var(--spacing-sm); } - + .queue-track-item .drag-handle { width: 24px; } - + .queue-track-item .drag-handle svg { width: 14px; height: 14px; } - + .queue-track-item .track-item-cover { width: 36px; height: 36px; } - + .queue-track-item .track-menu-btn { padding: 0.5rem; } - + .sidebar-nav .nav-item a { padding: 1rem 0.75rem; } - + .offline-notification, .update-notification, .install-prompt { @@ -2126,102 +2127,102 @@ input:checked + .slider::before { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: var(--spacing-sm); } - + .section-title { font-size: 1.25rem; } - + .detail-header-info .title { font-size: 1.75rem; } - + .search-tab { padding: var(--spacing-sm) var(--spacing-md); font-size: 0.9rem; } - + .player-controls .buttons { gap: 0.25rem; } - + .player-controls .buttons button { width: 24px; height: 24px; } - + .player-controls .buttons button svg { width: 16px; height: 16px; } - + .player-controls .buttons .play-pause-btn { width: 28px; height: 28px; } - + .player-controls .buttons .play-pause-btn svg { width: 18px; height: 18px; } - + .volume-controls button { padding: 0.25rem; min-width: 28px; min-height: 28px; } - + .volume-controls button svg { width: 16px; height: 16px; } - + .track-item { grid-template-columns: 24px 1fr 40px 28px; gap: 0.375rem; padding: 0.5rem; } - + .track-number { font-size: 0.75rem; width: 24px; } - + .track-item-cover { width: 32px; height: 32px; } - + .track-item-details .title { font-size: 0.8rem; } - + .track-item-details .artist { font-size: 0.7rem; } - + .track-item-duration { font-size: 0.7rem; } - + .track-menu-btn { padding: 0.25rem; } - + .track-menu-btn svg { width: 16px; height: 16px; } - + .queue-track-item { grid-template-columns: 20px 1fr 36px 24px; gap: 0.375rem; padding: 0.5rem; } - + .queue-track-item .drag-handle { width: 20px; } - + .queue-track-item .track-item-cover { width: 32px; height: 32px; @@ -2233,7 +2234,7 @@ input:checked + .slider::before { justify-content: space-between; width: 100%; } - + #shuffle-btn, #repeat-btn { display: none; @@ -2245,11 +2246,11 @@ input:checked + .slider::before { grid-template-columns: 1fr 2fr auto; padding: var(--spacing-md); } - + .volume-controls { display: flex; } - + .desktop-only { display: flex; } @@ -2272,7 +2273,7 @@ input:checked + .slider::before { .volume-bar { height: 8px; } - + .progress-bar .progress-fill::after, .volume-bar .volume-fill::after { content: ''; @@ -2286,17 +2287,17 @@ input:checked + .slider::before { border-radius: 50%; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); } - + .track-item, .queue-track-item { padding: var(--spacing-md) var(--spacing-sm); } - + button { min-height: 44px; min-width: 44px; } - + .player-controls .buttons button { min-height: 36px; min-width: 36px; @@ -2307,11 +2308,11 @@ input:checked + .slider::before { .main-header { padding-top: max(var(--spacing-md), env(safe-area-inset-top)); } - + .now-playing-bar { padding-bottom: max(var(--spacing-md), env(safe-area-inset-bottom)); } - + .sidebar { padding-top: max(1.5rem, env(safe-area-inset-top)); } @@ -2514,27 +2515,27 @@ input:checked + .slider::before { .lyrics-panel { width: 100vw; } - + .synced-line { font-size: 1rem; } - + .synced-line.active { font-size: 1.125rem; } - + .karaoke-title { font-size: 1.5rem; } - + .karaoke-artist { font-size: 1rem; } - + .karaoke-line { font-size: 1.25rem; } - + .karaoke-line.active { font-size: 1.75rem; } @@ -2714,32 +2715,32 @@ input:checked + .slider::before { padding-bottom: var(--spacing-md); margin-bottom: var(--spacing-lg); } - + #playlist-detail-image { width: 150px; height: 150px; } - + #playlist-detail-title { font-size: 2rem; line-height: 1.2; } - + #playlist-detail-meta { font-size: 0.85rem; gap: 0.35rem; } - + #playlist-detail-description { font-size: 0.85rem; max-width: none; } - + #page-playlist .detail-actions { width: 100%; flex-direction: column; } - + #play-playlist-btn, #download-playlist-btn { width: 100%; @@ -2756,4 +2757,4 @@ input:checked + .slider::before { #playlist-detail-title { font-size: 4rem; } -} \ No newline at end of file +}