feat(ui): add search history and shuffle buttons
- Implement persistent search history with 'Clear All' functionality - Add shuffle buttons for album and artist headers with robust logic - Fix repeat mode incorrectly defaulting to ON on startup - Adjust CSV progress bar position to avoid control overlap - Remove obsolete 'Inline Buttons' track action setting - Fix search history dropdown stacking and icon selector specificity
This commit is contained in:
parent
601262b70f
commit
c34d3a7db6
7 changed files with 2182 additions and 2730 deletions
4617
index.html
4617
index.html
File diff suppressed because one or more lines are too long
71
js/app.js
71
js/app.js
|
|
@ -464,21 +464,67 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
const btn = e.target.closest('#play-album-btn');
|
||||
if (btn.disabled) return;
|
||||
|
||||
const albumId = window.location.pathname.split('/')[2];
|
||||
const pathParts = window.location.pathname.split('/');
|
||||
const albumIndex = pathParts.indexOf('album');
|
||||
const albumId = albumIndex !== -1 ? pathParts[albumIndex + 1] : null;
|
||||
|
||||
if (!albumId) return;
|
||||
|
||||
try {
|
||||
const { tracks } = await api.getAlbum(albumId);
|
||||
if (tracks.length > 0) {
|
||||
player.setQueue(tracks, 0);
|
||||
document.getElementById('shuffle-btn').classList.remove('active');
|
||||
if (tracks && tracks.length > 0) {
|
||||
// Sort tracks by disc and track number for consistent playback
|
||||
const sortedTracks = [...tracks].sort((a, b) => {
|
||||
const discA = a.volumeNumber ?? a.discNumber ?? 1;
|
||||
const discB = b.volumeNumber ?? b.discNumber ?? 1;
|
||||
if (discA !== discB) return discA - discB;
|
||||
return a.trackNumber - b.trackNumber;
|
||||
});
|
||||
|
||||
player.setQueue(sortedTracks, 0);
|
||||
const shuffleBtn = document.getElementById('shuffle-btn');
|
||||
if (shuffleBtn) shuffleBtn.classList.remove('active');
|
||||
player.shuffleActive = false;
|
||||
player.playTrackFromQueue();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to play album:', error);
|
||||
alert('Failed to play album: ' + error.message);
|
||||
showNotification('Failed to play album');
|
||||
}
|
||||
}
|
||||
|
||||
if (e.target.closest('#shuffle-album-btn')) {
|
||||
const btn = e.target.closest('#shuffle-album-btn');
|
||||
if (btn.disabled) return;
|
||||
|
||||
const pathParts = window.location.pathname.split('/');
|
||||
const albumIndex = pathParts.indexOf('album');
|
||||
const albumId = albumIndex !== -1 ? pathParts[albumIndex + 1] : null;
|
||||
|
||||
if (!albumId) return;
|
||||
|
||||
try {
|
||||
const { tracks } = await api.getAlbum(albumId);
|
||||
if (tracks && tracks.length > 0) {
|
||||
const shuffledTracks = [...tracks].sort(() => Math.random() - 0.5);
|
||||
player.setQueue(shuffledTracks, 0);
|
||||
const shuffleBtn = document.getElementById('shuffle-btn');
|
||||
if (shuffleBtn) shuffleBtn.classList.remove('active');
|
||||
player.shuffleActive = false;
|
||||
player.playTrackFromQueue();
|
||||
showNotification('Shuffling album');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to shuffle album:', error);
|
||||
showNotification('Failed to shuffle album');
|
||||
}
|
||||
}
|
||||
|
||||
if (e.target.closest('#shuffle-artist-btn')) {
|
||||
const btn = e.target.closest('#shuffle-artist-btn');
|
||||
if (btn.disabled) return;
|
||||
document.getElementById('play-artist-radio-btn')?.click();
|
||||
}
|
||||
if (e.target.closest('#download-mix-btn')) {
|
||||
const btn = e.target.closest('#download-mix-btn');
|
||||
if (btn.disabled) return;
|
||||
|
|
@ -1180,6 +1226,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
|
||||
const performSearch = debounce((query) => {
|
||||
if (query) {
|
||||
ui.addToSearchHistory(query);
|
||||
navigate(`/search/${encodeURIComponent(query)}`);
|
||||
}
|
||||
}, 300);
|
||||
|
|
@ -1191,11 +1238,25 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
}
|
||||
});
|
||||
|
||||
searchInput.addEventListener('focus', () => {
|
||||
ui.renderSearchHistory();
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!e.target.closest('.search-bar')) {
|
||||
const historyEl = document.getElementById('search-history');
|
||||
if (historyEl) historyEl.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
searchForm.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
const query = searchInput.value.trim();
|
||||
if (query) {
|
||||
ui.addToSearchHistory(query);
|
||||
navigate(`/search/${encodeURIComponent(query)}`);
|
||||
const historyEl = document.getElementById('search-history');
|
||||
if (historyEl) historyEl.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
21
js/events.js
21
js/events.js
|
|
@ -40,7 +40,7 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) {
|
|||
shuffleBtn.classList.add('active');
|
||||
}
|
||||
|
||||
if (player.repeatMode !== REPEAT_MODE.OFF) {
|
||||
if (player.repeatMode && player.repeatMode !== REPEAT_MODE.OFF) {
|
||||
repeatBtn.classList.add('active');
|
||||
if (player.repeatMode === REPEAT_MODE.ONE) {
|
||||
repeatBtn.classList.add('repeat-one');
|
||||
|
|
@ -808,10 +808,9 @@ export async function handleTrackAction(
|
|||
return `
|
||||
<div class="modal-option ${alreadyContains ? 'already-contains' : ''}" data-id="${p.id}">
|
||||
<span>${p.name}</span>
|
||||
${
|
||||
alreadyContains
|
||||
? `<button class="remove-from-playlist-btn-modal" title="Remove from playlist" style="background: transparent; border: none; color: inherit; cursor: pointer; padding: 4px; display: flex; align-items: center;">${SVG_BIN}</button>`
|
||||
: ''
|
||||
${alreadyContains
|
||||
? `<button class="remove-from-playlist-btn-modal" title="Remove from playlist" style="background: transparent; border: none; color: inherit; cursor: pointer; padding: 4px; display: flex; align-items: center;">${SVG_BIN}</button>`
|
||||
: ''
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -1109,12 +1108,12 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
|
|||
const type = card.dataset.albumId
|
||||
? 'album'
|
||||
: card.dataset.playlistId
|
||||
? 'playlist'
|
||||
: card.dataset.mixId
|
||||
? 'mix'
|
||||
: card.dataset.href
|
||||
? card.dataset.href.split('/')[1]
|
||||
: 'item';
|
||||
? 'playlist'
|
||||
: card.dataset.mixId
|
||||
? 'mix'
|
||||
: card.dataset.href
|
||||
? card.dataset.href.split('/')[1]
|
||||
: 'item';
|
||||
const id = card.dataset.albumId || card.dataset.playlistId || card.dataset.mixId;
|
||||
|
||||
const item = trackDataStore.get(card) || {
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ export class Player {
|
|||
this.originalQueueBeforeShuffle = savedState.originalQueueBeforeShuffle || [];
|
||||
this.currentQueueIndex = savedState.currentQueueIndex ?? -1;
|
||||
this.shuffleActive = savedState.shuffleActive || false;
|
||||
this.repeatMode = savedState.repeatMode || REPEAT_MODE.OFF;
|
||||
this.repeatMode = savedState.repeatMode !== undefined ? savedState.repeatMode : REPEAT_MODE.OFF;
|
||||
|
||||
// Restore current track if queue exists and index is valid
|
||||
const currentQueue = this.shuffleActive ? this.shuffledQueue : this.queue;
|
||||
|
|
@ -244,7 +244,7 @@ export class Player {
|
|||
// Warm connection/cache
|
||||
// For Blob URLs (DASH), this head request is not needed and can cause errors.
|
||||
if (!streamUrl.startsWith('blob:')) {
|
||||
fetch(streamUrl, { method: 'HEAD', signal: this.preloadAbortController.signal }).catch(() => {});
|
||||
fetch(streamUrl, { method: 'HEAD', signal: this.preloadAbortController.signal }).catch(() => { });
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.name !== 'AbortError') {
|
||||
|
|
|
|||
|
|
@ -466,7 +466,8 @@ export const trackListSettings = {
|
|||
|
||||
getMode() {
|
||||
try {
|
||||
const mode = localStorage.getItem(this.STORAGE_KEY) || 'dropdown';
|
||||
let mode = localStorage.getItem(this.STORAGE_KEY) || 'dropdown';
|
||||
if (mode === 'inline') mode = 'dropdown';
|
||||
document.documentElement.setAttribute('data-track-actions-mode', mode);
|
||||
return mode;
|
||||
} catch {
|
||||
|
|
|
|||
93
js/ui.js
93
js/ui.js
|
|
@ -1533,10 +1533,10 @@ export class UIRenderer {
|
|||
dateDisplay =
|
||||
window.innerWidth > 768
|
||||
? releaseDate.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
: year;
|
||||
}
|
||||
}
|
||||
|
|
@ -2268,9 +2268,9 @@ export class UIRenderer {
|
|||
<span>${artist.popularity}% popularity</span>
|
||||
<div class="artist-tags">
|
||||
${(artist.artistRoles || [])
|
||||
.filter((role) => role.category)
|
||||
.map((role) => `<span class="artist-tag">${role.category}</span>`)
|
||||
.join('')}
|
||||
.filter((role) => role.category)
|
||||
.map((role) => `<span class="artist-tag">${role.category}</span>`)
|
||||
.join('')}
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
|
@ -2854,4 +2854,83 @@ export class UIRenderer {
|
|||
artistEl.textContent = e.message || 'Track not found or unavailable';
|
||||
}
|
||||
}
|
||||
|
||||
renderSearchHistory() {
|
||||
const historyEl = document.getElementById('search-history');
|
||||
if (!historyEl) return;
|
||||
|
||||
const history = JSON.parse(localStorage.getItem('search-history') || '[]');
|
||||
if (history.length === 0) {
|
||||
historyEl.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
historyEl.innerHTML = history
|
||||
.map(
|
||||
(query) => `
|
||||
<div class="search-history-item" data-query="${escapeHtml(query)}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="history-icon">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<polyline points="12 6 12 12 16 14"></polyline>
|
||||
</svg>
|
||||
<span class="query-text">${escapeHtml(query)}</span>
|
||||
<span class="delete-history-btn" title="Remove from history">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join('') +
|
||||
`
|
||||
<div class="search-history-clear-all" id="clear-search-history">
|
||||
Clear all history
|
||||
</div>
|
||||
`;
|
||||
|
||||
historyEl.style.display = 'block';
|
||||
|
||||
// Add event listeners
|
||||
historyEl.querySelectorAll('.search-history-item').forEach((item) => {
|
||||
item.addEventListener('click', (e) => {
|
||||
if (e.target.closest('.delete-history-btn')) {
|
||||
e.stopPropagation();
|
||||
this.removeFromSearchHistory(item.dataset.query);
|
||||
return;
|
||||
}
|
||||
const query = item.dataset.query;
|
||||
const searchInput = document.getElementById('search-input');
|
||||
if (searchInput) {
|
||||
searchInput.value = query;
|
||||
searchInput.dispatchEvent(new Event('input'));
|
||||
historyEl.style.display = 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const clearBtn = document.getElementById('clear-search-history');
|
||||
if (clearBtn) {
|
||||
clearBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
localStorage.removeItem('search-history');
|
||||
this.renderSearchHistory();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
removeFromSearchHistory(query) {
|
||||
let history = JSON.parse(localStorage.getItem('search-history') || '[]');
|
||||
history = history.filter((q) => q !== query);
|
||||
localStorage.setItem('search-history', JSON.stringify(history));
|
||||
this.renderSearchHistory();
|
||||
}
|
||||
|
||||
addToSearchHistory(query) {
|
||||
if (!query || query.trim().length === 0) return;
|
||||
let history = JSON.parse(localStorage.getItem('search-history') || '[]');
|
||||
history = history.filter((q) => q !== query);
|
||||
history.unshift(query);
|
||||
history = history.slice(0, 10);
|
||||
localStorage.setItem('search-history', JSON.stringify(history));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
103
styles.css
103
styles.css
|
|
@ -343,7 +343,11 @@ kbd {
|
|||
}
|
||||
|
||||
/* Ensure content sits on top of background */
|
||||
.main-header,
|
||||
.main-header {
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.page {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
|
@ -450,7 +454,7 @@ kbd {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.search-bar svg {
|
||||
.search-bar svg.search-icon {
|
||||
position: absolute;
|
||||
left: 0.75rem;
|
||||
top: 50%;
|
||||
|
|
@ -493,6 +497,7 @@ kbd {
|
|||
margin-bottom: var(--spacing-xl);
|
||||
gap: var(--spacing-md);
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.navigation-controls {
|
||||
|
|
@ -573,6 +578,83 @@ body.has-page-background .track-item:hover {
|
|||
background-color: var(--track-hover-bg, var(--secondary));
|
||||
}
|
||||
|
||||
.search-history {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-top: none;
|
||||
border-radius: 0 0 var(--radius) var(--radius);
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
z-index: 9999;
|
||||
box-shadow: var(--shadow-xl);
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
.search-history-item {
|
||||
padding: 0.75rem 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: background var(--transition);
|
||||
}
|
||||
|
||||
.search-history-item:hover {
|
||||
background: var(--secondary);
|
||||
}
|
||||
|
||||
.search-history-item .history-icon {
|
||||
color: var(--muted-foreground);
|
||||
opacity: 0.5;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-history-item .query-text {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.search-history-item .delete-history-btn {
|
||||
padding: 4px;
|
||||
color: var(--muted-foreground);
|
||||
opacity: 0.5;
|
||||
transition: opacity var(--transition);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-history-item .delete-history-btn:hover {
|
||||
opacity: 1;
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.search-history-clear-all {
|
||||
padding: 0.75rem 1rem;
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
color: var(--primary);
|
||||
cursor: pointer;
|
||||
border-top: 1px solid var(--border);
|
||||
transition: background var(--transition);
|
||||
}
|
||||
|
||||
.search-history-clear-all:hover {
|
||||
background: var(--secondary);
|
||||
}
|
||||
|
||||
input[type="search"]::-webkit-search-cancel-button {
|
||||
-webkit-appearance: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -637,6 +719,7 @@ body.has-page-background .track-item:hover {
|
|||
}
|
||||
|
||||
@keyframes pulse {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
|
|
@ -955,6 +1038,7 @@ body.has-page-background .track-item:hover {
|
|||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
|
||||
#home-recommended-songs,
|
||||
#artist-detail-tracks,
|
||||
#playlist-detail-recommended {
|
||||
|
|
@ -1080,7 +1164,8 @@ body.has-page-background .track-item:hover {
|
|||
|
||||
.track-item.dragging {
|
||||
opacity: 0.5;
|
||||
z-index: 1000;
|
||||
z-index: 2000;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.track-number {
|
||||
|
|
@ -1553,11 +1638,11 @@ body.has-page-background .track-item:hover {
|
|||
border-radius: 50%;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
input:checked+.slider {
|
||||
background-color: var(--primary);
|
||||
}
|
||||
|
||||
input:checked + .slider::before {
|
||||
input:checked+.slider::before {
|
||||
transform: translateX(16px);
|
||||
background-color: var(--primary-foreground);
|
||||
}
|
||||
|
|
@ -3362,8 +3447,8 @@ img[src=''] {
|
|||
/* fuck this chud ass shit bro */
|
||||
.csv-import-progress {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
bottom: 120px;
|
||||
right: 3%;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
|
|
@ -4049,7 +4134,7 @@ img[src=''] {
|
|||
}
|
||||
|
||||
.csv-import-progress {
|
||||
bottom: 10px;
|
||||
bottom: 145px;
|
||||
right: 10px;
|
||||
left: 10px;
|
||||
max-width: none;
|
||||
|
|
@ -4708,4 +4793,4 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
|
|||
width: 25px;
|
||||
height: 25px;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue