summaryrefslogtreecommitdiff
path: root/web/js/views/playback-nav.js
diff options
context:
space:
mode:
Diffstat (limited to 'web/js/views/playback-nav.js')
-rw-r--r--web/js/views/playback-nav.js251
1 files changed, 0 insertions, 251 deletions
diff --git a/web/js/views/playback-nav.js b/web/js/views/playback-nav.js
deleted file mode 100644
index eb581db..0000000
--- a/web/js/views/playback-nav.js
+++ /dev/null
@@ -1,251 +0,0 @@
-import { API } from '../api.js';
-import { state } from '../state.js';
-import { currentElement, selectByElement } from '../selection.js';
-import {
- currentMediaId,
- currentMediaInfo,
- hasLoadedMedia,
- isPlaybackActive,
- seekRelative,
- selectAndPlay,
-} from '../player.js';
-import {
- hasActiveFilters,
- loadMedia,
- mediaWithBrowsePath,
-} from './media-grid.js';
-
-let isShuffleCallback = () => false;
-
-export function initPlaybackNav({ isShuffle } = {}) {
- isShuffleCallback = isShuffle || (() => false);
-}
-
-export async function playSelected() {
- let el = currentElement();
- if (!el) {
- el = document.querySelector('#media-grid .media-card[data-id], #media-grid .media-row[data-id]');
- if (el) selectByElement(el);
- }
- if (!el) return;
- const idx = parseInt(el.dataset.index, 10);
- const media = state.media[idx];
- if (!media) return;
- try {
- const detail = await API.mediaDetail(media.id);
- const resumeFrom = detail?.progress?.position_seconds ?? 0;
- selectAndPlay(media, idx, resumeFrom);
- } catch {
- selectAndPlay(media, idx, 0);
- }
-}
-
-export async function playRandom() {
- if (!state.media.length) return;
- const idx = Math.floor(Math.random() * state.media.length);
- const media = state.media[idx];
- if (!media) return;
- try {
- const detail = await API.mediaDetail(media.id);
- const resumeFrom = detail?.progress?.position_seconds ?? 0;
- selectAndPlay(media, idx, resumeFrom);
- } catch {
- selectAndPlay(media, idx, 0);
- }
-}
-
-function visiblePlayableCards() {
- return Array.from(document.querySelectorAll('#media-grid .media-card[data-id], #media-grid .media-row[data-id]'));
-}
-
-function selectedPlayableIndex(cards, preferCurrent) {
- const currentID = String(currentMediaId() || '');
- if (preferCurrent && currentID) {
- const idx = cards.findIndex((el) => el.dataset.id === currentID);
- if (idx >= 0) return idx;
- }
- const selected = currentElement();
- const idx = selected ? cards.indexOf(selected) : -1;
- return idx >= 0 ? idx : -1;
-}
-
-export async function navigatePlayable(delta, options = {}) {
- const cards = visiblePlayableCards();
- const active = options.forcePlay || isPlaybackActive();
- const base = selectedPlayableIndex(cards, active);
-
- if (cards.length && !(active && base < 0)) {
- const atForwardEdge = delta > 0 && base === cards.length - 1;
- const atBackwardEdge = delta < 0 && base === 0;
- if (!((atForwardEdge || atBackwardEdge) && canTraverseBrowseFolders())) {
- const nextIdx = base >= 0
- ? (base + delta + cards.length) % cards.length
- : (delta > 0 ? 0 : cards.length - 1);
- const target = cards[nextIdx];
- selectByElement(target);
- if (active) await playSelected();
- return;
- }
- }
-
- const crossFolderTarget = await findCrossFolderPlayable(delta, active);
- if (!crossFolderTarget) return;
- await openPlayableTarget(crossFolderTarget, active);
-}
-
-function canTraverseBrowseFolders() {
- return !!state.selectedSetId && state.selectedSetIds.length === 1 && !isShuffleCallback() && !hasActiveFilters();
-}
-
-async function findCrossFolderPlayable(delta, preferCurrent) {
- if (!canTraverseBrowseFolders()) return null;
- const current = currentPlaybackCandidate(preferCurrent);
- if (!current) return null;
- const currentPath = current.browse_path ?? state.folderPath ?? '';
- const currentData = await browsePath(currentPath);
- const media = mediaWithBrowsePath(currentData.media || [], currentPath);
- const idx = media.findIndex((m) => String(m.id) === String(current.id));
-
- if (idx >= 0) {
- const nextInFolder = idx + delta;
- if (nextInFolder >= 0 && nextInFolder < media.length) {
- return { path: currentPath, media: media[nextInFolder] };
- }
- }
-
- return delta > 0
- ? findAfterFolder(currentPath)
- : findBeforeFolder(currentPath);
-}
-
-function currentPlaybackCandidate(preferCurrent) {
- const playing = currentMediaInfo();
- if (preferCurrent && playing) return playing;
- const selected = currentElement();
- if (selected?.dataset?.id) {
- const selectedMedia = state.media.find((m) => String(m.id) === selected.dataset.id);
- if (selectedMedia) return selectedMedia;
- }
- return playing;
-}
-
-async function findAfterFolder(path) {
- if (!path) return firstPlayableInFolder('');
- let childPath = normalizePath(path);
- while (childPath) {
- const parent = parentPath(childPath);
- const name = baseName(childPath);
- const data = await browsePath(parent);
- const items = browseItems(data, parent);
- const idx = items.findIndex((item) => item.type === 'folder' && item.name === name);
- for (const item of items.slice(idx + 1)) {
- const target = item.type === 'media'
- ? { path: parent, media: item.media }
- : await firstPlayableInFolder(joinPath(parent, item.name));
- if (target) return target;
- }
- childPath = parent;
- }
- return firstPlayableInFolder('');
-}
-
-async function findBeforeFolder(path) {
- if (!path) return lastPlayableInFolder('');
- let childPath = normalizePath(path);
- while (childPath) {
- const parent = parentPath(childPath);
- const name = baseName(childPath);
- const data = await browsePath(parent);
- const items = browseItems(data, parent);
- const idx = items.findIndex((item) => item.type === 'folder' && item.name === name);
- for (const item of items.slice(0, Math.max(0, idx)).reverse()) {
- const target = item.type === 'media'
- ? { path: parent, media: item.media }
- : await lastPlayableInFolder(joinPath(parent, item.name));
- if (target) return target;
- }
- childPath = parent;
- }
- return lastPlayableInFolder('');
-}
-
-async function firstPlayableInFolder(path) {
- const data = await browsePath(path);
- for (const folder of data.folders || []) {
- const target = await firstPlayableInFolder(joinPath(path, folder.name));
- if (target) return target;
- }
- const media = mediaWithBrowsePath(data.media || [], data.current_path || path);
- if (media.length) return { path: data.current_path || path, media: media[0] };
- return null;
-}
-
-async function lastPlayableInFolder(path) {
- const data = await browsePath(path);
- const media = mediaWithBrowsePath(data.media || [], data.current_path || path);
- if (media.length) return { path: data.current_path || path, media: media[media.length - 1] };
- const folders = data.folders || [];
- for (const folder of folders.slice().reverse()) {
- const target = await lastPlayableInFolder(joinPath(path, folder.name));
- if (target) return target;
- }
- return null;
-}
-
-function browseItems(data, path) {
- const folders = (data.folders || []).map((folder) => ({ type: 'folder', name: folder.name }));
- const media = mediaWithBrowsePath(data.media || [], data.current_path || path)
- .map((m) => ({ type: 'media', media: m }));
- return folders.concat(media);
-}
-
-async function browsePath(path) {
- return API.browse(state.selectedSetId, path || '');
-}
-
-function normalizePath(path) {
- return (path || '').split('/').filter(Boolean).join('/');
-}
-
-function parentPath(path) {
- const normalized = normalizePath(path);
- const idx = normalized.lastIndexOf('/');
- return idx >= 0 ? normalized.slice(0, idx) : '';
-}
-
-function baseName(path) {
- const normalized = normalizePath(path);
- const idx = normalized.lastIndexOf('/');
- return idx >= 0 ? normalized.slice(idx + 1) : normalized;
-}
-
-function joinPath(parent, child) {
- const cleanParent = normalizePath(parent);
- const cleanChild = normalizePath(child);
- return cleanParent ? `${cleanParent}/${cleanChild}` : cleanChild;
-}
-
-async function openPlayableTarget(target, play) {
- state.folderPath = target.path || '';
- await loadMedia();
- const card = document.querySelector(`#media-grid [data-id="${target.media.id}"]`);
- if (card) selectByElement(card);
- const idx = state.media.findIndex((m) => String(m.id) === String(target.media.id));
- const media = idx >= 0 ? state.media[idx] : { ...target.media, browse_path: target.path || '' };
- if (play) {
- try {
- const detail = await API.mediaDetail(media.id);
- const resumeFrom = detail?.progress?.position_seconds ?? 0;
- selectAndPlay(media, idx >= 0 ? idx : 0, resumeFrom);
- } catch {
- selectAndPlay(media, idx >= 0 ? idx : 0, 0);
- }
- }
-}
-
-export function seekByKeyboard(direction, repeated) {
- if (!hasLoadedMedia()) return false;
- if (!document.fullscreenElement) return false;
- const step = repeated ? 15 : 5;
- return seekRelative(direction * step);
-}