Merge pull request #19 from SamidyFR/ui-fixes

Mobile UX Improvements & SVG Refactoring
This commit is contained in:
Julien 2025-12-25 22:48:27 +01:00 committed by GitHub
commit 18182a8085
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 85 additions and 99 deletions

View file

@ -168,17 +168,9 @@
<div class="meta" id="album-detail-producer"></div>
<div class="detail-header-actions">
<button id="play-album-btn" class="btn-primary">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<polygon points="5 3 19 12 5 21 5 3"></polygon>
</svg>
<span>Play Album</span>
</button>
<button id="download-album-btn" class="btn-primary">
<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="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
<span>Download Album</span>
</button>
</div>
@ -196,17 +188,9 @@
<p id="playlist-detail-description" class="detail-description"></p>
<div class="detail-actions">
<button id="play-playlist-btn" class="btn-primary">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<polygon points="5 3 19 12 5 21 5 3"></polygon>
</svg>
<span>Play</span>
</button>
<button id="download-playlist-btn" class="btn-primary">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
<span>Download</span>
</button>
</div>
@ -229,11 +213,6 @@
<span>Artist Radio</span>
</button>
<button id="download-discography-btn" class="btn-primary">
<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="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
<span>Download Discography</span>
</button>
</div>
@ -276,6 +255,16 @@
<button class="btn-secondary" id="reset-custom-theme">Reset</button>
</div>
</div>
<div class="setting-item">
<div class="info">
<span class="label">Album Cover Background</span>
<span class="description">Use the album cover as a blurred background on album pages</span>
</div>
<label class="toggle-switch">
<input type="checkbox" id="album-background-toggle">
<span class="slider"></span>
</label>
</div>
<div class="setting-item">
<div class="info">
<span class="label">Last.fm Scrobbling</span>
@ -339,16 +328,6 @@
<span class="slider"></span>
</label>
</div>
<div class="setting-item">
<div class="info">
<span class="label">Album Cover Background</span>
<span class="description">Use the album cover as a blurred background on album pages</span>
</div>
<label class="toggle-switch">
<input type="checkbox" id="album-background-toggle">
<span class="slider"></span>
</label>
</div>
<div class="setting-item">
<div class="info">
<span class="label">Gapless Playback</span>
@ -373,6 +352,13 @@
</div>
<input type="text" id="zip-folder-template" class="template-input" placeholder="{albumTitle} - {albumArtist} - monochrome.tf">
</div>
<div class="setting-item">
<div class="info">
<span class="label">Keyboard Shortcuts</span>
<span class="description">View available keyboard shortcuts</span>
</div>
<button id="show-shortcuts-btn" class="btn-secondary">Show Shortcuts</button>
</div>
<div class="setting-item">
<div class="info">
<span class="label">Cache</span>

View file

@ -568,7 +568,11 @@ document.addEventListener('DOMContentLoaded', async () => {
}
});
if (!localStorage.getItem('shortcuts-shown')) {
document.getElementById('show-shortcuts-btn')?.addEventListener('click', () => {
showKeyboardShortcuts();
});
if (!localStorage.getItem('shortcuts-shown') && window.innerWidth > 768) {
setTimeout(() => {
showKeyboardShortcuts();
localStorage.setItem('shortcuts-shown', 'true');

View file

@ -1,5 +1,5 @@
//js/downloads.js
import { buildTrackFilename, sanitizeForFilename, RATE_LIMIT_ERROR_MESSAGE, getTrackArtists, getTrackTitle, formatTemplate } from './utils.js';
import { buildTrackFilename, sanitizeForFilename, RATE_LIMIT_ERROR_MESSAGE, getTrackArtists, getTrackTitle, formatTemplate, SVG_CLOSE } from './utils.js';
import { lyricsSettings } from './storage.js';
const downloadTasks = new Map();
@ -65,10 +65,7 @@ export function addDownloadTask(trackId, track, filename, api) {
<div class="download-status" style="font-size: 0.75rem; color: var(--muted-foreground); margin-top: 0.25rem;">Starting...</div>
</div>
<button class="download-cancel" style="background: transparent; border: none; color: var(--muted-foreground); cursor: pointer; padding: 4px; border-radius: 4px; transition: all 0.2s;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
${SVG_CLOSE}
</button>
</div>
`;
@ -132,10 +129,7 @@ export function completeDownloadTask(trackId, success = true, message = null) {
statusEl.textContent = message || '✗ Download failed';
statusEl.style.color = '#ef4444';
cancelBtn.innerHTML = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
${SVG_CLOSE}
`;
cancelBtn.onclick = () => removeDownloadTask(trackId);

View file

@ -37,6 +37,11 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler) {
updateTabTitle(player);
});
audioPlayer.addEventListener('playing', () => {
player.updateMediaSessionPlaybackState();
player.updateMediaSessionPositionState();
});
audioPlayer.addEventListener('pause', () => {
playPauseBtn.innerHTML = SVG_PLAY;
player.updateMediaSessionPlaybackState();

View file

@ -1,5 +1,5 @@
//js/lyrics.js
import { getTrackTitle, getTrackArtists } from './utils.js';
import { getTrackTitle, getTrackArtists, SVG_DOWNLOAD, SVG_CLOSE } from './utils.js';
export class LyricsManager {
constructor(api) {
@ -104,17 +104,10 @@ export function createLyricsPanel() {
<h3>Lyrics</h3>
<div class="lyrics-controls">
<button id="download-lrc-btn" class="btn-icon" title="Download LRC">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
${SVG_DOWNLOAD}
</button>
<button id="close-lyrics-btn" class="btn-icon" title="Close">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
${SVG_CLOSE}
</button>
</div>
</div>

View file

@ -305,9 +305,9 @@ export const nowPlayingSettings = {
getMode() {
try {
return localStorage.getItem(this.STORAGE_KEY) || 'album';
return localStorage.getItem(this.STORAGE_KEY) || 'cover';
} catch (e) {
return 'album';
return 'cover';
}
},

View file

@ -1,5 +1,5 @@
//js/ui-interactions.js
import { formatTime, trackDataStore, getTrackTitle, getTrackArtists } from './utils.js';
import { SVG_CLOSE, formatTime, trackDataStore, getTrackTitle, getTrackArtists } from './utils.js';
export function initializeUIInteractions(player, api) {
const sidebar = document.querySelector('.sidebar');
@ -78,10 +78,7 @@ export function initializeUIInteractions(player, api) {
</div>
<div class="track-item-duration">${formatTime(track.duration)}</div>
<button class="queue-remove-btn" data-track-index="${index}" title="Remove from queue">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
${SVG_CLOSE}
</button>
</div>
`;

View file

@ -1,5 +1,5 @@
//js/ui.js
import { formatTime, createPlaceholder, trackDataStore, hasExplicitContent, getTrackArtists, getTrackTitle, calculateTotalDuration, formatDuration } from './utils.js';
import { SVG_PLAY, SVG_DOWNLOAD, SVG_MENU, formatTime, createPlaceholder, trackDataStore, hasExplicitContent, getTrackArtists, getTrackTitle, calculateTotalDuration, formatDuration } from './utils.js';
import { recentActivityManager, backgroundSettings, trackListSettings } from './storage.js';
export class UIRenderer {
@ -42,11 +42,7 @@ export class UIRenderer {
createTrackMenuButton() {
return `
<button class="track-menu-btn" onclick="event.stopPropagation();" title="More options">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="1"></circle>
<circle cx="12" cy="5" r="1"></circle>
<circle cx="12" cy="19" r="1"></circle>
</svg>
${SVG_MENU}
</button>
`;
}
@ -61,7 +57,7 @@ export class UIRenderer {
}
createTrackItemHTML(track, index, showCover = false, hasMultipleDiscs = false) {
const playIconSmall = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>';
const playIconSmall = SVG_PLAY;
const trackImageHTML = showCover ? `<img src="${this.api.getCoverUrl(track.album?.cover, '80')}" alt="Track Cover" class="track-item-cover" loading="lazy">` : '';
let displayIndex;
@ -107,19 +103,11 @@ export class UIRenderer {
</svg>
</button>
<button class="track-action-btn" data-action="download" title="Download">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
${SVG_DOWNLOAD}
</button>
</div>
<button class="track-menu-btn" type="button" title="More options">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="1"></circle>
<circle cx="12" cy="5" r="1"></circle>
<circle cx="12" cy="19" r="1"></circle>
</svg>
${SVG_MENU}
</button>
`;
@ -488,6 +476,10 @@ export class UIRenderer {
const metaEl = document.getElementById('album-detail-meta');
const prodEl = document.getElementById('album-detail-producer');
const tracklistContainer = document.getElementById('album-detail-tracklist');
const playBtn = document.getElementById('play-album-btn');
if (playBtn) playBtn.innerHTML = `${SVG_PLAY}<span>Play Album</span>`;
const dlBtn = document.getElementById('download-album-btn');
if (dlBtn) dlBtn.innerHTML = `${SVG_DOWNLOAD}<span>Download Album</span>`;
imageEl.src = '';
imageEl.style.backgroundColor = 'var(--muted)';
@ -616,10 +608,14 @@ async renderPlaylistPage(playlistId) {
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');
const descEl = document.getElementById('playlist-detail-description');
const tracklistContainer = document.getElementById('playlist-detail-tracklist');
const playBtn = document.getElementById('play-playlist-btn');
if (playBtn) playBtn.innerHTML = `${SVG_PLAY}<span>Play</span>`;
const dlBtn = document.getElementById('download-playlist-btn');
if (dlBtn) dlBtn.innerHTML = `${SVG_DOWNLOAD}<span>Download</span>`;
imageEl.src = '';
imageEl.src = '';
imageEl.style.backgroundColor = 'var(--muted)';
titleEl.innerHTML = '<div class="skeleton" style="height: 48px; width: 300px; max-width: 90%;"></div>';
metaEl.innerHTML = '<div class="skeleton" style="height: 16px; width: 200px; max-width: 80%;"></div>';
@ -676,6 +672,8 @@ async renderPlaylistPage(playlistId) {
const metaEl = document.getElementById('artist-detail-meta');
const tracksContainer = document.getElementById('artist-detail-tracks');
const albumsContainer = document.getElementById('artist-detail-albums');
const dlBtn = document.getElementById('download-discography-btn');
if (dlBtn) dlBtn.innerHTML = `${SVG_DOWNLOAD}<span>Download Discography</span>`;
imageEl.src = '';
imageEl.style.backgroundColor = 'var(--muted)';
@ -727,7 +725,7 @@ async renderPlaylistPage(playlistId) {
container.innerHTML = instances.map((url, index) => {
const speedInfo = speeds[url];
const speedText = speedInfo
? (speedInfo.speed === Infinity
? (speedInfo.speed === Infinity || typeof speedInfo.speed !== 'number'
? `<span style="color: var(--muted-foreground); font-size: 0.8rem;">Failed</span>`
: `<span style="color: var(--muted-foreground); font-size: 0.8rem;">${speedInfo.speed.toFixed(0)}ms</span>`)
: '';

View file

@ -30,6 +30,9 @@ export const SVG_PLAY = '<svg xmlns="http://www.w3.org/2000/svg" width="24" heig
export const SVG_PAUSE = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect></svg>';
export const SVG_VOLUME = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon><path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path></svg>';
export const SVG_MUTE = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon><line x1="23" y1="9" x2="17" y2="15"></line><line x1="17" y1="9" x2="23" y2="15"></line></svg>';
export const SVG_DOWNLOAD = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>';
export const SVG_MENU = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg>';
export const SVG_CLOSE = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>';
export const formatTime = (seconds) => {
if (isNaN(seconds)) return '0:00';

View file

@ -142,6 +142,7 @@
box-sizing: border-box;
margin: 0;
padding: 0;
-webkit-tap-highlight-color: transparent;
}
html {
@ -869,6 +870,7 @@ body.has-page-background .track-item:hover {
cursor: pointer;
transition: all var(--transition);
box-shadow: var(--shadow-sm);
-webkit-tap-highlight-color: transparent;
}
.btn-primary:hover {
@ -1088,6 +1090,7 @@ input:checked + .slider::before {
height: 32px;
border-radius: 50%;
position: relative;
-webkit-tap-highlight-color: transparent;
}
.player-controls .buttons button:hover {
@ -1162,7 +1165,7 @@ input:checked + .slider::before {
.progress-bar .progress-fill {
width: 0;
height: 100%;
background-color: var(--foreground);
background-color: var(--muted-foreground);
border-radius: 3px;
transition: background-color 0.2s ease;
position: relative;
@ -1226,7 +1229,7 @@ input:checked + .slider::before {
.volume-controls .volume-bar .volume-fill {
width: var(--volume-level, 70%);
height: 100%;
background-color: var(--foreground);
background-color: var(--muted-foreground);
border-radius: 2px;
transition: background-color 0.2s ease;
position: relative;
@ -1664,12 +1667,6 @@ input:checked + .slider::before {
width: 100%;
}
#api-instance-manager {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid var(--border);
}
#api-instance-list {
list-style: none;
margin-bottom: 1rem;
@ -2196,25 +2193,32 @@ input:checked + .slider::before {
}
.detail-header {
flex-direction: column;
align-items: flex-start;
gap: var(--spacing-lg);
padding-bottom: var(--spacing-md);
flex-direction: row;
gap: var(--spacing-md);
padding-bottom: var(--spacing-sm);
margin-bottom: var(--spacing-lg);
}
.detail-header-image {
width: 150px;
height: 150px;
width: 120px;
height: 120px;
}
.detail-header-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: center;
}
.detail-header-info .title {
font-size: 2rem;
font-size: 1.5rem;
line-height: 1.2;
}
.detail-header-info .title.long-title {
font-size: 1.5rem;
font-size: 1.35rem;
}
.detail-header-info .title.very-long-title {
@ -2224,15 +2228,17 @@ input:checked + .slider::before {
.detail-header-info .meta {
font-size: 0.85rem;
gap: 0.35rem;
margin-top: 0.25em;
}
.detail-header-actions {
width: auto;
margin-top: 0.5em;
}
.detail-header-actions .btn-primary {
width: auto;
padding: 0.875rem;
padding: 0.5rem;
border-radius: 50%;
aspect-ratio: 1/1;
}
@ -2454,15 +2460,15 @@ input:checked + .slider::before {
}
.detail-header-info .title {
font-size: 1.75rem;
font-size: 1.25rem;
}
.detail-header-info .title.long-title {
font-size: 1.35rem;
font-size: 1.10rem;
}
.detail-header-info .title.very-long-title {
font-size: 1.1rem;
font-size: 0.9rem;
}
.search-tab {