summaryrefslogtreecommitdiff
path: root/web/js/app.js
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-17 15:25:52 +0300
committerPaul Buetow <paul@buetow.org>2026-05-17 15:25:52 +0300
commit914bd7cd6aa14e839332a98d91c30b19865b0cf2 (patch)
tree02b11537237a204048589e14ed24938b805e42f7 /web/js/app.js
parent3b24f0e1be832584d6550e8cc3e5d24329f66f90 (diff)
Restructure repo: move Go server into player-server/
Diffstat (limited to 'web/js/app.js')
-rw-r--r--web/js/app.js427
1 files changed, 0 insertions, 427 deletions
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 {}
- });
-}