diff options
Diffstat (limited to 'web/js')
38 files changed, 0 insertions, 5527 deletions
diff --git a/web/js/admin.js b/web/js/admin.js deleted file mode 100644 index ac43643..0000000 --- a/web/js/admin.js +++ /dev/null @@ -1,168 +0,0 @@ -import { API } from './api.js'; -import { escapeHtml, toast } from './utils.js'; -import { triggerRescan } from './views/admin-status.js'; - -export function initAdmin() { - const btn = document.getElementById('admin-toggle'); - const modal = document.getElementById('admin-modal'); - const closeBtn = document.getElementById('admin-close'); - const rescanBtn = document.getElementById('admin-rescan'); - const trashBtn = document.getElementById('admin-trash'); - const form = document.getElementById('admin-user-form'); - - btn?.addEventListener('click', () => { - modal?.classList.add('open'); - refreshAdmin(); - }); - closeBtn?.addEventListener('click', () => modal?.classList.remove('open')); - modal?.addEventListener('click', (e) => { if (e.target === modal) modal.classList.remove('open'); }); - - rescanBtn?.addEventListener('click', async () => { - rescanBtn.disabled = true; - try { await triggerRescan(); } - finally { rescanBtn.disabled = false; } - }); - - trashBtn?.addEventListener('click', async () => { - try { - const data = await API.trash(); - renderTrash(data); - } catch (err) { toast(err.message, 'error'); } - }); - - form?.addEventListener('submit', async (e) => { - e.preventDefault(); - const fd = new FormData(form); - const body = { - username: fd.get('username'), - password: fd.get('password'), - is_admin: !!fd.get('is_admin'), - }; - try { - await API.createUser(body); - form.reset(); - toast('User created'); - await refreshAdmin(); - } catch (err) { toast(err.message, 'error'); } - }); -} - -async function refreshAdmin() { - try { - const [users, perms] = await Promise.all([API.users(), API.permissions()]); - renderUsers(users); - renderPermissions(perms); - } catch (err) { toast(err.message, 'error'); } -} - -function renderUsers(users) { - const el = document.getElementById('admin-users'); - if (!el) return; - el.innerHTML = `<ul class="admin-list"> - ${users.map((u) => `<li class="${u.is_admin ? 'is-admin' : ''}"> - <span>${escapeHtml(u.username)}${u.is_admin ? ' <span class="badge">admin</span>' : ''}</span> - <button class="btn btn-danger btn-sm" data-id="${u.id}">Remove</button> - </li>`).join('')} - </ul>`; - el.querySelectorAll('button[data-id]').forEach((b) => { - b.addEventListener('click', async () => { - try { - await API.deleteUser(b.dataset.id); - toast('User removed'); - await refreshAdmin(); - } catch (err) { - const msg = err.message || ''; - if (msg.toLowerCase().includes('cannot delete yourself') || msg.toLowerCase().includes('self-delete')) { - toast('You cannot delete yourself', 'error'); - } else { - toast(err.message, 'error'); - } - } - }); - }); -} - -function renderPermissions(data) { - const el = document.getElementById('admin-permissions'); - if (!el || !data) return; - if (!data.sets?.length || !data.users?.length) { - el.innerHTML = '<p class="text-muted text-xs">No sets or users to manage.</p>'; - return; - } - - // Build a lookup: setId -> userId -> role - const roleMap = {}; - data.permissions?.forEach((p) => { - if (!roleMap[p.set_id]) roleMap[p.set_id] = {}; - roleMap[p.set_id][p.user_id] = p.role; - }); - - let html = '<table class="admin-table"><thead><tr><th>Set</th>'; - data.users.forEach((u) => { - html += `<th>${escapeHtml(u.username)}</th>`; - }); - html += '</tr></thead><tbody>'; - - data.sets.forEach((s) => { - html += `<tr><td>${escapeHtml(s.name)}</td>`; - data.users.forEach((u) => { - const role = roleMap[s.id]?.[u.id] || ''; - const selectId = `perm-${s.id}-${u.id}`; - html += `<td> - <select id="${selectId}" class="perm-select" data-set="${s.id}" data-user="${u.id}"> - <option value="" ${!role ? 'selected' : ''}>—</option> - <option value="viewer" ${role === 'viewer' ? 'selected' : ''}>Viewer</option> - <option value="owner" ${role === 'owner' ? 'selected' : ''}>Owner</option> - </select> - </td>`; - }); - html += '</tr>'; - }); - - html += '</tbody></table>'; - el.innerHTML = html; - - el.querySelectorAll('.perm-select').forEach((sel) => { - sel.addEventListener('change', async () => { - const setId = sel.dataset.set; - const userId = sel.dataset.user; - const role = sel.value; - try { - if (role) { - await API.setPermissions({ set_id: parseInt(setId), user_id: parseInt(userId), role }); - toast('Permission granted'); - } else { - await API.delPermissions({ set_id: parseInt(setId), user_id: parseInt(userId) }); - toast('Permission revoked'); - } - } catch (err) { toast(err.message, 'error'); } - }); - }); -} - -function renderTrash(data) { - const el = document.getElementById('admin-users'); - if (!el || !Array.isArray(data)) return; - if (!data.length) { - el.innerHTML = '<h4 class="text-90">Trash</h4><p class="text-muted text-xs">No deleted items.</p>'; - return; - } - el.innerHTML = `<h4 class="text-90">Trash</h4><ul class="admin-list"> - ${data.map((m) => `<li> - <span>${escapeHtml(m.file_name)}</span> - <span class="text-muted text-75">${escapeHtml(m.deleted_at || '')}</span> - <button class="btn btn-primary btn-sm" data-id="${m.id}">Restore</button> - </li>`).join('')} - </ul>`; - - el.querySelectorAll('button[data-id]').forEach((b) => { - b.addEventListener('click', async () => { - try { - await API.restore(b.dataset.id); - toast('Item restored'); - const data = await API.trash(); - renderTrash(data); - } catch (err) { toast(err.message, 'error'); } - }); - }); -} diff --git a/web/js/api.js b/web/js/api.js deleted file mode 100644 index 5decd89..0000000 --- a/web/js/api.js +++ /dev/null @@ -1,117 +0,0 @@ -const API_BASE = ''; - -async function api(path, options = {}) { - const url = `${API_BASE}${path}`; - const isForm = options.body instanceof FormData; - const headers = { Accept: 'application/json', ...(options.headers || {}) }; - if (!isForm && options.body && !headers['Content-Type']) { - headers['Content-Type'] = 'application/json'; - } - - const res = await fetch(url, { - credentials: 'include', - headers, - method: options.method || 'GET', - body: isForm ? options.body : (options.body ? JSON.stringify(options.body) : undefined), - }); - - if (res.status === 204) return null; - if (res.status === 401) { - let msg = 'Unauthorized'; - try { - const j = await res.json(); - if (j.error) msg = j.error; - } catch {} - const p = location.pathname; - if (!p.includes('login') && !p.includes('bootstrap')) { - location.href = '/login.html'; - } - throw new Error(msg); - } - if (!res.ok) { - let msg = res.statusText; - try { - const j = await res.json(); - if (j.error) msg = j.error; - } catch {} - throw new Error(msg || `HTTP ${res.status}`); - } - const ct = res.headers.get('content-type') || ''; - return ct.includes('application/json') ? res.json() : res.text(); -} - -export const API = { - login: (username, password) => api('/api/login', { method: 'POST', body: { username, password } }), - logout: () => api('/api/logout', { method: 'POST' }), - bootstrap: (username, password) => api('/api/bootstrap', { method: 'POST', body: { username, password } }), - config: () => api('/api/config'), - sets: () => api('/api/sets'), - media: (qs) => { - const params = new URLSearchParams(); - Object.entries(qs).forEach(([k, v]) => { - if (v !== '' && v !== null && v !== undefined && v !== false) params.set(k, v); - }); - const s = params.toString(); - return api(`/api/media${s ? '?' + s : ''}`); - }, - mediaDetail: (id) => api(`/api/media/${id}`), - progress: (mediaId, positionSeconds) => api('/api/progress', { method: 'POST', body: { media_id: mediaId, position_seconds: positionSeconds } }), - progressStatus: (mediaId, status) => api('/api/progress/status', { method: 'POST', body: { media_id: mediaId, status } }), - inProgress: () => api('/api/in-progress'), - favorite: (id) => api(`/api/media/${id}/favorite`, { method: 'POST' }), - notes: (id) => api(`/api/media/${id}/notes`), - saveNote: (id, content) => api(`/api/media/${id}/notes`, { method: 'POST', body: { content } }), - deleteNote: (id) => api(`/api/media/${id}/notes`, { method: 'DELETE' }), - share: (id) => api(`/api/media/${id}/shares`, { method: 'POST' }), - myShares: () => api('/api/shares'), - revokeShare: (token) => api(`/api/shares/${token}`, { method: 'DELETE' }), - tags: () => api('/api/tags'), - download: (id) => api(`/api/media/${id}/download`), - regenThumbnail: (id) => api(`/api/media/${id}/thumbnail`, { method: 'POST' }), - browse: (setId, parent) => { - const params = new URLSearchParams(); - if (parent) params.set('parent', parent); - const s = params.toString(); - return api(`/api/sets/${setId}/browse${s ? '?' + s : ''}`); - }, - regenCover: (setId, folder = '') => { - const params = new URLSearchParams(); - if (folder) params.set('folder', folder); - const s = params.toString(); - return api(`/api/sets/${setId}/cover${s ? '?' + s : ''}`, { method: 'POST' }); - }, - upload: (setId, formData) => api(`/api/sets/${setId}/upload`, { method: 'POST', body: formData }), - addTag: (id, tag) => api(`/api/media/${id}/tags`, { method: 'POST', body: { tag } }), - removeTag: (id, tag) => api(`/api/media/${id}/tags/${encodeURIComponent(tag)}`, { method: 'DELETE' }), - restore: (id) => api(`/api/media/${id}/restore`, { method: 'POST' }), - trash: () => api('/api/admin/trash'), - users: () => api('/api/admin/users'), - scanProgress: () => api('/api/admin/scan-progress'), - createUser: (body) => api('/api/admin/users', { method: 'POST', body }), - deleteUser: (id) => api(`/api/admin/users/${id}`, { method: 'DELETE' }), - permissions: () => api('/api/admin/permissions'), - setPermissions: (body) => api('/api/admin/permissions', { method: 'POST', body }), - delPermissions: (body) => api('/api/admin/permissions', { method: 'DELETE', body }), - rescan: () => api('/api/admin/rescan', { method: 'POST' }), - podcasts: () => api('/api/podcasts'), - podcastEpisodes: (setId, pagination, legacyOffset) => { - let limit = 50; - let offset = 0; - if (typeof pagination === 'number') { - if (typeof legacyOffset === 'number') { - limit = pagination; - offset = legacyOffset; - } else { - offset = pagination; - } - } else if (pagination) { - limit = pagination.limit ?? limit; - offset = pagination.offset ?? offset; - } - const params = new URLSearchParams({ limit, offset }); - return api(`/api/podcasts/${setId}/episodes?${params}`); - }, - subscribePodcast: (feedUrl, setName) => api('/api/podcasts', { method: 'POST', body: { feed_url: feedUrl, set_name: setName } }), - downloadEpisode: (episodeId) => api(`/api/podcasts/episodes/${episodeId}/download`, { method: 'POST' }), - toggleEpisodeComplete: (episodeId) => api(`/api/podcasts/episodes/${episodeId}/complete`, { method: 'POST' }), -}; diff --git a/web/js/app.js b/web/js/app.js deleted file mode 100644 index d7665fa..0000000 --- a/web/js/app.js +++ /dev/null @@ -1,427 +0,0 @@ -import { API } from './api.js'; -import { initKeyboard } from './keyboard.js'; -import { initSelection, selectByElement, currentElement, navUp, navDown, navLeft, navRight } from './selection.js'; -import { - initPlayer, - togglePlay, - toggleFullscreen, - toggleMinimize, - toggleDetach, - exitFullscreenIfNeeded, - toggleCrop, - shiftCropPosition, - cycleCropPosition, - currentMediaInfo, - hasLoadedMedia, - seekPercent, - stopAndClose, - zoomIn as playerZoomIn, - zoomOut as playerZoomOut, - isImageMode as playerIsImageMode, -} from './player.js'; -import { initSearch, parseQuery, showSearchHelp } from './search.js'; -import { initShuffle, enable as enableShuffle, isOn as isShuffle, revision as shuffleRevision } from './shuffle.js'; -import { initThemes } from './themes.js'; -import { initNotes } from './notes.js'; -import { initAdmin } from './admin.js'; -import { initPodcasts } from './podcasts.js'; -import { state } from './state.js'; -import { initPWA } from './pwa.js'; -import { closeAllModals } from './dom.js'; -import { toast } from './utils.js'; -import { showAdmin, triggerRescan } from './views/admin-status.js'; -import { initHelp, showSearch, toggleHelp, toggleSidebar } from './views/help.js'; -import { - initMediaGrid, - loadMedia, - enterFolder, - navigateBack, - setMediaPageSize, -} from './views/media-grid.js'; -import { - downloadSelected, - markAsFinished, - markAsNotStarted, - openNotesForSelected, - regenThumb, - selectedMediaId, - shareSelected, - toggleFavorite, -} from './views/media-actions.js'; -import { - closeMediaInfo, - initMediaInfo, - isMediaInfoOpen, - scrollMediaInfo, - toggleMediaInfo, -} from './views/media-info.js'; -import { - navigatePlayable, - initPlaybackNav, - playRandom, - playSelected, - seekByKeyboard, -} from './views/playback-nav.js'; -import { - initSets, - renderSets, - selectSetByHotkey, - setSetByDelta, - toggleSetSelection, - updateSetRowsUI, -} from './views/sets.js'; -import { - copySelectedShare, - deleteSelectedShare, - initShares, - isSharesOpen, - sharesNav, - toggleShares, -} from './views/shares.js'; -import { initTags, openTagsForElement, openTagsForSelected } from './views/tags.js'; -import { initUpload, showUpload } from './views/upload.js'; - -const pageMap = { '/index.html': 'spa', '/login.html': 'login', '/bootstrap.html': 'bootstrap' }; - -main(); - -function main() { - const page = pageMap[location.pathname] || 'spa'; - if (page === 'login') initLogin(); - else if (page === 'bootstrap') initBootstrap(); - else initApp(); - initThemes(); -} - -function initLogin() { - const form = document.getElementById('login-form'); - const err = document.getElementById('login-error'); - if (!form) return; - form.addEventListener('submit', async (e) => { - e.preventDefault(); - err.textContent = ''; - const fd = new FormData(form); - const username = fd.get('username'); - const password = fd.get('password'); - if (!username || !password) { - err.textContent = 'Please fill in all fields.'; - return; - } - try { - await API.login(username, password); - location.href = '/'; - } catch (ex) { - err.textContent = ex.message || 'Login failed'; - } - }); -} - -function initBootstrap() { - const form = document.getElementById('bootstrap-form'); - const err = document.getElementById('bootstrap-error'); - if (!form) return; - form.addEventListener('submit', async (e) => { - e.preventDefault(); - err.textContent = ''; - const fd = new FormData(form); - const username = fd.get('username'); - const password = fd.get('password'); - const confirm = fd.get('password-confirm'); - if (!username || !password || !confirm) { - err.textContent = 'All fields are required.'; - return; - } - if (password !== confirm) { - err.textContent = 'Passwords do not match.'; - return; - } - try { - await API.bootstrap(username, password); - location.href = '/login.html'; - } catch (ex) { - err.textContent = ex.message || 'Bootstrap failed'; - } - }); -} - -async function initApp() { - initPlayer({ - onNext: (options) => navigatePlayable(options?.delta ?? 1, options), - onPrevious: (options) => navigatePlayable(options?.delta ?? -1, options), - }); - initSelection(); - initSearch({ - onChange: (q) => { - const parsed = parseQuery(q); - state.virtualSet = ''; - syncChromeVirtualSets(); - applySearchSet(parsed); - delete parsed.set; - Object.assign(state.filters, parsed); - state.folderPath = ''; - document.dispatchEvent(new CustomEvent('filters:changed')); - console.log('[search] raw:', q, 'parsed:', parsed, 'filters:', state.filters); - loadMedia(); - }, - input: document.getElementById('search-input'), - clearBtn: document.getElementById('search-clear'), - }); - document.addEventListener('search:navigate-results', () => navDown()); - initShuffle({ onChange: () => loadMedia() }); - initPlaybackNav({ isShuffle }); - initSets({ - onLoadMedia: () => { - state.virtualSet = ''; - syncChromeVirtualSets(); - loadMedia(); - }, - }); - initMediaGrid({ - isShuffle, - shuffleRevision, - onSetCleared: updateSetRowsUI, - openNotesForSelected, - openTagsForElement, - openSet, - playSelected, - regenThumb, - markAsFinished: markAsFinishedAndRefresh, - markAsNotStarted: markAsNotStartedAndRefresh, - toggleFavorite, - onVirtualSetCleared: syncChromeVirtualSets, - }); - initKeyboard(keyboardHandlers()); - initNotes(() => toast('Note saved')); - initAdmin(); - initPodcasts(); - initPWA(); - initUpload({ onLoadMedia: loadMedia }); - initHelp(); - initShares(); - initMediaInfo({ - markAsFinished: markAsFinishedAndRefresh, - markAsNotStarted: markAsNotStartedAndRefresh, - }); - initTags({ - onFilterChange: () => { - document.dispatchEvent(new CustomEvent('filters:changed')); - loadMedia(); - }, - }); - initChrome(); - - try { - const [cfg, sets] = await Promise.all([ - API.config().catch(() => null), - API.sets(), - ]); - setMediaPageSize(cfg?.media_page_size); - state.sets = (sets || []).slice().sort((a, b) => a.name.localeCompare(b.name)); - API.users().then(() => { - state.isAdmin = true; - showAdmin(); - }).catch(() => {}); - renderSets(); - await loadMedia(); - } catch (err) { - toast(err.message || 'Error loading sets', 'error'); - } -} - -function applySearchSet(parsed) { - const setQuery = parsed.set?.trim(); - if (!setQuery) return; - const needle = setQuery.toLowerCase(); - const match = state.sets.find((set) => String(set.id) === setQuery) || - state.sets.find((set) => set.name.toLowerCase() === needle) || - state.sets.find((set) => set.name.toLowerCase().includes(needle)); - if (!match) return; - state.virtualSet = ''; - state.selectedSetId = match.id; - state.selectedSetIds = [match.id]; - updateSetRowsUI(); -} - -function keyboardHandlers() { - return { - navUp: () => navUp(), - navDown: () => navDown(), - navLeft: () => navLeft(), - navRight: () => navRight(), - seekBackward: (e) => seekByKeyboard(-1, e.repeat), - seekForward: (e) => seekByKeyboard(1, e.repeat), - seekPercent: (e, percent) => { - if (!hasLoadedMedia()) return false; - if (!document.fullscreenElement) return false; - return seekPercent(percent); - }, - nextSet: () => setSetByDelta(1), - prevSet: () => setSetByDelta(-1), - selectSetByHotkey: (key) => selectSetByHotkey(key), - isSidebarOpen: () => document.getElementById('sidebar')?.classList.contains('open'), - isSidebarFocused: () => { - const sidebar = document.getElementById('sidebar'); - return sidebar?.contains(document.activeElement); - }, - toggleSetSelect: () => toggleSetSelection(), - enter: () => { - activateGridElement(focusedGridElement() || currentElement() || firstGridElement()); - }, - playPause: () => togglePlay(), - nextTrack: () => navigatePlayable(1, { forcePlay: true }), - prevTrack: () => navigatePlayable(-1, { forcePlay: true }), - playRandom: () => playRandom(), - rescanMedia: () => { - if (state.isAdmin) triggerRescan(); - }, - mediaInfo: () => toggleMediaInfo(), - fullscreen: () => toggleFullscreen(), - toggleMinimize: () => toggleMinimize(), - toggleCrop: () => toggleCrop(), - shiftCropPosition: (dx, dy) => shiftCropPosition(dx, dy), - cycleCropPosition: () => cycleCropPosition(), - escape: () => { - exitFullscreenIfNeeded(); - const el = currentElement(); - if (el) el.classList.remove('selected'); - closeAllModals(); - }, - shuffle: () => { - enableShuffle(); - loadMedia(); - }, - share: () => shareSelected(), - search: () => showSearch(), - notes: openNotesForSelected, - tags: openTagsForSelected, - toggleDetach: () => toggleDetach(), - stopAndClose: () => stopAndClose(), - download: downloadSelected, - help: toggleHelp, - backspace: () => { navigateBack(); }, - isMediaInfoOpen, - closeMediaInfo, - mediaInfoScroll: scrollMediaInfo, - sidebar: toggleSidebar, - upload: () => showUpload(), - sharesToggle: toggleShares, - searchHelp: () => showSearchHelp(), - regenThumbnail: () => { - const id = selectedMediaId(); - if (id) regenThumb(id); - }, - isSharesOpen, - sharesNavUp: () => sharesNav(-1), - sharesNavDown: () => sharesNav(1), - sharesCopy: copySelectedShare, - sharesDelete: deleteSelectedShare, - isImageMode: () => currentMediaInfo()?.type === 'image', - imageFullscreenNavigate: (delta) => { - if (!document.fullscreenElement || !playerIsImageMode()) return false; - navigatePlayable(delta, { forcePlay: true }); - return true; - }, - zoomIn: () => { - if (playerIsImageMode()) playerZoomIn(); - }, - zoomOut: () => { - if (playerIsImageMode()) playerZoomOut(); - }, - }; -} - -function focusedGridElement() { - const active = document.activeElement; - if (!active || typeof active.closest !== 'function') return null; - return active.closest('#media-grid .media-card, #media-grid .media-row, #media-grid .folder-card, #media-grid .set-card'); -} - -function firstGridElement() { - return document.querySelector('#media-grid .media-card, #media-grid .media-row, #media-grid .folder-card, #media-grid .set-card'); -} - -function activateGridElement(el) { - if (!el) return; - selectByElement(el); - if (el.classList.contains('set-card')) { - const id = parseInt(el.dataset.setId, 10); - openSet(id); - return; - } - if (el.classList.contains('folder-card')) { - enterFolder(el.dataset.name); - return; - } - const idx = parseInt(el.dataset.index, 10); - const media = state.media[idx]; - if (media) playSelected(); -} - -function openSet(id) { - if (!Number.isFinite(id)) return; - state.virtualSet = ''; - state.selectedSetId = id; - state.selectedSetIds = [id]; - state.folderPath = ''; - state.mediaPage = 0; - updateSetRowsUI(); - loadMedia(); -} - -async function markAsFinishedAndRefresh(id) { - const updated = await markAsFinished(id); - if (updated && state.virtualSet === 'in-progress') await loadMedia(); - return updated; -} - -async function markAsNotStartedAndRefresh(id) { - const updated = await markAsNotStarted(id); - if (updated && state.virtualSet === 'in-progress') await loadMedia(); - return updated; -} - -function openInProgress() { - state.virtualSet = state.virtualSet === 'in-progress' ? '' : 'in-progress'; - state.selectedSetId = null; - state.selectedSetIds = []; - state.folderPath = ''; - state.mediaPage = 0; - updateSetRowsUI(); - syncChromeVirtualSets(); - loadMedia(); -} - -function syncChromeVirtualSets() { - document.getElementById('in-progress-toggle')?.classList.toggle('active', state.virtualSet === 'in-progress'); -} - -function initChrome() { - const shuffleBtn = document.getElementById('shuffle-toggle'); - if (shuffleBtn && !document.getElementById('in-progress-toggle')) { - const btn = document.createElement('button'); - btn.id = 'in-progress-toggle'; - btn.className = 'icon-btn'; - btn.type = 'button'; - btn.title = 'In Progress'; - btn.setAttribute('aria-label', 'Show in-progress media'); - btn.textContent = '◷'; - shuffleBtn.insertAdjacentElement('afterend', btn); - } - document.getElementById('in-progress-toggle')?.addEventListener('click', openInProgress); - - document.getElementById('sidebar-toggle')?.addEventListener('click', () => { - toggleSidebar(); - }); - - document.getElementById('menu-close')?.addEventListener('click', () => { - document.getElementById('sidebar')?.classList.remove('open'); - document.querySelector('.page')?.classList.remove('has-sidebar'); - }); - - document.getElementById('logout-btn')?.addEventListener('click', async () => { - try { - await API.logout(); - location.href = '/login.html'; - } catch {} - }); -} diff --git a/web/js/detach.js b/web/js/detach.js deleted file mode 100644 index c216f3f..0000000 --- a/web/js/detach.js +++ /dev/null @@ -1,152 +0,0 @@ -let deps = {}; -let detachWindow = null; -let detachReady = false; -let pendingDetachMessage = null; -let detachedPlaybackState = null; - -export function initDetach(options = {}) { - deps = options; - window.addEventListener('message', (ev) => { - if (ev.origin !== window.location.origin) return; - if (!ev.data || typeof ev.data !== 'object') return; - if (ev.data.type === 'detach-ready') { - onDetachReady(ev.source); - } else if (ev.data.type === 'detach-closing') { - onDetachClosing(ev.data.state); - } else if (ev.data.type === 'detach-state') { - detachedPlaybackState = mergeDetachedState(detachedPlaybackState, ev.data.state); - } else if (ev.data.type === 'detach-prev') { - deps.triggerPrevious?.({ forcePlay: ev.data.play ?? true }); - } else if (ev.data.type === 'detach-next') { - deps.triggerNext?.({ forcePlay: ev.data.play ?? true }); - } else if (ev.data.type === 'detach-nav') { - deps.triggerNavigate?.(Number(ev.data.delta || 0), { forcePlay: ev.data.play ?? true }); - } - }); -} - -export function isDetached() { - return !!detachWindow && !detachWindow.closed; -} - -export function detachedIsPlaying() { - return !!detachedPlaybackState?.playing; -} - -export function toggleDetach() { - if (isDetached()) { - reattachDetached(); - return; - } - const e = deps.els?.() || {}; - if (!e.player) return; - - const snapshot = deps.localPlaybackState?.() || {}; - const features = 'width=640,height=480,resizable=yes,scrollbars=no,status=no,location=no,menubar=no,toolbar=no'; - detachWindow = window.open('/detach.html', 'playerDetach', features); - if (!detachWindow) return; - - detachReady = false; - detachedPlaybackState = snapshot; - const media = deps.getCurrentMedia?.(); - if (media) { - pendingDetachMessage = detachedLoadMessage(media, snapshot.currentTime, snapshot.playing); - } - deps.currentMediaElement?.()?.pause(); - e.player.classList.add('hidden'); - showDetachedPlaceholder(true); -} - -export function sendDetachedLoad(media, resumeFrom = 0, play = true) { |
