From 0234df5a7ce3bf7d68b64c963e4e76980de9ce25 Mon Sep 17 00:00:00 2001 From: Samidy Date: Thu, 8 Jan 2026 14:14:46 +0300 Subject: [PATCH] add "add to playlist" button on player, add sleep timer feature --- index.html | 27 ++++++++++--- js/app.js | 75 +----------------------------------- js/events.js | 107 ++++++++++++++++++++++++++++++++++++++++----------- js/player.js | 84 +++++++++++++++++++++++++++++++++++++++- styles.css | 48 +++++++++++++++++++++++ 5 files changed, 240 insertions(+), 101 deletions(-) diff --git a/index.html b/index.html index ed656ae..3e7b0b6 100644 --- a/index.html +++ b/index.html @@ -804,6 +804,12 @@
+ -
+ + + + + + diff --git a/js/app.js b/js/app.js index 69ae0da..a50c54d 100644 --- a/js/app.js +++ b/js/app.js @@ -16,78 +16,7 @@ import { db } from './db.js'; import { syncManager } from './firebase/sync.js'; import { registerSW } from 'virtual:pwa-register'; -function initializeCasting(audioPlayer, castBtn) { - if (!castBtn) return; - if ('remote' in audioPlayer) { - audioPlayer.remote.watchAvailability((available) => { - if (available) { - castBtn.style.display = 'flex'; - castBtn.classList.add('available'); - } - }).catch(err => { - console.log('Remote playback not available:', err); - if (window.innerWidth > 768) { - castBtn.style.display = 'flex'; - } - }); - - castBtn.addEventListener('click', () => { - if (!audioPlayer.src) { - alert('Please play a track first to enable casting.'); - return; - } - audioPlayer.remote.prompt().catch(err => { - if (err.name === 'NotAllowedError') return; - if (err.name === 'NotFoundError') { - alert('No remote playback devices (Chromecast/AirPlay) were found on your network.'); - return; - } - console.log('Cast prompt error:', err); - }); - }); - - 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'); - } else { - castBtn.classList.remove('connected'); - } - }); - } - else if (window.innerWidth > 768) { - castBtn.style.display = 'flex'; - castBtn.addEventListener('click', () => { - alert('Casting is not supported in this browser. Try Chrome for Chromecast or Safari for AirPlay.'); - }); - } -} function initializeKeyboardShortcuts(player, audioPlayer) { document.addEventListener('keydown', (e) => { @@ -199,8 +128,7 @@ document.addEventListener('DOMContentLoaded', async () => { initializeUIInteractions(player, api); initializeKeyboardShortcuts(player, audioPlayer); - const castBtn = document.getElementById('cast-btn'); - initializeCasting(audioPlayer, castBtn); + // Restore UI state for the current track (like button, theme) if (player.currentTrack) { @@ -1171,3 +1099,4 @@ function showKeyboardShortcuts() { } }); } + diff --git a/js/events.js b/js/events.js index f56fa95..d720f9c 100644 --- a/js/events.js +++ b/js/events.js @@ -13,6 +13,7 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) { const prevBtn = document.getElementById('prev-btn'); const shuffleBtn = document.getElementById('shuffle-btn'); const repeatBtn = document.getElementById('repeat-btn'); + const sleepTimerBtn = document.getElementById('sleep-timer-btn'); // History tracking let historyLoggedTrackId = null; @@ -113,6 +114,16 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) { : (mode === REPEAT_MODE.ALL ? 'Repeat Queue' : 'Repeat One'); }); + // Sleep Timer + sleepTimerBtn.addEventListener('click', () => { + if (player.isSleepTimerActive()) { + player.clearSleepTimer(); + showNotification('Sleep timer cancelled'); + } else { + showSleepTimerModal(player); + } + }); + // Volume controls const volumeBar = document.getElementById('volume-bar'); const volumeFill = document.getElementById('volume-fill'); @@ -680,44 +691,96 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen } }); } -} -function renderQueue(player) { - // This will be called from queue module - if (window.renderQueueFunction) { - window.renderQueueFunction(); + const nowPlayingAddPlaylistBtn = document.getElementById('now-playing-add-playlist-btn'); + if (nowPlayingAddPlaylistBtn) { + nowPlayingAddPlaylistBtn.addEventListener('click', async (e) => { + e.stopPropagation(); + if (player.currentTrack) { + await handleTrackAction('add-to-playlist', player.currentTrack, player, api, lyricsManager, 'track', ui, scrobbler); + } + }); + } + + // Mobile add playlist button functionality + const mobileAddPlaylistBtn = document.getElementById('mobile-add-playlist-btn'); + + if (mobileAddPlaylistBtn) { + mobileAddPlaylistBtn.addEventListener('click', async (e) => { + e.stopPropagation(); + if (player.currentTrack) { + await handleTrackAction('add-to-playlist', player.currentTrack, player, api, lyricsManager, 'track', ui, scrobbler); + } + }); } } +function showSleepTimerModal(player) { + const modal = document.createElement('div'); + modal.className = 'sleep-timer-modal'; + modal.innerHTML = ` + + `; + document.body.appendChild(modal); - -async function updateContextMenuLikeState(menu, track) { - const likeItem = menu.querySelector('[data-action="toggle-like"]'); - if (likeItem) { - const isLiked = await db.isFavorite('track', track.id); - likeItem.textContent = isLiked ? 'Remove from Favorites' : 'Add to Favorites'; - } - - const mixItem = menu.querySelector('[data-action="track-mix"]'); - if (mixItem) { - if (track.mixes && track.mixes.TRACK_MIX) { - mixItem.style.display = 'block'; - } else { - mixItem.style.display = 'none'; + modal.addEventListener('click', (e) => { + if (e.target.id === 'cancel-sleep-timer' || e.target.classList.contains('modal-overlay')) { + modal.remove(); + return; } - } + + const timerOption = e.target.closest('.timer-option'); + if (timerOption) { + const minutes = parseInt(timerOption.dataset.minutes); + if (minutes) { + player.setSleepTimer(minutes); + showNotification(`Sleep timer set for ${minutes} minute${minutes === 1 ? '' : 's'}`); + modal.remove(); + } + } + + if (e.target.id === 'custom-timer-btn') { + const customInput = document.getElementById('custom-minutes'); + const minutes = parseInt(customInput.value); + if (minutes && minutes > 0 && minutes <= 480) { + player.setSleepTimer(minutes); + showNotification(`Sleep timer set for ${minutes} minute${minutes === 1 ? '' : 's'}`); + modal.remove(); + } else { + showNotification('Please enter a valid number of minutes (1-480)'); + } + } + }); } function positionMenu(menu, x, y, anchorRect = null) { // Temporarily show to measure dimensions menu.style.visibility = 'hidden'; menu.style.display = 'block'; - + const menuWidth = menu.offsetWidth; const menuHeight = menu.offsetHeight; const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; - + let left = x; let top = y; diff --git a/js/player.js b/js/player.js index 8f68ea2..84773af 100644 --- a/js/player.js +++ b/js/player.js @@ -17,9 +17,14 @@ export class Player { this.preloadAbortController = null; this.currentTrack = null; + // Sleep timer properties + this.sleepTimer = null; + this.sleepTimerEndTime = null; + this.sleepTimerInterval = null; + this.loadQueueState(); this.setupMediaSession(); - + window.addEventListener('beforeunload', () => { this.saveQueueState(); }); @@ -523,4 +528,81 @@ export class Player { console.debug('Failed to update Media Session position:', error); } } + + // Sleep Timer Methods + setSleepTimer(minutes) { + this.clearSleepTimer(); // Clear any existing timer + + this.sleepTimerEndTime = Date.now() + (minutes * 60 * 1000); + + this.sleepTimer = setTimeout(() => { + this.audio.pause(); + this.clearSleepTimer(); + this.updateSleepTimerUI(); + }, minutes * 60 * 1000); + + // Update UI every second + this.sleepTimerInterval = setInterval(() => { + this.updateSleepTimerUI(); + }, 1000); + + this.updateSleepTimerUI(); + } + + clearSleepTimer() { + if (this.sleepTimer) { + clearTimeout(this.sleepTimer); + this.sleepTimer = null; + } + if (this.sleepTimerInterval) { + clearInterval(this.sleepTimerInterval); + this.sleepTimerInterval = null; + } + this.sleepTimerEndTime = null; + this.updateSleepTimerUI(); + } + + getSleepTimerRemaining() { + if (!this.sleepTimerEndTime) return null; + const remaining = Math.max(0, this.sleepTimerEndTime - Date.now()); + return Math.ceil(remaining / 1000); // Return seconds remaining + } + + isSleepTimerActive() { + return this.sleepTimer !== null; + } + + updateSleepTimerUI() { + const timerBtn = document.getElementById('sleep-timer-btn'); + if (!timerBtn) return; + + if (this.isSleepTimerActive()) { + const remaining = this.getSleepTimerRemaining(); + if (remaining > 0) { + const minutes = Math.floor(remaining / 60); + const seconds = remaining % 60; + timerBtn.innerHTML = `${minutes}:${seconds.toString().padStart(2, '0')}`; + timerBtn.title = `Sleep Timer: ${minutes}:${seconds.toString().padStart(2, '0')} remaining`; + timerBtn.classList.add('active'); + } else { + timerBtn.innerHTML = ` + + + + + `; + timerBtn.title = 'Sleep Timer'; + timerBtn.classList.remove('active'); + } + } else { + timerBtn.innerHTML = ` + + + + + `; + timerBtn.title = 'Sleep Timer'; + timerBtn.classList.remove('active'); + } + } } diff --git a/styles.css b/styles.css index f9d58fe..dc192bb 100644 --- a/styles.css +++ b/styles.css @@ -1570,6 +1570,37 @@ input:checked + .slider::before { box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); } +/* Sleep Timer Button */ +#sleep-timer-btn { + position: relative; + font-size: 0.8rem; + font-weight: bold; + color: var(--foreground); + transition: all var(--transition); +} + +#sleep-timer-btn:hover { + color: var(--highlight); +} + +#sleep-timer-btn.active { + color: var(--primary); + text-shadow: 0 0 8px rgba(var(--highlight-rgb), 0.5); +} + +#sleep-timer-btn svg { + width: 20px; + height: 20px; +} + +#sleep-timer-btn span { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + pointer-events: none; +} + #context-menu { display: none; position: fixed; @@ -3802,3 +3833,20 @@ img:not([src]), img[src=''] { } + +/* Default responsive classes */ +.mobile-only { + display: none !important; +} + +/* Mobile-specific overrides */ +@media (max-width: 768px) { + .mobile-only { + display: flex !important; + } + + .desktop-only { + display: none !important; + } +} +