Compare commits

...

1 Commits

Author SHA1 Message Date
eb2aed40c1 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>
2026-02-06 22:24:20 +03:00
3 changed files with 327 additions and 8 deletions

View File

@@ -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;

View File

@@ -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">

View File

@@ -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;
}
}