import { app } from '../../app/state.js'; import { getCsrfHeaders } from '../../core/csrf.js'; import { formatFileSize } from '../../core/formatters.js'; import { i18n } from '../../core/i18n.js'; import { oxiIcon } from '../../core/icons.js'; import { Modal } from '../../core/modal.js'; import { notifications } from '../../core/notifications.js'; /** @import {FileItem, Musicshare, Playlist, PlaylistItem} from '../../core/types.js' */ /** * OxiCloud - Music Library View * Playlist management with track listings and audio player */ const musicView = { /** @type {Playlist[]} */ playlists: [], /** @type {Playlist | null} */ currentPlaylist: null, /** @type {PlaylistItem[]} */ currentTracks: [], loading: false, /** @type {HTMLDivElement | null} */ _container: null, _initialized: false, selected: new Set(), _headers(json = false) { const h = { ...getCsrfHeaders() }; if (json) h['Content-Type'] = 'application/json'; return h; }, init() { if (!this._container) { const contentArea = document.querySelector('.content-area'); if (!contentArea) return; const el = document.createElement('div'); el.id = 'music-container'; el.className = 'music-container'; contentArea.appendChild(el); this._container = el; } musicPlayer.init(); this._initialized = true; }, show() { this.init(); if (!this._container) return; this._container.classList.add('active'); this.currentPlaylist = null; this.currentTracks = []; this._container.innerHTML = ''; this._renderPlaylists(); this._loadPlaylists(); }, hide() { if (this._container) { this._container.classList.remove('active'); } this.currentPlaylist = null; this.currentTracks = []; }, async _loadPlaylists() { 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 = /** @type {Playlist[]} */ (await resp.json()); this._renderPlaylists(); } catch (err) { console.error('Music load error:', err); this._showError(/** @type {Error} */ (err).message); } finally { this.loading = false; this._showLoading(false); } }, _renderPlaylists() { if (!this._container) return; // FIXME should call directly // Empty state: no playlists at all — show full-width centered onboarding if (this.playlists.length === 0) { this._container.innerHTML = `
${i18n.t('music.empty_hint')}
${i18n.t('music.select_hint')}
${i18n.t('music.no_playlists')}
${i18n.t('music.no_tracks')}
${this._escapeHtml(message)}
${i18n.t('music.no_shares')}
`; return; } body.innerHTML = shares .map( (s) => ` ` ) .join(''); body.querySelectorAll('.music-share-remove-btn').forEach((btn) => { btn.addEventListener('click', async () => { const item = /** @type {HTMLDivElement} */ (btn.closest('.music-share-item')); const userId = item.dataset.userId; await this._removeShare(userId, dialog); }); }); } catch (err) { body.innerHTML = `${this._escapeHtml(/** @type {Error} */ (err).message)}
`; } }, /** * * @param {string} userId * @param {HTMLDivElement} dialog * @returns */ async _removeShare(userId, dialog) { if (!this.currentPlaylist) return; try { const resp = await fetch(`/api/playlists/${this.currentPlaylist.id}/share/${encodeURIComponent(userId)}`, { method: 'DELETE', credentials: 'include', headers: this._headers() }); if (!resp.ok) throw new Error('Failed to remove share'); this._loadSharesList(dialog); } catch (err) { if (notifications) { notifications.addNotification({ icon: 'fa-exclamation-circle', iconClass: 'error', title: i18n.t('music.error'), text: /** @type {Error} */ (err).message }); } } }, async _togglePublic() { if (!this.currentPlaylist) return; const newValue = !this.currentPlaylist.is_public; try { const resp = await fetch(`/api/playlists/${this.currentPlaylist.id}`, { method: 'PUT', credentials: 'include', headers: this._headers(true), body: JSON.stringify({ is_public: newValue }) }); 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); if (idx !== -1) this.playlists[idx].is_public = newValue; const badge = document.getElementById('music-public-badge'); if (badge) badge.classList.toggle('hidden', !newValue); const btn = document.getElementById('music-toggle-public-btn'); if (btn) { btn.title = newValue ? i18n.t('music.make_private') : i18n.t('music.make_public'); btn.classList.toggle('active', newValue); } if (notifications) { const status = newValue ? i18n.t('music.public') : i18n.t('music.private'); notifications.addNotification({ icon: 'fa-check-circle', iconClass: 'upload', title: i18n.t('music.toggle_public'), text: status }); } } catch (err) { console.error('Toggle public error:', err); if (notifications) { notifications.addNotification({ icon: 'fa-exclamation-circle', iconClass: 'error', title: i18n.t('music.error'), text: /** @type {Error} */ (err).message }); } } }, async _showCoverPicker() { if (!this.currentPlaylist) return; const input = document.createElement('input'); input.type = 'file'; input.accept = 'image/*'; input.style.display = 'none'; document.body.appendChild(input); input.addEventListener('change', async () => { const file = input.files[0]; input.remove(); if (!file) return; try { const formData = new FormData(); formData.append('file', file); const folderId = app?.currentPath || app?.userHomeFolderId || ''; formData.append('folder_id', folderId); const uploadResp = await fetch('/api/files/upload', { method: 'POST', credentials: 'include', headers: getCsrfHeaders(), body: formData }); if (!uploadResp.ok) throw new Error('Upload failed'); const uploaded = await uploadResp.json(); if (!uploaded.id) throw new Error('No file ID returned'); const resp = await fetch(`/api/playlists/${this.currentPlaylist.id}`, { method: 'PUT', credentials: 'include', headers: this._headers(true), body: JSON.stringify({ cover_file_id: uploaded.id }) }); 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); if (plIdx !== -1) this.playlists[plIdx].cover_file_id = uploaded.id; const coverEl = document.getElementById('music-playlist-cover'); if (coverEl) { coverEl.innerHTML = `${i18n.t('music.queue_empty')}