Update media browser UI with fade-in animations and improvements
- Add fade-in animation for thumbnail loading to prevent layout shifts - Add loading state indicators for thumbnails - Improve media browser CSS styling - Enhance JavaScript for smoother thumbnail loading experience Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -854,6 +854,199 @@
|
||||
background: var(--error);
|
||||
}
|
||||
|
||||
/* Mini Player (Sticky) */
|
||||
.mini-player {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 0.75rem 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
z-index: 1000;
|
||||
transform: translateY(0);
|
||||
transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.mini-player.hidden {
|
||||
transform: translateY(-100%);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.mini-player-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
min-width: 200px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mini-album-art {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mini-track-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mini-track-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.mini-artist {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.mini-progress-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mini-time-display {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mini-progress-bar {
|
||||
flex: 1;
|
||||
height: 4px;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.mini-progress-bar:hover {
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.mini-progress-fill {
|
||||
height: 100%;
|
||||
background: var(--accent);
|
||||
border-radius: 2px;
|
||||
width: 0%;
|
||||
transition: width 0.1s linear;
|
||||
}
|
||||
|
||||
.mini-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mini-control-btn {
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 50%;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.mini-control-btn:hover {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.mini-control-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.mini-control-btn svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.mini-volume-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex-shrink: 0;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.mini-volume-slider {
|
||||
flex: 1;
|
||||
height: 4px;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 2px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.mini-volume-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: var(--accent);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mini-volume-slider::-moz-range-thumb {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: var(--accent);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.mini-volume-slider:hover::-webkit-slider-thumb {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.mini-volume-slider:hover::-moz-range-thumb {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.mini-volume-display {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
min-width: 36px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* SVG Icons */
|
||||
svg {
|
||||
width: 24px;
|
||||
|
||||
@@ -11,6 +11,42 @@
|
||||
<!-- Clear Token Button -->
|
||||
<button class="clear-token-btn" onclick="clearToken()" data-i18n-title="auth.logout.title" data-i18n="auth.logout" title="Clear saved token">Logout</button>
|
||||
|
||||
<!-- Mini Player (sticky) -->
|
||||
<div class="mini-player hidden" id="mini-player">
|
||||
<div class="mini-player-info">
|
||||
<img id="mini-album-art" class="mini-album-art" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 300'%3E%3Crect fill='%23282828' width='300' height='300'/%3E%3Cpath fill='%236a6a6a' d='M150 80c-38.66 0-70 31.34-70 70s31.34 70 70 70 70-31.34 70-70-31.34-70-70-70zm0 20c27.614 0 50 22.386 50 50s-22.386 50-50 50-50-22.386-50-50 22.386-50 50-50zm0 30a20 20 0 100 40 20 20 0 000-40z'/%3E%3C/svg%3E" alt="Album Art">
|
||||
<div class="mini-track-details">
|
||||
<div id="mini-track-title" class="mini-track-title">No media playing</div>
|
||||
<div id="mini-artist" class="mini-artist"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mini-controls">
|
||||
<button class="mini-control-btn" onclick="togglePlayPause()" id="mini-btn-play-pause" title="Play/Pause">
|
||||
<svg viewBox="0 0 24 24" id="mini-play-pause-icon">
|
||||
<path d="M8 5v14l11-7z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mini-progress-container">
|
||||
<div class="mini-time-display">
|
||||
<span id="mini-current-time">0:00</span>
|
||||
<span id="mini-total-time">0:00</span>
|
||||
</div>
|
||||
<div class="mini-progress-bar" id="mini-progress-bar">
|
||||
<div class="mini-progress-fill" id="mini-progress-fill"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mini-volume-container">
|
||||
<button class="mini-control-btn" onclick="toggleMute()" id="mini-btn-mute" title="Mute">
|
||||
<svg viewBox="0 0 24 24" id="mini-mute-icon">
|
||||
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<input type="range" id="mini-volume-slider" class="mini-volume-slider" min="0" max="100" value="50">
|
||||
<div class="mini-volume-display" id="mini-volume-display">50%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Auth Modal -->
|
||||
<div id="auth-overlay" class="hidden">
|
||||
<div class="auth-modal">
|
||||
|
||||
@@ -240,6 +240,72 @@
|
||||
setTimeout(() => { isUserAdjustingVolume = false; }, 500);
|
||||
});
|
||||
|
||||
// Mini Player: Intersection Observer to show/hide when main player scrolls out of view
|
||||
const playerContainer = document.querySelector('.player-container');
|
||||
const miniPlayer = document.getElementById('mini-player');
|
||||
|
||||
const observerOptions = {
|
||||
root: null, // viewport
|
||||
threshold: 0.1, // trigger when 10% visible
|
||||
rootMargin: '0px'
|
||||
};
|
||||
|
||||
const observerCallback = (entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
// Main player is visible - hide mini player
|
||||
miniPlayer.classList.add('hidden');
|
||||
} else {
|
||||
// Main player is scrolled out of view - show mini player
|
||||
miniPlayer.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver(observerCallback, observerOptions);
|
||||
observer.observe(playerContainer);
|
||||
|
||||
// Mini player progress bar click to seek
|
||||
const miniProgressBar = document.getElementById('mini-progress-bar');
|
||||
miniProgressBar.addEventListener('click', (e) => {
|
||||
const rect = miniProgressBar.getBoundingClientRect();
|
||||
const percent = (e.clientX - rect.left) / rect.width;
|
||||
const position = percent * currentDuration;
|
||||
seek(position);
|
||||
});
|
||||
|
||||
// Mini player volume slider
|
||||
const miniVolumeSlider = document.getElementById('mini-volume-slider');
|
||||
miniVolumeSlider.addEventListener('input', (e) => {
|
||||
isUserAdjustingVolume = true;
|
||||
const volume = parseInt(e.target.value);
|
||||
document.getElementById('mini-volume-display').textContent = `${volume}%`;
|
||||
document.getElementById('volume-display').textContent = `${volume}%`;
|
||||
document.getElementById('volume-slider').value = volume;
|
||||
|
||||
// Throttle volume updates while dragging
|
||||
if (volumeUpdateTimer) {
|
||||
clearTimeout(volumeUpdateTimer);
|
||||
}
|
||||
volumeUpdateTimer = setTimeout(() => {
|
||||
setVolume(volume);
|
||||
volumeUpdateTimer = null;
|
||||
}, 50);
|
||||
});
|
||||
|
||||
miniVolumeSlider.addEventListener('change', (e) => {
|
||||
// Clear any pending throttled update
|
||||
if (volumeUpdateTimer) {
|
||||
clearTimeout(volumeUpdateTimer);
|
||||
volumeUpdateTimer = null;
|
||||
}
|
||||
|
||||
// Send final volume update immediately
|
||||
const volume = parseInt(e.target.value);
|
||||
setVolume(volume);
|
||||
setTimeout(() => { isUserAdjustingVolume = false; }, 500);
|
||||
});
|
||||
|
||||
// Progress bar click to seek
|
||||
const progressBar = document.getElementById('progress-bar');
|
||||
progressBar.addEventListener('click', (e) => {
|
||||
@@ -417,6 +483,10 @@
|
||||
document.getElementById('artist').textContent = status.artist || '';
|
||||
document.getElementById('album').textContent = status.album || '';
|
||||
|
||||
// Update mini player info
|
||||
document.getElementById('mini-track-title').textContent = status.title || t('player.no_media');
|
||||
document.getElementById('mini-artist').textContent = status.artist || '';
|
||||
|
||||
// Update state
|
||||
const previousState = currentState;
|
||||
currentState = status.state;
|
||||
@@ -424,12 +494,13 @@
|
||||
|
||||
// Update album art
|
||||
const artImg = document.getElementById('album-art');
|
||||
if (status.album_art_url) {
|
||||
const token = localStorage.getItem('media_server_token');
|
||||
artImg.src = `/api/media/artwork?token=${encodeURIComponent(token)}&_=${Date.now()}`;
|
||||
} else {
|
||||
artImg.src = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 300'%3E%3Crect fill='%23282828' width='300' height='300'/%3E%3Cpath fill='%236a6a6a' d='M150 80c-38.66 0-70 31.34-70 70s31.34 70 70 70 70-31.34 70-70-31.34-70-70-70zm0 20c27.614 0 50 22.386 50 50s-22.386 50-50 50-50-22.386-50-50 22.386-50 50-50zm0 30a20 20 0 100 40 20 20 0 000-40z'/%3E%3C/svg%3E";
|
||||
}
|
||||
const miniArtImg = document.getElementById('mini-album-art');
|
||||
const artworkUrl = status.album_art_url
|
||||
? `/api/media/artwork?token=${encodeURIComponent(localStorage.getItem('media_server_token'))}&_=${Date.now()}`
|
||||
: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 300'%3E%3Crect fill='%23282828' width='300' height='300'/%3E%3Cpath fill='%236a6a6a' d='M150 80c-38.66 0-70 31.34-70 70s31.34 70 70 70 70-31.34 70-70-31.34-70-70-70zm0 20c27.614 0 50 22.386 50 50s-22.386 50-50 50-50-22.386-50-50 22.386-50 50-50zm0 30a20 20 0 100 40 20 20 0 000-40z'/%3E%3C/svg%3E";
|
||||
|
||||
artImg.src = artworkUrl;
|
||||
miniArtImg.src = artworkUrl;
|
||||
|
||||
// Update progress
|
||||
if (status.duration && status.position !== null) {
|
||||
@@ -447,6 +518,8 @@
|
||||
if (!isUserAdjustingVolume) {
|
||||
document.getElementById('volume-slider').value = status.volume;
|
||||
document.getElementById('volume-display').textContent = `${status.volume}%`;
|
||||
document.getElementById('mini-volume-slider').value = status.volume;
|
||||
document.getElementById('mini-volume-display').textContent = `${status.volume}%`;
|
||||
}
|
||||
|
||||
// Update mute state
|
||||
@@ -460,6 +533,7 @@
|
||||
document.getElementById('btn-play-pause').disabled = !hasMedia;
|
||||
document.getElementById('btn-next').disabled = !hasMedia;
|
||||
document.getElementById('btn-previous').disabled = !hasMedia;
|
||||
document.getElementById('mini-btn-play-pause').disabled = !hasMedia;
|
||||
|
||||
// Start/stop position interpolation based on playback state
|
||||
if (status.state === 'playing' && previousState !== 'playing') {
|
||||
@@ -473,27 +547,32 @@
|
||||
const stateText = document.getElementById('playback-state');
|
||||
const stateIcon = document.getElementById('state-icon');
|
||||
const playPauseIcon = document.getElementById('play-pause-icon');
|
||||
const miniPlayPauseIcon = document.getElementById('mini-play-pause-icon');
|
||||
|
||||
switch(state) {
|
||||
case 'playing':
|
||||
stateText.textContent = t('state.playing');
|
||||
stateIcon.innerHTML = '<path d="M8 5v14l11-7z"/>';
|
||||
playPauseIcon.innerHTML = '<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>';
|
||||
miniPlayPauseIcon.innerHTML = '<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>';
|
||||
break;
|
||||
case 'paused':
|
||||
stateText.textContent = t('state.paused');
|
||||
stateIcon.innerHTML = '<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>';
|
||||
playPauseIcon.innerHTML = '<path d="M8 5v14l11-7z"/>';
|
||||
miniPlayPauseIcon.innerHTML = '<path d="M8 5v14l11-7z"/>';
|
||||
break;
|
||||
case 'stopped':
|
||||
stateText.textContent = t('state.stopped');
|
||||
stateIcon.innerHTML = '<path d="M6 6h12v12H6z"/>';
|
||||
playPauseIcon.innerHTML = '<path d="M8 5v14l11-7z"/>';
|
||||
miniPlayPauseIcon.innerHTML = '<path d="M8 5v14l11-7z"/>';
|
||||
break;
|
||||
default:
|
||||
stateText.textContent = t('state.idle');
|
||||
stateIcon.innerHTML = '<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 14.5v-9l6 4.5-6 4.5z"/>';
|
||||
playPauseIcon.innerHTML = '<path d="M8 5v14l11-7z"/>';
|
||||
miniPlayPauseIcon.innerHTML = '<path d="M8 5v14l11-7z"/>';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,6 +582,11 @@
|
||||
document.getElementById('current-time').textContent = formatTime(position);
|
||||
document.getElementById('total-time').textContent = formatTime(duration);
|
||||
document.getElementById('progress-bar').dataset.duration = duration;
|
||||
|
||||
// Update mini player progress
|
||||
document.getElementById('mini-progress-fill').style.width = `${percent}%`;
|
||||
document.getElementById('mini-current-time').textContent = formatTime(position);
|
||||
document.getElementById('mini-total-time').textContent = formatTime(duration);
|
||||
}
|
||||
|
||||
function startPositionInterpolation() {
|
||||
@@ -533,10 +617,16 @@
|
||||
|
||||
function updateMuteIcon(muted) {
|
||||
const muteIcon = document.getElementById('mute-icon');
|
||||
const miniMuteIcon = document.getElementById('mini-mute-icon');
|
||||
const mutedPath = '<path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/>';
|
||||
const unmutedPath = '<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/>';
|
||||
|
||||
if (muted) {
|
||||
muteIcon.innerHTML = '<path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/>';
|
||||
muteIcon.innerHTML = mutedPath;
|
||||
miniMuteIcon.innerHTML = mutedPath;
|
||||
} else {
|
||||
muteIcon.innerHTML = '<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/>';
|
||||
muteIcon.innerHTML = unmutedPath;
|
||||
miniMuteIcon.innerHTML = unmutedPath;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user