add "add to playlist" button on player, add sleep timer feature
This commit is contained in:
parent
6635f5ed4d
commit
0234df5a7c
5 changed files with 240 additions and 101 deletions
27
index.html
27
index.html
|
|
@ -804,6 +804,12 @@
|
||||||
<div class="player-actions-row">
|
<div class="player-actions-row">
|
||||||
<button id="now-playing-like-btn" class="like-btn" data-action="toggle-like" title="Save to Favorites" style="display: none;">
|
<button id="now-playing-like-btn" class="like-btn" data-action="toggle-like" title="Save to Favorites" style="display: none;">
|
||||||
</button>
|
</button>
|
||||||
|
<button id="now-playing-add-playlist-btn" title="Add to Playlist" class="desktop-only">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||||||
|
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
<button id="now-playing-mix-btn" class="mix-btn" data-action="track-mix" title="Track Mix" style="display: none;">
|
<button id="now-playing-mix-btn" class="mix-btn" data-action="track-mix" title="Track Mix" style="display: none;">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||||
<path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/>
|
<path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/>
|
||||||
|
|
@ -819,12 +825,11 @@
|
||||||
<line x1="12" y1="15" x2="12" y2="3"></line>
|
<line x1="12" y1="15" x2="12" y2="3"></line>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button id="cast-btn" title="Cast" class="desktop-only">
|
|
||||||
|
<button id="mobile-add-playlist-btn" class="mobile-only">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M2 8V6a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-6"></path>
|
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||||||
<path d="M2 12a9 9 0 0 1 9 9"></path>
|
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||||||
<path d="M2 17a5 5 0 0 1 5 5"></path>
|
|
||||||
<path d="M2 22h.01"></path>
|
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button id="queue-btn" title="Queue">
|
<button id="queue-btn" title="Queue">
|
||||||
|
|
@ -836,10 +841,22 @@
|
||||||
<div id="volume-bar" class="volume-bar">
|
<div id="volume-bar" class="volume-bar">
|
||||||
<div id="volume-fill" class="volume-fill"></div>
|
<div id="volume-fill" class="volume-fill"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<button id="sleep-timer-btn" title="Sleep Timer">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="10"/>
|
||||||
|
<polyline points="12,6 12,12 16,14"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
<script type="module" src="js/app.js"></script>
|
<script type="module" src="js/app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
</div>
|
||||||
|
</html>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
|
||||||
75
js/app.js
75
js/app.js
|
|
@ -16,78 +16,7 @@ import { db } from './db.js';
|
||||||
import { syncManager } from './firebase/sync.js';
|
import { syncManager } from './firebase/sync.js';
|
||||||
import { registerSW } from 'virtual:pwa-register';
|
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) {
|
function initializeKeyboardShortcuts(player, audioPlayer) {
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
|
|
@ -199,8 +128,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
initializeUIInteractions(player, api);
|
initializeUIInteractions(player, api);
|
||||||
initializeKeyboardShortcuts(player, audioPlayer);
|
initializeKeyboardShortcuts(player, audioPlayer);
|
||||||
|
|
||||||
const castBtn = document.getElementById('cast-btn');
|
|
||||||
initializeCasting(audioPlayer, castBtn);
|
|
||||||
|
|
||||||
// Restore UI state for the current track (like button, theme)
|
// Restore UI state for the current track (like button, theme)
|
||||||
if (player.currentTrack) {
|
if (player.currentTrack) {
|
||||||
|
|
@ -1171,3 +1099,4 @@ function showKeyboardShortcuts() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
103
js/events.js
103
js/events.js
|
|
@ -13,6 +13,7 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) {
|
||||||
const prevBtn = document.getElementById('prev-btn');
|
const prevBtn = document.getElementById('prev-btn');
|
||||||
const shuffleBtn = document.getElementById('shuffle-btn');
|
const shuffleBtn = document.getElementById('shuffle-btn');
|
||||||
const repeatBtn = document.getElementById('repeat-btn');
|
const repeatBtn = document.getElementById('repeat-btn');
|
||||||
|
const sleepTimerBtn = document.getElementById('sleep-timer-btn');
|
||||||
|
|
||||||
// History tracking
|
// History tracking
|
||||||
let historyLoggedTrackId = null;
|
let historyLoggedTrackId = null;
|
||||||
|
|
@ -113,6 +114,16 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) {
|
||||||
: (mode === REPEAT_MODE.ALL ? 'Repeat Queue' : 'Repeat One');
|
: (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
|
// Volume controls
|
||||||
const volumeBar = document.getElementById('volume-bar');
|
const volumeBar = document.getElementById('volume-bar');
|
||||||
const volumeFill = document.getElementById('volume-fill');
|
const volumeFill = document.getElementById('volume-fill');
|
||||||
|
|
@ -680,32 +691,84 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function renderQueue(player) {
|
const nowPlayingAddPlaylistBtn = document.getElementById('now-playing-add-playlist-btn');
|
||||||
// This will be called from queue module
|
if (nowPlayingAddPlaylistBtn) {
|
||||||
if (window.renderQueueFunction) {
|
nowPlayingAddPlaylistBtn.addEventListener('click', async (e) => {
|
||||||
window.renderQueueFunction();
|
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 = `
|
||||||
|
<div class="modal-overlay" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; display: flex; align-items: center; justify-content: center;">
|
||||||
|
<div class="modal-content" style="background: var(--card); padding: 2rem; border-radius: var(--radius); max-width: 300px; width: 90%;">
|
||||||
|
<h3 style="text-align: center; margin-bottom: 1.5rem;">Sleep Timer</h3>
|
||||||
|
<div class="timer-options" style="display: flex; flex-direction: column; gap: 0.5rem;">
|
||||||
|
<button class="timer-option btn-secondary" data-minutes="5">5 minutes</button>
|
||||||
|
<button class="timer-option btn-secondary" data-minutes="15">15 minutes</button>
|
||||||
|
<button class="timer-option btn-secondary" data-minutes="30">30 minutes</button>
|
||||||
|
<button class="timer-option btn-secondary" data-minutes="60">1 hour</button>
|
||||||
|
<button class="timer-option btn-secondary" data-minutes="120">2 hours</button>
|
||||||
|
<div style="display: flex; gap: 0.5rem; margin-top: 0.5rem;">
|
||||||
|
<input type="number" id="custom-minutes" placeholder="Custom" min="1" max="480" style="flex: 1; padding: 0.5rem; border: 1px solid var(--border); border-radius: var(--radius); background: var(--background); color: var(--foreground);">
|
||||||
|
<button class="timer-option btn-primary" id="custom-timer-btn" style="padding: 0.5rem 1rem;">Set</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions" style="display: flex; gap: 0.5rem; justify-content: center; margin-top: 1.5rem;">
|
||||||
|
<button id="cancel-sleep-timer" class="btn-secondary">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
|
||||||
|
modal.addEventListener('click', (e) => {
|
||||||
async function updateContextMenuLikeState(menu, track) {
|
if (e.target.id === 'cancel-sleep-timer' || e.target.classList.contains('modal-overlay')) {
|
||||||
const likeItem = menu.querySelector('[data-action="toggle-like"]');
|
modal.remove();
|
||||||
if (likeItem) {
|
return;
|
||||||
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';
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
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) {
|
function positionMenu(menu, x, y, anchorRect = null) {
|
||||||
|
|
|
||||||
82
js/player.js
82
js/player.js
|
|
@ -17,6 +17,11 @@ export class Player {
|
||||||
this.preloadAbortController = null;
|
this.preloadAbortController = null;
|
||||||
this.currentTrack = null;
|
this.currentTrack = null;
|
||||||
|
|
||||||
|
// Sleep timer properties
|
||||||
|
this.sleepTimer = null;
|
||||||
|
this.sleepTimerEndTime = null;
|
||||||
|
this.sleepTimerInterval = null;
|
||||||
|
|
||||||
this.loadQueueState();
|
this.loadQueueState();
|
||||||
this.setupMediaSession();
|
this.setupMediaSession();
|
||||||
|
|
||||||
|
|
@ -523,4 +528,81 @@ export class Player {
|
||||||
console.debug('Failed to update Media Session position:', error);
|
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 = `<span style="font-size: 12px; font-weight: bold;">${minutes}:${seconds.toString().padStart(2, '0')}</span>`;
|
||||||
|
timerBtn.title = `Sleep Timer: ${minutes}:${seconds.toString().padStart(2, '0')} remaining`;
|
||||||
|
timerBtn.classList.add('active');
|
||||||
|
} else {
|
||||||
|
timerBtn.innerHTML = `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="10"/>
|
||||||
|
<polyline points="12,6 12,12 16,14"/>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
timerBtn.title = 'Sleep Timer';
|
||||||
|
timerBtn.classList.remove('active');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
timerBtn.innerHTML = `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="10"/>
|
||||||
|
<polyline points="12,6 12,12 16,14"/>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
timerBtn.title = 'Sleep Timer';
|
||||||
|
timerBtn.classList.remove('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
48
styles.css
48
styles.css
|
|
@ -1570,6 +1570,37 @@ input:checked + .slider::before {
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
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 {
|
#context-menu {
|
||||||
display: none;
|
display: none;
|
||||||
position: fixed;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue