The duplicate scan was one blocking request with a spinner: no progress, a modal that could not be put away, and a result lost if the tab closed. - POST /prompt_manager/scan_duplicates/stream starts the scan as a background job (same ScanJob fan-out as the output scan) or attaches to the running one; GET /prompt_manager/scan_duplicates/status reports it. The job holds the duplicates lock, so deletes are refused while it runs. - hashing runs on the configured worker threads and reports progress about every percent - the gallery modal shows a progress bar, closing it minimizes to a pill that reopens it, results wait in the pill if the modal was closed, and a reload re-attaches to a running scan Infinite scrolling is a new Display Setting (off by default): the next page is appended when the viewport nears the bottom, pagination controls hide, and the load-more rule lives in list-state.js where it is unit tested. Thumb-size buttons and the column setting no longer override each other.
65 lines
2.9 KiB
JavaScript
65 lines
2.9 KiB
JavaScript
/**
|
|
* Pure state helpers for the paginated prompt and image lists.
|
|
*
|
|
* Loadable in the browser (window.ListState) and in Node for tests.
|
|
*/
|
|
(function (root) {
|
|
"use strict";
|
|
|
|
function nonNegativeInt(value) {
|
|
return Number.isInteger(value) && value >= 0 ? value : null;
|
|
}
|
|
|
|
/**
|
|
* Pagination state after a page fetch succeeded. Callers keep their current
|
|
* state untouched while the request is in flight and only adopt the result.
|
|
*
|
|
* @param {{page: number, limit: number, total: number}} current
|
|
* @param {{page?: number, limit?: number, total?: number}} response - the page
|
|
* that was requested and the total the server reported
|
|
* @returns {{page: number, limit: number, total: number, totalPages: number}}
|
|
*/
|
|
function nextPageState(current, response) {
|
|
const limit = Number.isInteger(response.limit) && response.limit > 0 ? response.limit : current.limit;
|
|
const total = nonNegativeInt(response.total) ?? current.total;
|
|
const totalPages = Math.max(1, Math.ceil(total / limit));
|
|
const requested = Number.isInteger(response.page) ? response.page : current.page;
|
|
const page = Math.min(Math.max(1, requested), totalPages);
|
|
return { page, limit, total, totalPages };
|
|
}
|
|
|
|
/** Whether an in-place prompt edit changed the text to something worth saving. */
|
|
function shouldSaveEdit(originalText, newText) {
|
|
if (typeof newText !== "string") return false;
|
|
const trimmed = newText.trim();
|
|
return trimmed !== "" && trimmed !== String(originalText ?? "").trim();
|
|
}
|
|
|
|
/**
|
|
* Whether another page should be fetched from a capped list endpoint such as
|
|
* /prompt_manager/images/all: the server said has_more and the last page was not empty.
|
|
*/
|
|
function hasMorePages(response, receivedCount) {
|
|
const pagination = response && response.pagination;
|
|
return Boolean(pagination && pagination.has_more) && receivedCount > 0;
|
|
}
|
|
|
|
/**
|
|
* Infinite scrolling: fetch the next page when the user is within `threshold`
|
|
* pixels of the bottom (or the content does not fill the viewport yet), no
|
|
* load is in flight and pages remain.
|
|
*/
|
|
function shouldLoadMore({ infiniteScroll, loading, page, limit, total, scrollTop, clientHeight, scrollHeight, threshold = 600 }) {
|
|
if (!infiniteScroll || loading) return false;
|
|
const pageSize = nonNegativeInt(limit) || 1;
|
|
const totalPages = Math.ceil(nonNegativeInt(total) / pageSize);
|
|
if (nonNegativeInt(page) >= totalPages) return false;
|
|
const remaining = scrollHeight - clientHeight - scrollTop;
|
|
return remaining < threshold;
|
|
}
|
|
|
|
const api = { nextPageState, shouldSaveEdit, hasMorePages, shouldLoadMore };
|
|
if (typeof module !== "undefined" && module.exports) module.exports = api;
|
|
else root.ListState = api;
|
|
})(typeof window !== "undefined" ? window : globalThis);
|