diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-17 15:16:44 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-17 15:16:44 +0300 |
| commit | e56136f4fc5ac12735f332f1cde8f3748103ef1d (patch) | |
| tree | a59a27702cfe4e67caa02989cc433fcdc16d454f /web/js/views | |
| parent | 5de8f79ef831d1218499c1e33bad6cc121115dd1 (diff) | |
Add in-progress frontend actions
Diffstat (limited to 'web/js/views')
| -rw-r--r-- | web/js/views/media-actions.js | 21 | ||||
| -rw-r--r-- | web/js/views/media-grid.js | 41 | ||||
| -rw-r--r-- | web/js/views/media-info.js | 22 |
3 files changed, 80 insertions, 4 deletions
diff --git a/web/js/views/media-actions.js b/web/js/views/media-actions.js index 2367241..93f3b0b 100644 --- a/web/js/views/media-actions.js +++ b/web/js/views/media-actions.js @@ -28,6 +28,27 @@ export async function toggleFavorite(id, btn) { } } +async function setProgressStatus(id, status, successMessage) { + const mediaId = id || selectedMediaId(); + if (!mediaId) return false; + try { + await API.progressStatus(mediaId, status); + toast(successMessage); + return true; + } catch (err) { + toast(err.message || 'Progress update failed', 'error'); + return false; + } +} + +export function markAsFinished(id) { + return setProgressStatus(id, 'finished', 'Marked as finished'); +} + +export function markAsNotStarted(id) { + return setProgressStatus(id, 'not_started', 'Marked as not started'); +} + export async function openNotesForSelected() { const el = currentElement(); if (!el) return; diff --git a/web/js/views/media-grid.js b/web/js/views/media-grid.js index e82ae98..542277e 100644 --- a/web/js/views/media-grid.js +++ b/web/js/views/media-grid.js @@ -76,7 +76,15 @@ export async function loadMedia() { const setIds = state.selectedSetIds.length > 1 ? state.selectedSetIds.join(',') : ''; const singleSetId = state.selectedSetIds.length === 1 ? state.selectedSetIds[0] : null; - if (!singleSetId && !setIds && !callbacks.isShuffle?.() && !hasActiveFilters()) { + if (state.virtualSet === 'in-progress') { + breadcrumb?.classList.add('hidden'); + syncMediaPage('virtual:in-progress'); + const data = await API.inProgress(); + const list = Array.isArray(data) ? data : data?.media || []; + setMedia(list); + const page = renderGrid(list, 'No in-progress media.'); + resultCount.textContent = resultText(list.length, page); + } else if (!singleSetId && !setIds && !callbacks.isShuffle?.() && !hasActiveFilters()) { breadcrumb?.classList.add('hidden'); setMedia([]); syncMediaPage('sets'); @@ -176,6 +184,13 @@ export function mediaWithBrowsePath(media, path) { export function navigateBack() { if (!state.folderPath) { + if (state.virtualSet) { + state.virtualSet = ''; + state.mediaPage = 0; + callbacks.onVirtualSetCleared?.(); + loadMedia(); + return; + } if (state.selectedSetId || state.selectedSetIds.length) { state.selectedSetId = null; state.selectedSetIds = []; @@ -349,13 +364,13 @@ function renderFolder(folder, index) { `; } -function renderGrid(items) { +function renderGrid(items, emptyMessage = 'No results.') { const grid = document.getElementById('media-grid'); if (!grid) return; if (!items.length) { const page = paginateItems([], state.mediaPage); state.mediaPage = page.page; - grid.innerHTML = `<p class="text-muted text-sm grid-full">No results.</p>`; + grid.innerHTML = `<p class="text-muted text-sm grid-full">${escapeHtml(emptyMessage)}</p>`; clearSelection(); return page; } @@ -409,6 +424,8 @@ function bindMediaItems(grid) { const downloadBtn = el.querySelector('[data-action="download"]'); const tagBtn = el.querySelector('[data-action="tags"]'); const thumbBtn = el.querySelector('[data-action="regen-thumb"]'); + const finishedBtn = el.querySelector('[data-action="mark-finished"]'); + const notStartedBtn = el.querySelector('[data-action="mark-not-started"]'); playBtn?.addEventListener('click', (e) => { e.stopPropagation(); selectByElement(el); @@ -439,9 +456,24 @@ function bindMediaItems(grid) { e.stopPropagation(); callbacks.regenThumb?.(el.dataset.id); }); + finishedBtn?.addEventListener('click', async (e) => { + e.stopPropagation(); + await callbacks.markAsFinished?.(el.dataset.id); + }); + notStartedBtn?.addEventListener('click', async (e) => { + e.stopPropagation(); + await callbacks.markAsNotStarted?.(el.dataset.id); + }); }); } +function renderProgressActions() { + return ` + <button class="icon-btn btn-sm" data-action="mark-finished" title="Mark as finished">✓</button> + <button class="icon-btn btn-sm" data-action="mark-not-started" title="Mark as not started">↺</button> + `; +} + function renderItem(m, index) { const sizeText = fmtSize(m.file_size_bytes); const flattenedFolder = m.flattened_folder ? 'true' : 'false'; @@ -455,6 +487,7 @@ function renderItem(m, index) { <button class="icon-btn btn-sm" data-action="play" title="Play">▶</button> <button class="icon-btn btn-sm" data-action="favorite" title="Favorite">♥</button> <button class="icon-btn btn-sm" data-action="notes" title="Notes">📝</button> + ${renderProgressActions()} <button class="icon-btn btn-sm" data-action="download" title="Download">⬇</button> <button class="icon-btn btn-sm" data-action="tags" title="Tags">🏷</button> <button class="icon-btn btn-sm" data-action="regen-thumb" title="Regenerate thumbnail">🔄</button> @@ -479,6 +512,7 @@ function renderItem(m, index) { <button class="icon-btn btn-sm" data-action="play" title="Play">▶</button> <button class="icon-btn btn-sm" data-action="favorite" title="Favorite">♥</button> <button class="icon-btn btn-sm" data-action="notes" title="Notes">📝</button> + ${renderProgressActions()} <button class="icon-btn btn-sm" data-action="download" title="Download">⬇</button> <button class="icon-btn btn-sm" data-action="tags" title="Tags">🏷</button> <button class="icon-btn btn-sm" data-action="regen-thumb" title="Regenerate thumbnail">🔄</button> @@ -500,6 +534,7 @@ function renderItem(m, index) { <button class="icon-btn btn-sm" data-action="play" title="Play">▶</button> <button class="icon-btn btn-sm" data-action="favorite" title="Favorite">♥</button> <button class="icon-btn btn-sm" data-action="notes" title="Notes">📝</button> + ${renderProgressActions()} <button class="icon-btn btn-sm" data-action="download" title="Download">⬇</button> <button class="icon-btn btn-sm" data-action="tags" title="Tags">🏷</button> </div> diff --git a/web/js/views/media-info.js b/web/js/views/media-info.js index 51e576c..bbb1f98 100644 --- a/web/js/views/media-info.js +++ b/web/js/views/media-info.js @@ -3,13 +3,29 @@ import { fmtDateTime, fmtSize } from '../dom.js'; import { escapeHtml, fmtDur, toast } from '../utils.js'; import { selectedMediaId } from './media-actions.js'; -export function initMediaInfo() { +let callbacks = {}; + +export function initMediaInfo(options = {}) { + callbacks = options; const modal = document.getElementById('media-info-modal'); const closeBtn = document.getElementById('media-info-close'); closeBtn?.addEventListener('click', closeMediaInfo); modal?.addEventListener('click', (e) => { if (e.target === modal) closeMediaInfo(); }); + modal?.addEventListener('click', async (e) => { + const button = e.target.closest('[data-media-info-action]'); + if (!button) return; + const id = button.dataset.mediaId; + if (!id) return; + let updated = false; + if (button.dataset.mediaInfoAction === 'mark-finished') { + updated = await callbacks.markAsFinished?.(id); + } else if (button.dataset.mediaInfoAction === 'mark-not-started') { + updated = await callbacks.markAsNotStarted?.(id); + } + if (updated) await openMediaInfo(id); + }); } export function closeMediaInfo() { @@ -113,6 +129,10 @@ function renderMediaInfo(detail) { .join(''); const raw = escapeHtml(JSON.stringify(detail || {}, null, 2)); return ` + <div class="flex flex-wrap gap-2 mt-1 mb-2"> + <button class="btn btn-ghost btn-sm" type="button" data-media-info-action="mark-finished" data-media-id="${escapeHtml(String(media.id || ''))}">✓ Finished</button> + <button class="btn btn-ghost btn-sm" type="button" data-media-info-action="mark-not-started" data-media-id="${escapeHtml(String(media.id || ''))}">↺ Not started</button> + </div> <table class="media-info-table">${table}</table> <details class="media-info-raw"> <summary>Raw API detail</summary> |
