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 {
margin-left: auto;
font-size: 12px;
color: #718096;
margin-left: auto;
font-size: 12px;
color: #718096;
}
/* Custom confirm dialog */
+14 -8
View File
@@ -106,7 +106,9 @@
display: flex;
align-items: 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));
}
@@ -426,7 +428,9 @@
}
@keyframes spin {
to { transform: rotate(360deg); }
to {
transform: rotate(360deg);
}
}
/* Dark theme */
@@ -757,7 +761,7 @@
}
.player-queue::after {
content: '';
content: "";
position: absolute;
bottom: -8px;
right: 60px;
@@ -916,7 +920,7 @@
}
.player-btn.repeat-one i::after {
content: '1';
content: "1";
font-size: 8px;
position: absolute;
top: 0;
@@ -1076,7 +1080,9 @@
padding: 4px 6px;
border-radius: 4px;
opacity: 0;
transition: opacity 0.15s, color 0.15s;
transition:
opacity 0.15s,
color 0.15s;
font-size: 0.85rem;
}
.music-track:hover .music-track-remove-btn {
@@ -1105,7 +1111,7 @@
display: flex;
align-items: center;
justify-content: center;
background: rgba(0,0,0,0.4);
background: rgba(0, 0, 0, 0.4);
color: #fff;
font-size: 1.1rem;
opacity: 0;
@@ -1140,7 +1146,7 @@
.music-shares-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.45);
background: rgba(0, 0, 0, 0.45);
display: flex;
align-items: center;
justify-content: center;
@@ -1154,7 +1160,7 @@
max-height: 80vh;
display: flex;
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 {
display: flex;
+1 -1
View File
@@ -621,7 +621,7 @@ function setupEventListeners() {
switchToMusicSection();
break;
case 'nav.trash':
case 'nav.trash':
switchToTrashSection();
break;
+1 -1
View File
@@ -334,7 +334,7 @@ function switchToMusicSection() {
// Hide breadcrumb
const breadcrumb = document.querySelector('.breadcrumb');
breadcrumb?.classList.add("hidden");
breadcrumb?.classList.add('hidden');
// Hide file containers
toggleFileContainer(false);
+12 -12
View File
@@ -1082,7 +1082,7 @@ const contextMenus = {
const dialog = document.getElementById('playlist-dialog');
const container = document.getElementById('playlist-select-container');
const filesInfo = document.getElementById('playlist-dialog-files-info');
if (!dialog || !container) {
console.error('Playlist dialog elements not found');
return;
@@ -1099,7 +1099,7 @@ const contextMenus = {
// Reset selection
this._selectedPlaylistId = null;
container.innerHTML = '<div class="folder-select-loading"><i class="fas fa-spinner fa-spin"></i></div>';
// Reset add button state
const addBtn = document.getElementById('playlist-add-btn');
if (addBtn) addBtn.disabled = true;
@@ -1112,7 +1112,7 @@ const contextMenus = {
try {
const resp = await fetch('/api/playlists', { credentials: 'include' });
if (!resp.ok) throw new Error('Failed to load playlists');
const playlists = await resp.json();
this._renderPlaylistSelect(container, playlists);
} catch (err) {
@@ -1122,16 +1122,16 @@ const contextMenus = {
},
_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 = '';
if (playlists.length === 0) {
container.innerHTML = `<div class="folder-select-empty">${t('music.no_playlists', 'No playlists yet. Create one first!')}</div>`;
return;
}
playlists.forEach(playlist => {
playlists.forEach((playlist) => {
const item = document.createElement('div');
item.className = 'folder-select-item';
item.dataset.id = playlist.id;
@@ -1140,15 +1140,15 @@ const contextMenus = {
<span>${this._escapeHtml(playlist.name)}</span>
<span class="playlist-track-count">${playlist.track_count || 0} ${t('music.tracks', 'tracks')}</span>
`;
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');
this._selectedPlaylistId = playlist.id;
const addBtn = document.getElementById('playlist-add-btn');
if (addBtn) addBtn.disabled = false;
});
container.appendChild(item);
});
},
@@ -1156,7 +1156,7 @@ const contextMenus = {
async addSelectedFilesToPlaylist() {
const playlistId = this._selectedPlaylistId;
const files = window.app.playlistDialogFiles || [];
if (!playlistId || files.length === 0) return;
const addBtn = document.getElementById('playlist-add-btn');
@@ -1170,7 +1170,7 @@ const contextMenus = {
'Content-Type': 'application/json',
...getCsrfHeaders()
},
body: JSON.stringify({ file_ids: files.map(f => f.id) })
body: JSON.stringify({ file_ids: files.map((f) => f.id) })
});
if (!resp.ok) {
+191 -75
View File
@@ -55,15 +55,15 @@ const musicView = {
if (this.loading) return;
this.loading = true;
this._showLoading(true);
try {
const resp = await fetch('/api/playlists', {
credentials: 'include',
headers: this._headers()
});
if (!resp.ok) throw new Error('Failed to load playlists');
this.playlists = await resp.json();
this._renderPlaylists();
} catch (err) {
@@ -77,7 +77,7 @@ const musicView = {
_renderPlaylists() {
if (!this._container) return;
const t = (key, fallback = '') => {
return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key;
};
@@ -191,7 +191,9 @@ const musicView = {
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-icon">
<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>
</div>
</div>
`).join('');
`
)
.join('');
listEl.querySelectorAll('.music-playlist-item').forEach(item => {
listEl.querySelectorAll('.music-playlist-item').forEach((item) => {
item.addEventListener('click', () => {
const id = item.dataset.id;
this._selectPlaylist(id);
@@ -264,11 +268,11 @@ const musicView = {
},
async _selectPlaylist(playlistId) {
const playlist = this.playlists.find(p => p.id === playlistId);
const playlist = this.playlists.find((p) => p.id === playlistId);
if (!playlist) return;
this.currentPlaylist = playlist;
const detailEl = document.getElementById('music-playlist-detail');
const welcomeEl = this._container.querySelector('.music-welcome');
const nameEl = document.getElementById('music-playlist-name');
@@ -301,12 +305,12 @@ const musicView = {
}
const togglePublicBtn = document.getElementById('music-toggle-public-btn');
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.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);
});
@@ -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-actions"></span>
</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">
<span class="music-track-col music-track-drag"><i class="fas fa-grip-vertical"></i></span>
<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>
</span>
</div>
`).join('')}
`
)
.join('')}
`;
const self = this;
trackListEl.querySelectorAll('.music-track').forEach(row => {
trackListEl.querySelectorAll('.music-track').forEach((row) => {
row.addEventListener('click', () => {
const idx = parseInt(row.dataset.idx);
// Toggle selection
trackListEl.querySelectorAll('.music-track').forEach(r => {
trackListEl.querySelectorAll('.music-track').forEach((r) => {
if (r !== row) r.classList.remove('selected');
});
row.classList.toggle('selected');
@@ -413,7 +421,7 @@ const musicView = {
});
row.addEventListener('dragend', () => {
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) => {
e.preventDefault();
@@ -517,12 +525,22 @@ const musicView = {
this._renderPlaylists();
this._selectPlaylist(playlist.id);
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) {
console.error('Create playlist error:', err);
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 {
if (createBtn) createBtn.disabled = false;
@@ -536,8 +554,11 @@ const musicView = {
if (!this.currentPlaylist) return;
const confirmed = await new Promise(resolve => {
if (!window.Modal) { resolve(confirm(t('music.confirm_delete', 'Delete this playlist?'))); return; }
const confirmed = await new Promise((resolve) => {
if (!window.Modal) {
resolve(confirm(t('music.confirm_delete', 'Delete this playlist?')));
return;
}
window.Modal.prompt({
title: t('music.delete', 'Delete'),
label: t('music.confirm_delete', 'Delete this playlist?'),
@@ -545,7 +566,7 @@ const musicView = {
value: this.currentPlaylist.name,
icon: 'fa-trash',
confirmText: t('music.delete', 'Delete')
}).then(val => resolve(val !== null));
}).then((val) => resolve(val !== null));
});
if (!confirmed) return;
@@ -561,7 +582,7 @@ const musicView = {
if (!resp.ok) throw new Error('Failed to delete playlist');
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.currentTracks = [];
this._renderPlaylists();
@@ -571,7 +592,12 @@ const musicView = {
} catch (err) {
console.error('Delete playlist error:', err);
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 {
if (deleteBtn) deleteBtn.disabled = false;
@@ -640,7 +666,7 @@ const musicView = {
if (!resp.ok) throw new Error('Failed to update playlist');
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();
const nameEl = document.getElementById('music-playlist-name');
@@ -649,7 +675,12 @@ const musicView = {
} catch (err) {
console.error('Edit playlist error:', err);
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 (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) {
console.error('Share playlist error:', err);
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 (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);
// 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) {
playlist.track_count = (playlist.track_count || 0) + fileIds.length;
this.currentPlaylist.track_count = playlist.track_count;
@@ -763,7 +809,12 @@ const musicView = {
} catch (err) {
console.error('Add tracks error:', err);
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) {
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 {
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 (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);
const playlist = this.playlists.find(p => p.id === this.currentPlaylist.id);
const playlist = this.playlists.find((p) => p.id === this.currentPlaylist.id);
if (playlist) {
playlist.track_count = Math.max(0, (playlist.track_count || 1) - 1);
this.currentPlaylist.track_count = playlist.track_count;
@@ -798,14 +854,19 @@ const musicView = {
} catch (err) {
console.error('Remove track error:', err);
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) {
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 [moved] = tracks.splice(fromIdx, 1);
@@ -813,7 +874,7 @@ const musicView = {
this.currentTracks = tracks;
this._renderTracks();
const itemIds = tracks.map(tr => tr.id);
const itemIds = tracks.map((tr) => tr.id);
try {
const resp = await fetch(`/api/playlists/${this.currentPlaylist.id}/reorder`, {
method: 'PUT',
@@ -825,7 +886,12 @@ const musicView = {
} catch (err) {
console.error('Reorder error:', err);
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);
}
@@ -833,7 +899,7 @@ const musicView = {
async _showManageSharesDialog() {
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');
if (existing) existing.remove();
@@ -864,7 +930,9 @@ const musicView = {
document.body.appendChild(dialog);
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 () => {
const userInput = dialog.querySelector('#music-share-user-input');
@@ -884,11 +952,21 @@ const musicView = {
writeInput.checked = false;
this._loadSharesList(dialog);
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) {
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) {
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');
if (!body) return;
@@ -917,15 +995,19 @@ const musicView = {
return;
}
body.innerHTML = shares.map(s => `
body.innerHTML = shares
.map(
(s) => `
<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-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>
</div>
`).join('');
`
)
.join('');
body.querySelectorAll('.music-share-remove-btn').forEach(btn => {
body.querySelectorAll('.music-share-remove-btn').forEach((btn) => {
btn.addEventListener('click', async () => {
const item = btn.closest('.music-share-item');
const userId = item.dataset.userId;
@@ -939,7 +1021,7 @@ const musicView = {
async _removeShare(userId, dialog) {
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 {
const resp = await fetch(`/api/playlists/${this.currentPlaylist.id}/share/${encodeURIComponent(userId)}`, {
@@ -951,14 +1033,19 @@ const musicView = {
this._loadSharesList(dialog);
} catch (err) {
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() {
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;
try {
@@ -971,7 +1058,7 @@ const musicView = {
if (!resp.ok) throw new Error('Failed to update playlist');
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;
const badge = document.getElementById('music-public-badge');
@@ -985,19 +1072,29 @@ const musicView = {
if (window.notifications) {
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) {
console.error('Toggle public error:', err);
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() {
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');
input.type = 'file';
@@ -1035,7 +1132,7 @@ const musicView = {
if (!resp.ok) throw new Error('Failed to set cover');
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;
const coverEl = document.getElementById('music-playlist-cover');
@@ -1044,12 +1141,22 @@ const musicView = {
}
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) {
console.error('Cover upload error:', err);
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() {
if (this._initialized) return;
this.audio = new Audio();
this.audio.volume = this.volume;
this.audio.addEventListener('ended', () => this._onEnded());
this.audio.addEventListener('timeupdate', () => this._onTimeUpdate());
this.audio.addEventListener('loadedmetadata', () => this._onLoadedMetadata());
@@ -1254,7 +1361,7 @@ const musicPlayer = {
playTrack(index) {
if (index < 0 || index >= this.queue.length) return;
this.currentIndex = index;
this.currentTrack = this.queue[index];
this._loadAndPlay();
@@ -1265,7 +1372,7 @@ const musicPlayer = {
const fileId = this.currentTrack.file_id;
this.audio.src = `/api/files/${fileId}`;
this.audio.play().catch(err => {
this.audio.play().catch((err) => {
console.error('Playback error:', err);
});
this.isPlaying = true;
@@ -1301,7 +1408,7 @@ const musicPlayer = {
next() {
if (this.queue.length === 0) return;
let nextIndex;
if (this.shuffle) {
nextIndex = Math.floor(Math.random() * this.queue.length);
@@ -1341,7 +1448,7 @@ const musicPlayer = {
const modes = ['none', 'all', 'one'];
const currentIdx = modes.indexOf(this.repeat);
this.repeat = modes[(currentIdx + 1) % modes.length];
const btn = document.getElementById('player-repeat-btn');
if (btn) {
btn.classList.remove('active', 'repeat-one');
@@ -1358,7 +1465,7 @@ const musicPlayer = {
this.audio.volume = this.volume;
this.isMuted = this.volume === 0;
this._updateVolumeIcon();
const input = document.getElementById('player-volume-input');
if (input) {
input.value = this.volume * 100;
@@ -1431,15 +1538,15 @@ const musicPlayer = {
if (totalTimeEl) {
totalTimeEl.textContent = this._formatTime(this.audio.duration);
}
if (this.currentTrack && 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`);
if (trackDurationEl) {
trackDurationEl.textContent = this._formatDuration(this.audio.duration);
}
const queueDurationEl = document.querySelector(`.queue-item[data-idx="${this.currentIndex}"] .queue-item-duration`);
if (queueDurationEl) {
queueDurationEl.textContent = this._formatDuration(this.audio.duration);
@@ -1470,7 +1577,12 @@ const musicPlayer = {
};
if (window.notifications) {
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 = '') => {
return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key;
};
trackName.textContent = this.currentTrack
? (this.currentTrack.title || this.currentTrack.file_name || t('music.unknown_title', 'Unknown'))
trackName.textContent = this.currentTrack
? this.currentTrack.title || this.currentTrack.file_name || t('music.unknown_title', 'Unknown')
: t('music.not_playing', 'Not playing');
}
@@ -1506,13 +1618,13 @@ const musicPlayer = {
}
if (musicView.currentTracks.length > 0) {
document.querySelectorAll('.music-track').forEach(row => {
document.querySelectorAll('.music-track').forEach((row) => {
const idx = parseInt(row.dataset.idx);
row.classList.toggle('playing', idx === this.currentIndex && this.isPlaying);
const numText = row.querySelector('.track-num-text');
const playIcon = row.querySelector('.track-play-icon');
if (idx === this.currentIndex) {
if (numText) numText.classList.add('hidden');
if (playIcon) playIcon.classList.remove('hidden');
@@ -1562,7 +1674,9 @@ const musicPlayer = {
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}">
<span class="queue-item-num">${idx + 1}</span>
<span class="queue-item-info">
@@ -1574,9 +1688,11 @@ const musicPlayer = {
<i class="fas fa-times"></i>
</button>
</div>
`).join('');
`
)
.join('');
queueList.querySelectorAll('.player-queue-item').forEach(item => {
queueList.querySelectorAll('.player-queue-item').forEach((item) => {
item.addEventListener('click', (e) => {
if (e.target.closest('.queue-item-remove')) return;
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) => {
e.stopPropagation();
const idx = parseInt(btn.dataset.idx);