style(css+js): apply biome formatter

This commit is contained in:
Edouard Vanbelle
2026-04-11 18:20:43 +02:00
parent d32a32c30a
commit c7eeb734d7
6 changed files with 222 additions and 100 deletions
+3 -3
View File
@@ -499,9 +499,9 @@
/* Playlist track count in selector */ /* Playlist track count in selector */
.playlist-track-count { .playlist-track-count {
margin-left: auto; margin-left: auto;
font-size: 12px; font-size: 12px;
color: #718096; color: #718096;
} }
/* Custom confirm dialog */ /* Custom confirm dialog */
+14 -8
View File
@@ -106,7 +106,9 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
transition: transform 0.15s, box-shadow 0.15s; transition:
transform 0.15s,
box-shadow 0.15s;
box-shadow: 0 2px 8px var(--color-accent-shadow, rgba(255, 94, 58, 0.25)); box-shadow: 0 2px 8px var(--color-accent-shadow, rgba(255, 94, 58, 0.25));
} }
@@ -426,7 +428,9 @@
} }
@keyframes spin { @keyframes spin {
to { transform: rotate(360deg); } to {
transform: rotate(360deg);
}
} }
/* Dark theme */ /* Dark theme */
@@ -757,7 +761,7 @@
} }
.player-queue::after { .player-queue::after {
content: ''; content: "";
position: absolute; position: absolute;
bottom: -8px; bottom: -8px;
right: 60px; right: 60px;
@@ -916,7 +920,7 @@
} }
.player-btn.repeat-one i::after { .player-btn.repeat-one i::after {
content: '1'; content: "1";
font-size: 8px; font-size: 8px;
position: absolute; position: absolute;
top: 0; top: 0;
@@ -1076,7 +1080,9 @@
padding: 4px 6px; padding: 4px 6px;
border-radius: 4px; border-radius: 4px;
opacity: 0; opacity: 0;
transition: opacity 0.15s, color 0.15s; transition:
opacity 0.15s,
color 0.15s;
font-size: 0.85rem; font-size: 0.85rem;
} }
.music-track:hover .music-track-remove-btn { .music-track:hover .music-track-remove-btn {
@@ -1105,7 +1111,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: rgba(0,0,0,0.4); background: rgba(0, 0, 0, 0.4);
color: #fff; color: #fff;
font-size: 1.1rem; font-size: 1.1rem;
opacity: 0; opacity: 0;
@@ -1140,7 +1146,7 @@
.music-shares-overlay { .music-shares-overlay {
position: fixed; position: fixed;
inset: 0; inset: 0;
background: rgba(0,0,0,0.45); background: rgba(0, 0, 0, 0.45);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -1154,7 +1160,7 @@
max-height: 80vh; max-height: 80vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
box-shadow: 0 8px 32px rgba(0,0,0,0.18); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
} }
.music-shares-header { .music-shares-header {
display: flex; display: flex;
+1 -1
View File
@@ -621,7 +621,7 @@ function setupEventListeners() {
switchToMusicSection(); switchToMusicSection();
break; break;
case 'nav.trash': case 'nav.trash':
switchToTrashSection(); switchToTrashSection();
break; break;
+1 -1
View File
@@ -334,7 +334,7 @@ function switchToMusicSection() {
// Hide breadcrumb // Hide breadcrumb
const breadcrumb = document.querySelector('.breadcrumb'); const breadcrumb = document.querySelector('.breadcrumb');
breadcrumb?.classList.add("hidden"); breadcrumb?.classList.add('hidden');
// Hide file containers // Hide file containers
toggleFileContainer(false); toggleFileContainer(false);
+12 -12
View File
@@ -1082,7 +1082,7 @@ const contextMenus = {
const dialog = document.getElementById('playlist-dialog'); const dialog = document.getElementById('playlist-dialog');
const container = document.getElementById('playlist-select-container'); const container = document.getElementById('playlist-select-container');
const filesInfo = document.getElementById('playlist-dialog-files-info'); const filesInfo = document.getElementById('playlist-dialog-files-info');
if (!dialog || !container) { if (!dialog || !container) {
console.error('Playlist dialog elements not found'); console.error('Playlist dialog elements not found');
return; return;
@@ -1099,7 +1099,7 @@ const contextMenus = {
// Reset selection // Reset selection
this._selectedPlaylistId = null; this._selectedPlaylistId = null;
container.innerHTML = '<div class="folder-select-loading"><i class="fas fa-spinner fa-spin"></i></div>'; container.innerHTML = '<div class="folder-select-loading"><i class="fas fa-spinner fa-spin"></i></div>';
// Reset add button state // Reset add button state
const addBtn = document.getElementById('playlist-add-btn'); const addBtn = document.getElementById('playlist-add-btn');
if (addBtn) addBtn.disabled = true; if (addBtn) addBtn.disabled = true;
@@ -1112,7 +1112,7 @@ const contextMenus = {
try { try {
const resp = await fetch('/api/playlists', { credentials: 'include' }); const resp = await fetch('/api/playlists', { credentials: 'include' });
if (!resp.ok) throw new Error('Failed to load playlists'); if (!resp.ok) throw new Error('Failed to load playlists');
const playlists = await resp.json(); const playlists = await resp.json();
this._renderPlaylistSelect(container, playlists); this._renderPlaylistSelect(container, playlists);
} catch (err) { } catch (err) {
@@ -1122,16 +1122,16 @@ const contextMenus = {
}, },
_renderPlaylistSelect(container, playlists) { _renderPlaylistSelect(container, playlists) {
const t = (key, fallback) => window.i18n ? window.i18n.t(key, fallback) : fallback; const t = (key, fallback) => (window.i18n ? window.i18n.t(key, fallback) : fallback);
container.innerHTML = ''; container.innerHTML = '';
if (playlists.length === 0) { if (playlists.length === 0) {
container.innerHTML = `<div class="folder-select-empty">${t('music.no_playlists', 'No playlists yet. Create one first!')}</div>`; container.innerHTML = `<div class="folder-select-empty">${t('music.no_playlists', 'No playlists yet. Create one first!')}</div>`;
return; return;
} }
playlists.forEach(playlist => { playlists.forEach((playlist) => {
const item = document.createElement('div'); const item = document.createElement('div');
item.className = 'folder-select-item'; item.className = 'folder-select-item';
item.dataset.id = playlist.id; item.dataset.id = playlist.id;
@@ -1140,15 +1140,15 @@ const contextMenus = {
<span>${this._escapeHtml(playlist.name)}</span> <span>${this._escapeHtml(playlist.name)}</span>
<span class="playlist-track-count">${playlist.track_count || 0} ${t('music.tracks', 'tracks')}</span> <span class="playlist-track-count">${playlist.track_count || 0} ${t('music.tracks', 'tracks')}</span>
`; `;
item.addEventListener('click', () => { item.addEventListener('click', () => {
container.querySelectorAll('.folder-select-item').forEach(el => el.classList.remove('selected')); container.querySelectorAll('.folder-select-item').forEach((el) => el.classList.remove('selected'));
item.classList.add('selected'); item.classList.add('selected');
this._selectedPlaylistId = playlist.id; this._selectedPlaylistId = playlist.id;
const addBtn = document.getElementById('playlist-add-btn'); const addBtn = document.getElementById('playlist-add-btn');
if (addBtn) addBtn.disabled = false; if (addBtn) addBtn.disabled = false;
}); });
container.appendChild(item); container.appendChild(item);
}); });
}, },
@@ -1156,7 +1156,7 @@ const contextMenus = {
async addSelectedFilesToPlaylist() { async addSelectedFilesToPlaylist() {
const playlistId = this._selectedPlaylistId; const playlistId = this._selectedPlaylistId;
const files = window.app.playlistDialogFiles || []; const files = window.app.playlistDialogFiles || [];
if (!playlistId || files.length === 0) return; if (!playlistId || files.length === 0) return;
const addBtn = document.getElementById('playlist-add-btn'); const addBtn = document.getElementById('playlist-add-btn');
@@ -1170,7 +1170,7 @@ const contextMenus = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...getCsrfHeaders() ...getCsrfHeaders()
}, },
body: JSON.stringify({ file_ids: files.map(f => f.id) }) body: JSON.stringify({ file_ids: files.map((f) => f.id) })
}); });
if (!resp.ok) { if (!resp.ok) {
+191 -75
View File
@@ -55,15 +55,15 @@ const musicView = {
if (this.loading) return; if (this.loading) return;
this.loading = true; this.loading = true;
this._showLoading(true); this._showLoading(true);
try { try {
const resp = await fetch('/api/playlists', { const resp = await fetch('/api/playlists', {
credentials: 'include', credentials: 'include',
headers: this._headers() headers: this._headers()
}); });
if (!resp.ok) throw new Error('Failed to load playlists'); if (!resp.ok) throw new Error('Failed to load playlists');
this.playlists = await resp.json(); this.playlists = await resp.json();
this._renderPlaylists(); this._renderPlaylists();
} catch (err) { } catch (err) {
@@ -77,7 +77,7 @@ const musicView = {
_renderPlaylists() { _renderPlaylists() {
if (!this._container) return; if (!this._container) return;
const t = (key, fallback = '') => { const t = (key, fallback = '') => {
return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key; return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key;
}; };
@@ -191,7 +191,9 @@ const musicView = {
return; return;
} }
listEl.innerHTML = this.playlists.map(p => ` listEl.innerHTML = this.playlists
.map(
(p) => `
<div class="music-playlist-item" data-id="${p.id}"> <div class="music-playlist-item" data-id="${p.id}">
<div class="music-playlist-icon"> <div class="music-playlist-icon">
<i class="fas fa-music"></i> <i class="fas fa-music"></i>
@@ -201,9 +203,11 @@ const musicView = {
<span class="music-playlist-item-count">${p.track_count || 0} ${t('music.tracks', 'tracks')}</span> <span class="music-playlist-item-count">${p.track_count || 0} ${t('music.tracks', 'tracks')}</span>
</div> </div>
</div> </div>
`).join(''); `
)
.join('');
listEl.querySelectorAll('.music-playlist-item').forEach(item => { listEl.querySelectorAll('.music-playlist-item').forEach((item) => {
item.addEventListener('click', () => { item.addEventListener('click', () => {
const id = item.dataset.id; const id = item.dataset.id;
this._selectPlaylist(id); this._selectPlaylist(id);
@@ -264,11 +268,11 @@ const musicView = {
}, },
async _selectPlaylist(playlistId) { async _selectPlaylist(playlistId) {
const playlist = this.playlists.find(p => p.id === playlistId); const playlist = this.playlists.find((p) => p.id === playlistId);
if (!playlist) return; if (!playlist) return;
this.currentPlaylist = playlist; this.currentPlaylist = playlist;
const detailEl = document.getElementById('music-playlist-detail'); const detailEl = document.getElementById('music-playlist-detail');
const welcomeEl = this._container.querySelector('.music-welcome'); const welcomeEl = this._container.querySelector('.music-welcome');
const nameEl = document.getElementById('music-playlist-name'); const nameEl = document.getElementById('music-playlist-name');
@@ -301,12 +305,12 @@ const musicView = {
} }
const togglePublicBtn = document.getElementById('music-toggle-public-btn'); const togglePublicBtn = document.getElementById('music-toggle-public-btn');
if (togglePublicBtn) { if (togglePublicBtn) {
const t2 = (key, fallback = '') => typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key; const t2 = (key, fallback = '') => (typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key);
togglePublicBtn.title = playlist.is_public ? t2('music.make_private', 'Make private') : t2('music.make_public', 'Make public'); togglePublicBtn.title = playlist.is_public ? t2('music.make_private', 'Make private') : t2('music.make_public', 'Make public');
togglePublicBtn.classList.toggle('active', playlist.is_public); togglePublicBtn.classList.toggle('active', playlist.is_public);
} }
document.querySelectorAll('.music-playlist-item').forEach(item => { document.querySelectorAll('.music-playlist-item').forEach((item) => {
item.classList.toggle('active', item.dataset.id === playlistId); item.classList.toggle('active', item.dataset.id === playlistId);
}); });
@@ -363,7 +367,9 @@ const musicView = {
<span class="music-track-col music-track-duration"><i class="far fa-clock"></i></span> <span class="music-track-col music-track-duration"><i class="far fa-clock"></i></span>
<span class="music-track-col music-track-actions"></span> <span class="music-track-col music-track-actions"></span>
</div> </div>
${this.currentTracks.map((track, idx) => ` ${this.currentTracks
.map(
(track, idx) => `
<div class="music-track ${musicPlayer.currentTrack?.id === track.id ? 'playing' : ''}" data-idx="${idx}" data-id="${track.id}" data-file-id="${track.file_id}" draggable="true"> <div class="music-track ${musicPlayer.currentTrack?.id === track.id ? 'playing' : ''}" data-idx="${idx}" data-id="${track.id}" data-file-id="${track.file_id}" draggable="true">
<span class="music-track-col music-track-drag"><i class="fas fa-grip-vertical"></i></span> <span class="music-track-col music-track-drag"><i class="fas fa-grip-vertical"></i></span>
<span class="music-track-col music-track-num"> <span class="music-track-col music-track-num">
@@ -381,15 +387,17 @@ const musicView = {
<button class="music-track-remove-btn" title="${t('music.remove', 'Remove')}"><i class="fas fa-times"></i></button> <button class="music-track-remove-btn" title="${t('music.remove', 'Remove')}"><i class="fas fa-times"></i></button>
</span> </span>
</div> </div>
`).join('')} `
)
.join('')}
`; `;
const self = this; const self = this;
trackListEl.querySelectorAll('.music-track').forEach(row => { trackListEl.querySelectorAll('.music-track').forEach((row) => {
row.addEventListener('click', () => { row.addEventListener('click', () => {
const idx = parseInt(row.dataset.idx); const idx = parseInt(row.dataset.idx);
// Toggle selection // Toggle selection
trackListEl.querySelectorAll('.music-track').forEach(r => { trackListEl.querySelectorAll('.music-track').forEach((r) => {
if (r !== row) r.classList.remove('selected'); if (r !== row) r.classList.remove('selected');
}); });
row.classList.toggle('selected'); row.classList.toggle('selected');
@@ -413,7 +421,7 @@ const musicView = {
}); });
row.addEventListener('dragend', () => { row.addEventListener('dragend', () => {
row.classList.remove('dragging'); row.classList.remove('dragging');
trackListEl.querySelectorAll('.music-track').forEach(r => r.classList.remove('drag-over')); trackListEl.querySelectorAll('.music-track').forEach((r) => r.classList.remove('drag-over'));
}); });
row.addEventListener('dragover', (e) => { row.addEventListener('dragover', (e) => {
e.preventDefault(); e.preventDefault();
@@ -517,12 +525,22 @@ const musicView = {
this._renderPlaylists(); this._renderPlaylists();
this._selectPlaylist(playlist.id); this._selectPlaylist(playlist.id);
if (window.notifications) { if (window.notifications) {
window.notifications.addNotification({ icon: 'fa-check-circle', iconClass: 'upload', title: t('music.create_playlist', 'Create Playlist'), text: name }); window.notifications.addNotification({
icon: 'fa-check-circle',
iconClass: 'upload',
title: t('music.create_playlist', 'Create Playlist'),
text: name
});
} }
} catch (err) { } catch (err) {
console.error('Create playlist error:', err); console.error('Create playlist error:', err);
if (window.notifications) { if (window.notifications) {
window.notifications.addNotification({ icon: 'fa-exclamation-circle', iconClass: 'error', title: t('music.error', 'Error'), text: err.message }); window.notifications.addNotification({
icon: 'fa-exclamation-circle',
iconClass: 'error',
title: t('music.error', 'Error'),
text: err.message
});
} }
} finally { } finally {
if (createBtn) createBtn.disabled = false; if (createBtn) createBtn.disabled = false;
@@ -536,8 +554,11 @@ const musicView = {
if (!this.currentPlaylist) return; if (!this.currentPlaylist) return;
const confirmed = await new Promise(resolve => { const confirmed = await new Promise((resolve) => {
if (!window.Modal) { resolve(confirm(t('music.confirm_delete', 'Delete this playlist?'))); return; } if (!window.Modal) {
resolve(confirm(t('music.confirm_delete', 'Delete this playlist?')));
return;
}
window.Modal.prompt({ window.Modal.prompt({
title: t('music.delete', 'Delete'), title: t('music.delete', 'Delete'),
label: t('music.confirm_delete', 'Delete this playlist?'), label: t('music.confirm_delete', 'Delete this playlist?'),
@@ -545,7 +566,7 @@ const musicView = {
value: this.currentPlaylist.name, value: this.currentPlaylist.name,
icon: 'fa-trash', icon: 'fa-trash',
confirmText: t('music.delete', 'Delete') confirmText: t('music.delete', 'Delete')
}).then(val => resolve(val !== null)); }).then((val) => resolve(val !== null));
}); });
if (!confirmed) return; if (!confirmed) return;
@@ -561,7 +582,7 @@ const musicView = {
if (!resp.ok) throw new Error('Failed to delete playlist'); if (!resp.ok) throw new Error('Failed to delete playlist');
const deletedName = this.currentPlaylist.name; const deletedName = this.currentPlaylist.name;
this.playlists = this.playlists.filter(p => p.id !== this.currentPlaylist.id); this.playlists = this.playlists.filter((p) => p.id !== this.currentPlaylist.id);
this.currentPlaylist = null; this.currentPlaylist = null;
this.currentTracks = []; this.currentTracks = [];
this._renderPlaylists(); this._renderPlaylists();
@@ -571,7 +592,12 @@ const musicView = {
} catch (err) { } catch (err) {
console.error('Delete playlist error:', err); console.error('Delete playlist error:', err);
if (window.notifications) { if (window.notifications) {
window.notifications.addNotification({ icon: 'fa-exclamation-circle', iconClass: 'error', title: t('music.error', 'Error'), text: err.message }); window.notifications.addNotification({
icon: 'fa-exclamation-circle',
iconClass: 'error',
title: t('music.error', 'Error'),
text: err.message
});
} }
} finally { } finally {
if (deleteBtn) deleteBtn.disabled = false; if (deleteBtn) deleteBtn.disabled = false;
@@ -640,7 +666,7 @@ const musicView = {
if (!resp.ok) throw new Error('Failed to update playlist'); if (!resp.ok) throw new Error('Failed to update playlist');
this.currentPlaylist.name = newName.trim(); this.currentPlaylist.name = newName.trim();
const idx = this.playlists.findIndex(p => p.id === this.currentPlaylist.id); const idx = this.playlists.findIndex((p) => p.id === this.currentPlaylist.id);
if (idx !== -1) this.playlists[idx].name = newName.trim(); if (idx !== -1) this.playlists[idx].name = newName.trim();
const nameEl = document.getElementById('music-playlist-name'); const nameEl = document.getElementById('music-playlist-name');
@@ -649,7 +675,12 @@ const musicView = {
} catch (err) { } catch (err) {
console.error('Edit playlist error:', err); console.error('Edit playlist error:', err);
if (window.notifications) { if (window.notifications) {
window.notifications.addNotification({ icon: 'fa-exclamation-circle', iconClass: 'error', title: t('music.error', 'Error'), text: err.message }); window.notifications.addNotification({
icon: 'fa-exclamation-circle',
iconClass: 'error',
title: t('music.error', 'Error'),
text: err.message
});
} }
} }
}, },
@@ -681,12 +712,22 @@ const musicView = {
if (!resp.ok) throw new Error('Failed to share playlist'); if (!resp.ok) throw new Error('Failed to share playlist');
if (window.notifications) { if (window.notifications) {
window.notifications.addNotification({ icon: 'fa-check-circle', iconClass: 'upload', title: t('music.share', 'Share'), text: t('music.added', 'Added!') }); window.notifications.addNotification({
icon: 'fa-check-circle',
iconClass: 'upload',
title: t('music.share', 'Share'),
text: t('music.added', 'Added!')
});
} }
} catch (err) { } catch (err) {
console.error('Share playlist error:', err); console.error('Share playlist error:', err);
if (window.notifications) { if (window.notifications) {
window.notifications.addNotification({ icon: 'fa-exclamation-circle', iconClass: 'error', title: t('music.error', 'Error'), text: err.message }); window.notifications.addNotification({
icon: 'fa-exclamation-circle',
iconClass: 'error',
title: t('music.error', 'Error'),
text: err.message
});
} }
} }
}, },
@@ -747,12 +788,17 @@ const musicView = {
if (!resp.ok) throw new Error('Failed to add tracks'); if (!resp.ok) throw new Error('Failed to add tracks');
if (window.notifications) { if (window.notifications) {
window.notifications.addNotification({ icon: 'fa-check-circle', iconClass: 'upload', title: t('music.add_tracks', 'Add Tracks'), text: `${fileIds.length} ${t('music.added_to_playlist', 'added to playlist')}` }); window.notifications.addNotification({
icon: 'fa-check-circle',
iconClass: 'upload',
title: t('music.add_tracks', 'Add Tracks'),
text: `${fileIds.length} ${t('music.added_to_playlist', 'added to playlist')}`
});
} }
await this._loadPlaylistTracks(this.currentPlaylist.id); await this._loadPlaylistTracks(this.currentPlaylist.id);
// Update track count // Update track count
const playlist = this.playlists.find(p => p.id === this.currentPlaylist.id); const playlist = this.playlists.find((p) => p.id === this.currentPlaylist.id);
if (playlist) { if (playlist) {
playlist.track_count = (playlist.track_count || 0) + fileIds.length; playlist.track_count = (playlist.track_count || 0) + fileIds.length;
this.currentPlaylist.track_count = playlist.track_count; this.currentPlaylist.track_count = playlist.track_count;
@@ -763,7 +809,12 @@ const musicView = {
} catch (err) { } catch (err) {
console.error('Add tracks error:', err); console.error('Add tracks error:', err);
if (window.notifications) { if (window.notifications) {
window.notifications.addNotification({ icon: 'fa-exclamation-circle', iconClass: 'error', title: t('music.error', 'Error'), text: t('music.add_error', 'Could not add tracks to playlist') }); window.notifications.addNotification({
icon: 'fa-exclamation-circle',
iconClass: 'error',
title: t('music.error', 'Error'),
text: t('music.add_error', 'Could not add tracks to playlist')
});
} }
} }
}); });
@@ -773,7 +824,7 @@ const musicView = {
async _removeTrackFromPlaylist(trackId, fileId) { async _removeTrackFromPlaylist(trackId, fileId) {
if (!this.currentPlaylist) return; if (!this.currentPlaylist) return;
const t = (key, fallback = '') => typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key; const t = (key, fallback = '') => (typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key);
try { try {
const resp = await fetch(`/api/playlists/${this.currentPlaylist.id}/tracks/${encodeURIComponent(fileId)}`, { const resp = await fetch(`/api/playlists/${this.currentPlaylist.id}/tracks/${encodeURIComponent(fileId)}`, {
@@ -784,10 +835,15 @@ const musicView = {
if (!resp.ok) throw new Error('Failed to remove track'); if (!resp.ok) throw new Error('Failed to remove track');
if (window.notifications) { if (window.notifications) {
window.notifications.addNotification({ icon: 'fa-check-circle', iconClass: 'upload', title: t('music.remove', 'Remove'), text: t('music.track_removed', 'Track removed') }); window.notifications.addNotification({
icon: 'fa-check-circle',
iconClass: 'upload',
title: t('music.remove', 'Remove'),
text: t('music.track_removed', 'Track removed')
});
} }
await this._loadPlaylistTracks(this.currentPlaylist.id); await this._loadPlaylistTracks(this.currentPlaylist.id);
const playlist = this.playlists.find(p => p.id === this.currentPlaylist.id); const playlist = this.playlists.find((p) => p.id === this.currentPlaylist.id);
if (playlist) { if (playlist) {
playlist.track_count = Math.max(0, (playlist.track_count || 1) - 1); playlist.track_count = Math.max(0, (playlist.track_count || 1) - 1);
this.currentPlaylist.track_count = playlist.track_count; this.currentPlaylist.track_count = playlist.track_count;
@@ -798,14 +854,19 @@ const musicView = {
} catch (err) { } catch (err) {
console.error('Remove track error:', err); console.error('Remove track error:', err);
if (window.notifications) { if (window.notifications) {
window.notifications.addNotification({ icon: 'fa-exclamation-circle', iconClass: 'error', title: t('music.error', 'Error'), text: err.message }); window.notifications.addNotification({
icon: 'fa-exclamation-circle',
iconClass: 'error',
title: t('music.error', 'Error'),
text: err.message
});
} }
} }
}, },
async _reorderTrack(fromIdx, toIdx) { async _reorderTrack(fromIdx, toIdx) {
if (!this.currentPlaylist) return; if (!this.currentPlaylist) return;
const t = (key, fallback = '') => typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key; const t = (key, fallback = '') => (typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key);
const tracks = [...this.currentTracks]; const tracks = [...this.currentTracks];
const [moved] = tracks.splice(fromIdx, 1); const [moved] = tracks.splice(fromIdx, 1);
@@ -813,7 +874,7 @@ const musicView = {
this.currentTracks = tracks; this.currentTracks = tracks;
this._renderTracks(); this._renderTracks();
const itemIds = tracks.map(tr => tr.id); const itemIds = tracks.map((tr) => tr.id);
try { try {
const resp = await fetch(`/api/playlists/${this.currentPlaylist.id}/reorder`, { const resp = await fetch(`/api/playlists/${this.currentPlaylist.id}/reorder`, {
method: 'PUT', method: 'PUT',
@@ -825,7 +886,12 @@ const musicView = {
} catch (err) { } catch (err) {
console.error('Reorder error:', err); console.error('Reorder error:', err);
if (window.notifications) { if (window.notifications) {
window.notifications.addNotification({ icon: 'fa-exclamation-circle', iconClass: 'error', title: t('music.error', 'Error'), text: err.message }); window.notifications.addNotification({
icon: 'fa-exclamation-circle',
iconClass: 'error',
title: t('music.error', 'Error'),
text: err.message
});
} }
await this._loadPlaylistTracks(this.currentPlaylist.id); await this._loadPlaylistTracks(this.currentPlaylist.id);
} }
@@ -833,7 +899,7 @@ const musicView = {
async _showManageSharesDialog() { async _showManageSharesDialog() {
if (!this.currentPlaylist) return; if (!this.currentPlaylist) return;
const t = (key, fallback = '') => typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key; const t = (key, fallback = '') => (typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key);
const existing = document.getElementById('music-shares-dialog'); const existing = document.getElementById('music-shares-dialog');
if (existing) existing.remove(); if (existing) existing.remove();
@@ -864,7 +930,9 @@ const musicView = {
document.body.appendChild(dialog); document.body.appendChild(dialog);
dialog.querySelector('.music-shares-close-btn').addEventListener('click', () => dialog.remove()); dialog.querySelector('.music-shares-close-btn').addEventListener('click', () => dialog.remove());
dialog.addEventListener('click', (e) => { if (e.target === dialog) dialog.remove(); }); dialog.addEventListener('click', (e) => {
if (e.target === dialog) dialog.remove();
});
dialog.querySelector('#music-share-add-btn').addEventListener('click', async () => { dialog.querySelector('#music-share-add-btn').addEventListener('click', async () => {
const userInput = dialog.querySelector('#music-share-user-input'); const userInput = dialog.querySelector('#music-share-user-input');
@@ -884,11 +952,21 @@ const musicView = {
writeInput.checked = false; writeInput.checked = false;
this._loadSharesList(dialog); this._loadSharesList(dialog);
if (window.notifications) { if (window.notifications) {
window.notifications.addNotification({ icon: 'fa-check-circle', iconClass: 'upload', title: t('music.share', 'Share'), text: t('music.added', 'Added!') }); window.notifications.addNotification({
icon: 'fa-check-circle',
iconClass: 'upload',
title: t('music.share', 'Share'),
text: t('music.added', 'Added!')
});
} }
} catch (err) { } catch (err) {
if (window.notifications) { if (window.notifications) {
window.notifications.addNotification({ icon: 'fa-exclamation-circle', iconClass: 'error', title: t('music.error', 'Error'), text: err.message }); window.notifications.addNotification({
icon: 'fa-exclamation-circle',
iconClass: 'error',
title: t('music.error', 'Error'),
text: err.message
});
} }
} }
}); });
@@ -898,7 +976,7 @@ const musicView = {
async _loadSharesList(dialog) { async _loadSharesList(dialog) {
if (!this.currentPlaylist) return; if (!this.currentPlaylist) return;
const t = (key, fallback = '') => typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key; const t = (key, fallback = '') => (typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key);
const body = dialog.querySelector('.music-shares-body'); const body = dialog.querySelector('.music-shares-body');
if (!body) return; if (!body) return;
@@ -917,15 +995,19 @@ const musicView = {
return; return;
} }
body.innerHTML = shares.map(s => ` body.innerHTML = shares
.map(
(s) => `
<div class="music-share-item" data-user-id="${this._escapeHtml(s.user_id)}"> <div class="music-share-item" data-user-id="${this._escapeHtml(s.user_id)}">
<span class="music-share-user"><i class="fas fa-user"></i> ${this._escapeHtml(s.user_id)}</span> <span class="music-share-user"><i class="fas fa-user"></i> ${this._escapeHtml(s.user_id)}</span>
<span class="music-share-perm">${s.can_write ? t('music.can_write', 'Can edit') : t('music.read_only', 'Read only')}</span> <span class="music-share-perm">${s.can_write ? t('music.can_write', 'Can edit') : t('music.read_only', 'Read only')}</span>
<button class="music-share-remove-btn" title="${t('music.remove_share', 'Remove share')}"><i class="fas fa-times"></i></button> <button class="music-share-remove-btn" title="${t('music.remove_share', 'Remove share')}"><i class="fas fa-times"></i></button>
</div> </div>
`).join(''); `
)
.join('');
body.querySelectorAll('.music-share-remove-btn').forEach(btn => { body.querySelectorAll('.music-share-remove-btn').forEach((btn) => {
btn.addEventListener('click', async () => { btn.addEventListener('click', async () => {
const item = btn.closest('.music-share-item'); const item = btn.closest('.music-share-item');
const userId = item.dataset.userId; const userId = item.dataset.userId;
@@ -939,7 +1021,7 @@ const musicView = {
async _removeShare(userId, dialog) { async _removeShare(userId, dialog) {
if (!this.currentPlaylist) return; if (!this.currentPlaylist) return;
const t = (key, fallback = '') => typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key; const t = (key, fallback = '') => (typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key);
try { try {
const resp = await fetch(`/api/playlists/${this.currentPlaylist.id}/share/${encodeURIComponent(userId)}`, { const resp = await fetch(`/api/playlists/${this.currentPlaylist.id}/share/${encodeURIComponent(userId)}`, {
@@ -951,14 +1033,19 @@ const musicView = {
this._loadSharesList(dialog); this._loadSharesList(dialog);
} catch (err) { } catch (err) {
if (window.notifications) { if (window.notifications) {
window.notifications.addNotification({ icon: 'fa-exclamation-circle', iconClass: 'error', title: t('music.error', 'Error'), text: err.message }); window.notifications.addNotification({
icon: 'fa-exclamation-circle',
iconClass: 'error',
title: t('music.error', 'Error'),
text: err.message
});
} }
} }
}, },
async _togglePublic() { async _togglePublic() {
if (!this.currentPlaylist) return; if (!this.currentPlaylist) return;
const t = (key, fallback = '') => typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key; const t = (key, fallback = '') => (typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key);
const newValue = !this.currentPlaylist.is_public; const newValue = !this.currentPlaylist.is_public;
try { try {
@@ -971,7 +1058,7 @@ const musicView = {
if (!resp.ok) throw new Error('Failed to update playlist'); if (!resp.ok) throw new Error('Failed to update playlist');
this.currentPlaylist.is_public = newValue; this.currentPlaylist.is_public = newValue;
const idx = this.playlists.findIndex(p => p.id === this.currentPlaylist.id); const idx = this.playlists.findIndex((p) => p.id === this.currentPlaylist.id);
if (idx !== -1) this.playlists[idx].is_public = newValue; if (idx !== -1) this.playlists[idx].is_public = newValue;
const badge = document.getElementById('music-public-badge'); const badge = document.getElementById('music-public-badge');
@@ -985,19 +1072,29 @@ const musicView = {
if (window.notifications) { if (window.notifications) {
const status = newValue ? t('music.public', 'Public') : t('music.private', 'Private'); const status = newValue ? t('music.public', 'Public') : t('music.private', 'Private');
window.notifications.addNotification({ icon: 'fa-check-circle', iconClass: 'upload', title: t('music.toggle_public', 'Visibility'), text: status }); window.notifications.addNotification({
icon: 'fa-check-circle',
iconClass: 'upload',
title: t('music.toggle_public', 'Visibility'),
text: status
});
} }
} catch (err) { } catch (err) {
console.error('Toggle public error:', err); console.error('Toggle public error:', err);
if (window.notifications) { if (window.notifications) {
window.notifications.addNotification({ icon: 'fa-exclamation-circle', iconClass: 'error', title: t('music.error', 'Error'), text: err.message }); window.notifications.addNotification({
icon: 'fa-exclamation-circle',
iconClass: 'error',
title: t('music.error', 'Error'),
text: err.message
});
} }
} }
}, },
async _showCoverPicker() { async _showCoverPicker() {
if (!this.currentPlaylist) return; if (!this.currentPlaylist) return;
const t = (key, fallback = '') => typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key; const t = (key, fallback = '') => (typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key);
const input = document.createElement('input'); const input = document.createElement('input');
input.type = 'file'; input.type = 'file';
@@ -1035,7 +1132,7 @@ const musicView = {
if (!resp.ok) throw new Error('Failed to set cover'); if (!resp.ok) throw new Error('Failed to set cover');
this.currentPlaylist.cover_file_id = uploaded.id; this.currentPlaylist.cover_file_id = uploaded.id;
const plIdx = this.playlists.findIndex(p => p.id === this.currentPlaylist.id); const plIdx = this.playlists.findIndex((p) => p.id === this.currentPlaylist.id);
if (plIdx !== -1) this.playlists[plIdx].cover_file_id = uploaded.id; if (plIdx !== -1) this.playlists[plIdx].cover_file_id = uploaded.id;
const coverEl = document.getElementById('music-playlist-cover'); const coverEl = document.getElementById('music-playlist-cover');
@@ -1044,12 +1141,22 @@ const musicView = {
} }
if (window.notifications) { if (window.notifications) {
window.notifications.addNotification({ icon: 'fa-check-circle', iconClass: 'upload', title: t('music.set_cover', 'Set cover'), text: t('music.cover_updated', 'Cover updated') }); window.notifications.addNotification({
icon: 'fa-check-circle',
iconClass: 'upload',
title: t('music.set_cover', 'Set cover'),
text: t('music.cover_updated', 'Cover updated')
});
} }
} catch (err) { } catch (err) {
console.error('Cover upload error:', err); console.error('Cover upload error:', err);
if (window.notifications) { if (window.notifications) {
window.notifications.addNotification({ icon: 'fa-exclamation-circle', iconClass: 'error', title: t('music.error', 'Error'), text: err.message }); window.notifications.addNotification({
icon: 'fa-exclamation-circle',
iconClass: 'error',
title: t('music.error', 'Error'),
text: err.message
});
} }
} }
}); });
@@ -1077,10 +1184,10 @@ const musicPlayer = {
init() { init() {
if (this._initialized) return; if (this._initialized) return;
this.audio = new Audio(); this.audio = new Audio();
this.audio.volume = this.volume; this.audio.volume = this.volume;
this.audio.addEventListener('ended', () => this._onEnded()); this.audio.addEventListener('ended', () => this._onEnded());
this.audio.addEventListener('timeupdate', () => this._onTimeUpdate()); this.audio.addEventListener('timeupdate', () => this._onTimeUpdate());
this.audio.addEventListener('loadedmetadata', () => this._onLoadedMetadata()); this.audio.addEventListener('loadedmetadata', () => this._onLoadedMetadata());
@@ -1254,7 +1361,7 @@ const musicPlayer = {
playTrack(index) { playTrack(index) {
if (index < 0 || index >= this.queue.length) return; if (index < 0 || index >= this.queue.length) return;
this.currentIndex = index; this.currentIndex = index;
this.currentTrack = this.queue[index]; this.currentTrack = this.queue[index];
this._loadAndPlay(); this._loadAndPlay();
@@ -1265,7 +1372,7 @@ const musicPlayer = {
const fileId = this.currentTrack.file_id; const fileId = this.currentTrack.file_id;
this.audio.src = `/api/files/${fileId}`; this.audio.src = `/api/files/${fileId}`;
this.audio.play().catch(err => { this.audio.play().catch((err) => {
console.error('Playback error:', err); console.error('Playback error:', err);
}); });
this.isPlaying = true; this.isPlaying = true;
@@ -1301,7 +1408,7 @@ const musicPlayer = {
next() { next() {
if (this.queue.length === 0) return; if (this.queue.length === 0) return;
let nextIndex; let nextIndex;
if (this.shuffle) { if (this.shuffle) {
nextIndex = Math.floor(Math.random() * this.queue.length); nextIndex = Math.floor(Math.random() * this.queue.length);
@@ -1341,7 +1448,7 @@ const musicPlayer = {
const modes = ['none', 'all', 'one']; const modes = ['none', 'all', 'one'];
const currentIdx = modes.indexOf(this.repeat); const currentIdx = modes.indexOf(this.repeat);
this.repeat = modes[(currentIdx + 1) % modes.length]; this.repeat = modes[(currentIdx + 1) % modes.length];
const btn = document.getElementById('player-repeat-btn'); const btn = document.getElementById('player-repeat-btn');
if (btn) { if (btn) {
btn.classList.remove('active', 'repeat-one'); btn.classList.remove('active', 'repeat-one');
@@ -1358,7 +1465,7 @@ const musicPlayer = {
this.audio.volume = this.volume; this.audio.volume = this.volume;
this.isMuted = this.volume === 0; this.isMuted = this.volume === 0;
this._updateVolumeIcon(); this._updateVolumeIcon();
const input = document.getElementById('player-volume-input'); const input = document.getElementById('player-volume-input');
if (input) { if (input) {
input.value = this.volume * 100; input.value = this.volume * 100;
@@ -1431,15 +1538,15 @@ const musicPlayer = {
if (totalTimeEl) { if (totalTimeEl) {
totalTimeEl.textContent = this._formatTime(this.audio.duration); totalTimeEl.textContent = this._formatTime(this.audio.duration);
} }
if (this.currentTrack && this.audio.duration) { if (this.currentTrack && this.audio.duration) {
this.currentTrack.duration_secs = Math.round(this.audio.duration); this.currentTrack.duration_secs = Math.round(this.audio.duration);
const trackDurationEl = document.querySelector(`.music-track[data-idx="${this.currentIndex}"] .music-track-duration`); const trackDurationEl = document.querySelector(`.music-track[data-idx="${this.currentIndex}"] .music-track-duration`);
if (trackDurationEl) { if (trackDurationEl) {
trackDurationEl.textContent = this._formatDuration(this.audio.duration); trackDurationEl.textContent = this._formatDuration(this.audio.duration);
} }
const queueDurationEl = document.querySelector(`.queue-item[data-idx="${this.currentIndex}"] .queue-item-duration`); const queueDurationEl = document.querySelector(`.queue-item[data-idx="${this.currentIndex}"] .queue-item-duration`);
if (queueDurationEl) { if (queueDurationEl) {
queueDurationEl.textContent = this._formatDuration(this.audio.duration); queueDurationEl.textContent = this._formatDuration(this.audio.duration);
@@ -1470,7 +1577,12 @@ const musicPlayer = {
}; };
if (window.notifications) { if (window.notifications) {
const trackName = this.currentTrack?.title || this.currentTrack?.file_name || t('music.unknown_title', 'Unknown'); const trackName = this.currentTrack?.title || this.currentTrack?.file_name || t('music.unknown_title', 'Unknown');
window.notifications.addNotification({ icon: 'fa-exclamation-circle', iconClass: 'error', title: t('music.error', 'Error'), text: `${t('music.playback_error', 'Playback failed')}: ${trackName}` }); window.notifications.addNotification({
icon: 'fa-exclamation-circle',
iconClass: 'error',
title: t('music.error', 'Error'),
text: `${t('music.playback_error', 'Playback failed')}: ${trackName}`
});
} }
}, },
@@ -1496,8 +1608,8 @@ const musicPlayer = {
const t = (key, fallback = '') => { const t = (key, fallback = '') => {
return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key; return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key;
}; };
trackName.textContent = this.currentTrack trackName.textContent = this.currentTrack
? (this.currentTrack.title || this.currentTrack.file_name || t('music.unknown_title', 'Unknown')) ? this.currentTrack.title || this.currentTrack.file_name || t('music.unknown_title', 'Unknown')
: t('music.not_playing', 'Not playing'); : t('music.not_playing', 'Not playing');
} }
@@ -1506,13 +1618,13 @@ const musicPlayer = {
} }
if (musicView.currentTracks.length > 0) { if (musicView.currentTracks.length > 0) {
document.querySelectorAll('.music-track').forEach(row => { document.querySelectorAll('.music-track').forEach((row) => {
const idx = parseInt(row.dataset.idx); const idx = parseInt(row.dataset.idx);
row.classList.toggle('playing', idx === this.currentIndex && this.isPlaying); row.classList.toggle('playing', idx === this.currentIndex && this.isPlaying);
const numText = row.querySelector('.track-num-text'); const numText = row.querySelector('.track-num-text');
const playIcon = row.querySelector('.track-play-icon'); const playIcon = row.querySelector('.track-play-icon');
if (idx === this.currentIndex) { if (idx === this.currentIndex) {
if (numText) numText.classList.add('hidden'); if (numText) numText.classList.add('hidden');
if (playIcon) playIcon.classList.remove('hidden'); if (playIcon) playIcon.classList.remove('hidden');
@@ -1562,7 +1674,9 @@ const musicPlayer = {
return; return;
} }
queueList.innerHTML = this.queue.map((track, idx) => ` queueList.innerHTML = this.queue
.map(
(track, idx) => `
<div class="player-queue-item ${idx === this.currentIndex ? 'active' : ''}" data-idx="${idx}"> <div class="player-queue-item ${idx === this.currentIndex ? 'active' : ''}" data-idx="${idx}">
<span class="queue-item-num">${idx + 1}</span> <span class="queue-item-num">${idx + 1}</span>
<span class="queue-item-info"> <span class="queue-item-info">
@@ -1574,9 +1688,11 @@ const musicPlayer = {
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
</button> </button>
</div> </div>
`).join(''); `
)
.join('');
queueList.querySelectorAll('.player-queue-item').forEach(item => { queueList.querySelectorAll('.player-queue-item').forEach((item) => {
item.addEventListener('click', (e) => { item.addEventListener('click', (e) => {
if (e.target.closest('.queue-item-remove')) return; if (e.target.closest('.queue-item-remove')) return;
const idx = parseInt(item.dataset.idx); const idx = parseInt(item.dataset.idx);
@@ -1584,7 +1700,7 @@ const musicPlayer = {
}); });
}); });
queueList.querySelectorAll('.queue-item-remove').forEach(btn => { queueList.querySelectorAll('.queue-item-remove').forEach((btn) => {
btn.addEventListener('click', (e) => { btn.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();
const idx = parseInt(btn.dataset.idx); const idx = parseInt(btn.dataset.idx);