diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-17 15:25:52 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-17 15:25:52 +0300 |
| commit | 914bd7cd6aa14e839332a98d91c30b19865b0cf2 (patch) | |
| tree | 02b11537237a204048589e14ed24938b805e42f7 /player-server/web/js/app.js | |
| parent | 3b24f0e1be832584d6550e8cc3e5d24329f66f90 (diff) | |
Restructure repo: move Go server into player-server/
Diffstat (limited to 'player-server/web/js/app.js')
| -rw-r--r-- | player-server/web/js/app.js | 427 |
1 files changed, 427 insertions, 0 deletions
diff --git a/player-server/web/js/app.js b/player-server/web/js/app.js new file mode 100644 index 0000000..d7665fa --- /dev/null +++ b/player-server/web/js/app.js @@ -0,0 +1,427 @@ +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 {} + }); +} |
