summaryrefslogtreecommitdiff
path: root/web/js/views
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-17 15:16:44 +0300
committerPaul Buetow <paul@buetow.org>2026-05-17 15:16:44 +0300
commite56136f4fc5ac12735f332f1cde8f3748103ef1d (patch)
treea59a27702cfe4e67caa02989cc433fcdc16d454f /web/js/views
parent5de8f79ef831d1218499c1e33bad6cc121115dd1 (diff)
Add in-progress frontend actions
Diffstat (limited to 'web/js/views')
-rw-r--r--web/js/views/media-actions.js21
-rw-r--r--web/js/views/media-grid.js41
-rw-r--r--web/js/views/media-info.js22
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>