pasteup 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pasteup/__init__.py +3 -0
- pasteup/__main__.py +4 -0
- pasteup/cli.py +171 -0
- pasteup/main.py +34 -0
- pasteup/models/__init__.py +0 -0
- pasteup/models/schemas.py +410 -0
- pasteup/routes/__init__.py +0 -0
- pasteup/routes/jobs.py +678 -0
- pasteup/routes/ui.py +43 -0
- pasteup/services/__init__.py +0 -0
- pasteup/services/cover_collage.py +379 -0
- pasteup/services/exporter.py +113 -0
- pasteup/services/folder_scanner.py +164 -0
- pasteup/services/grid_composer.py +199 -0
- pasteup/services/image_batcher.py +21 -0
- pasteup/services/video_exporter.py +227 -0
- pasteup/shortcut.py +97 -0
- pasteup/static/css/styles.css +983 -0
- pasteup/static/favicon.svg +9 -0
- pasteup/static/fonts/OFL-Caveat.txt +93 -0
- pasteup/static/fonts/OFL-PatrickHand.txt +93 -0
- pasteup/static/fonts/caveat-latin-700.woff2 +0 -0
- pasteup/static/fonts/patrick-hand-latin-400.woff2 +0 -0
- pasteup/static/js/app.js +1680 -0
- pasteup/templates/index.html +309 -0
- pasteup-0.1.0.dist-info/METADATA +481 -0
- pasteup-0.1.0.dist-info/RECORD +32 -0
- pasteup-0.1.0.dist-info/WHEEL +4 -0
- pasteup-0.1.0.dist-info/entry_points.txt +2 -0
- pasteup-0.1.0.dist-info/licenses/LICENSE +202 -0
- pasteup-0.1.0.dist-info/licenses/src/pasteup/static/fonts/OFL-Caveat.txt +93 -0
- pasteup-0.1.0.dist-info/licenses/src/pasteup/static/fonts/OFL-PatrickHand.txt +93 -0
pasteup/static/js/app.js
ADDED
|
@@ -0,0 +1,1680 @@
|
|
|
1
|
+
// Matches IMAGES_PER_PAGE in services/image_batcher.py: a sheet is a 4 x 3 grid.
|
|
2
|
+
const IMAGES_PER_SHEET = 12;
|
|
3
|
+
const PART_FILLED_OPTION = "“Include a part-filled last page”";
|
|
4
|
+
const GENERATION_MODES = {
|
|
5
|
+
single: {
|
|
6
|
+
inputLabel: "Bundle folder",
|
|
7
|
+
outputLabel: "Saves to",
|
|
8
|
+
endpoint: "/api/jobs/generate",
|
|
9
|
+
payload: () => ({
|
|
10
|
+
folder_path: readWorkingFolderPath(),
|
|
11
|
+
output_path: outputFolderEl.value.trim(),
|
|
12
|
+
...readCommonGenerationSettings(),
|
|
13
|
+
}),
|
|
14
|
+
statusMessage: "Making preview sheets…",
|
|
15
|
+
buttonText: "Make preview sheets",
|
|
16
|
+
helpText: "Point Pasteup at the folder that holds one bundle's images.",
|
|
17
|
+
modeStepCopy: "Counts your images and checks they can be read.",
|
|
18
|
+
folderStepCopy: "Results go in a ---Created--- folder next to it.",
|
|
19
|
+
outputStepCopy: "Preview sheets of every image, or one cover for the listing.",
|
|
20
|
+
outputHintReady: "Ready! Make preview sheets, a cover, or both.",
|
|
21
|
+
},
|
|
22
|
+
batch: {
|
|
23
|
+
inputLabel: "Folder of bundles",
|
|
24
|
+
outputLabel: "Saves to",
|
|
25
|
+
endpoint: "/api/jobs/generate-all-folders",
|
|
26
|
+
payload: () => readBatchFolderPayload(),
|
|
27
|
+
statusMessage: "Making preview sheets for the selected bundles…",
|
|
28
|
+
buttonText: "Make preview sheets",
|
|
29
|
+
helpText: "Point Pasteup at a folder with one folder inside per bundle.",
|
|
30
|
+
modeStepCopy: "Lists the bundle folders inside and checks each one.",
|
|
31
|
+
folderStepCopy: "Each bundle gets its own folder inside a ---Created--- folder next to it.",
|
|
32
|
+
outputStepCopy: "Preview sheets or a cover for every bundle you checked.",
|
|
33
|
+
outputHintReady: "Ready! Make images for the checked bundles.",
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const generationModeEls = document.querySelectorAll('input[name="generation-mode"]');
|
|
38
|
+
const inputFolderLabelEl = document.getElementById("input-folder-label");
|
|
39
|
+
const outputFolderLabelEl = document.getElementById("output-folder-label");
|
|
40
|
+
const modeHelpEl = document.getElementById("mode-help");
|
|
41
|
+
const modeStepCopyEl = document.getElementById("mode-step-copy");
|
|
42
|
+
const folderStepCopyEl = document.getElementById("folder-step-copy");
|
|
43
|
+
const outputStepCopyEl = document.getElementById("output-step-copy");
|
|
44
|
+
const inputFolderEl = document.getElementById("input-folder");
|
|
45
|
+
const outputFolderEl = document.getElementById("output-folder");
|
|
46
|
+
const pageWidthEl = document.getElementById("page-width");
|
|
47
|
+
const pageHeightEl = document.getElementById("page-height");
|
|
48
|
+
const gapEl = document.getElementById("gap");
|
|
49
|
+
const marginEl = document.getElementById("margin");
|
|
50
|
+
const formatEl = document.getElementById("format");
|
|
51
|
+
const createPreviewVideoEl = document.getElementById("create-preview-video");
|
|
52
|
+
const videoDurationSecondsEl = document.getElementById("video-duration-seconds");
|
|
53
|
+
const videoTransitionEl = document.getElementById("video-transition");
|
|
54
|
+
const createPreviewVideoHelpEl = document.getElementById("create-preview-video-help");
|
|
55
|
+
const coverCollageTemplateEl = document.getElementById("cover-collage-template");
|
|
56
|
+
const coverCollageIncludeListEl = document.getElementById("cover-collage-include-list");
|
|
57
|
+
const pageUnitEl = document.getElementById("page-unit");
|
|
58
|
+
const dpiEl = document.getElementById("dpi");
|
|
59
|
+
const backgroundCleanModeEl = document.getElementById("background-clean-mode");
|
|
60
|
+
const backgroundToleranceEl = document.getElementById("background-tolerance");
|
|
61
|
+
const backgroundFeatherEl = document.getElementById("background-feather");
|
|
62
|
+
const includePartialPageEl = document.getElementById("include-partial-page");
|
|
63
|
+
|
|
64
|
+
const scanButton = document.getElementById("scan-button");
|
|
65
|
+
const generateButton = document.getElementById("generate-button");
|
|
66
|
+
const coverCollageButton = document.getElementById("cover-collage-button");
|
|
67
|
+
const outputActionsSectionEl = document.getElementById("output-actions-section");
|
|
68
|
+
const outputActionsHintEl = document.getElementById("output-actions-hint");
|
|
69
|
+
const scanResultEl = document.getElementById("scan-result");
|
|
70
|
+
const generationResultEl = document.getElementById("generation-result");
|
|
71
|
+
const subfolderSectionEl = document.getElementById("subfolder-picker-section");
|
|
72
|
+
const subfolderSingleControlsEl = document.getElementById("subfolder-single-controls");
|
|
73
|
+
const subfolderBatchControlsEl = document.getElementById("subfolder-batch-controls");
|
|
74
|
+
const subfolderPickerEl = document.getElementById("subfolder-picker");
|
|
75
|
+
const batchSubfolderChecklistEl = document.getElementById("batch-subfolder-checklist");
|
|
76
|
+
const useSubfolderButton = document.getElementById("use-subfolder-button");
|
|
77
|
+
const selectAllSubfoldersButton = document.getElementById("select-all-subfolders-button");
|
|
78
|
+
const clearSubfolderSelectionButton = document.getElementById("clear-subfolder-selection-button");
|
|
79
|
+
const batchScanButton = document.getElementById("batch-scan-button");
|
|
80
|
+
const batchSelectionSummaryEl = document.getElementById("batch-selection-summary");
|
|
81
|
+
const batchSelectionStatusEl = document.getElementById("batch-selection-status");
|
|
82
|
+
const videoSettingsEl = document.getElementById("video-settings");
|
|
83
|
+
const backgroundCleanSettingsEl = document.getElementById("background-clean-settings");
|
|
84
|
+
|
|
85
|
+
const generationState = {
|
|
86
|
+
mode: document.querySelector('input[name="generation-mode"]:checked')?.value || "single",
|
|
87
|
+
singleFolderReady: false,
|
|
88
|
+
scannedRootFolder: "",
|
|
89
|
+
outputDerivationSourceFolder: "",
|
|
90
|
+
selectedWorkingFolder: "",
|
|
91
|
+
availableSubfolders: [],
|
|
92
|
+
selectedBatchFolders: [],
|
|
93
|
+
batchParentScanComplete: false,
|
|
94
|
+
batchValidationReady: false,
|
|
95
|
+
validatedBatchFolders: [],
|
|
96
|
+
batchValidationResults: [],
|
|
97
|
+
validatedBatchSelectionSignature: "",
|
|
98
|
+
busy: false,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Every action posts JSON and renders the reply. Failures arrive in three
|
|
102
|
+
// shapes: a `message` from a failed generation, FastAPI's `detail` (a string, or
|
|
103
|
+
// a list of validation errors), or no JSON at all when the request never
|
|
104
|
+
// completed. They are normalised here so no caller renders an error as success.
|
|
105
|
+
async function postJson(url, payload) {
|
|
106
|
+
let response;
|
|
107
|
+
try {
|
|
108
|
+
response = await fetch(url, {
|
|
109
|
+
method: "POST",
|
|
110
|
+
headers: { "Content-Type": "application/json" },
|
|
111
|
+
body: JSON.stringify(payload),
|
|
112
|
+
});
|
|
113
|
+
} catch (error) {
|
|
114
|
+
return { ok: false, data: null, errorMessage: `Can't reach Pasteup. Is it still running? (${error.message})` };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let data = null;
|
|
118
|
+
try {
|
|
119
|
+
data = await response.json();
|
|
120
|
+
} catch {
|
|
121
|
+
data = null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (response.ok && data) {
|
|
125
|
+
return { ok: true, data, errorMessage: "" };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { ok: false, data, errorMessage: describeErrorResponse(response, data) };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function describeErrorResponse(response, data) {
|
|
132
|
+
if (typeof data?.message === "string" && data.message) {
|
|
133
|
+
return data.message;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (typeof data?.detail === "string" && data.detail) {
|
|
137
|
+
return data.detail;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (Array.isArray(data?.detail) && data.detail.length > 0) {
|
|
141
|
+
return data.detail
|
|
142
|
+
.map((issue) => {
|
|
143
|
+
const field = Array.isArray(issue?.loc) ? issue.loc.filter((part) => part !== "body").join(".") : "";
|
|
144
|
+
return field ? `${field}: ${issue?.msg}` : String(issue?.msg ?? "");
|
|
145
|
+
})
|
|
146
|
+
.join(" ");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return `The server returned an error (HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ""}).`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// One request-backed action at a time: while it runs, the action buttons are
|
|
153
|
+
// disabled, so a second click cannot start a duplicate export.
|
|
154
|
+
async function runExclusive(action) {
|
|
155
|
+
if (generationState.busy) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
setBusy(true);
|
|
160
|
+
try {
|
|
161
|
+
await action();
|
|
162
|
+
} finally {
|
|
163
|
+
setBusy(false);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function setBusy(isBusy) {
|
|
168
|
+
generationState.busy = Boolean(isBusy);
|
|
169
|
+
document.body.classList.toggle("is-busy", generationState.busy);
|
|
170
|
+
recomputeStepFiveState();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Each result card sits beside its step on wide screens and below it on narrow
|
|
174
|
+
// ones, where it can be out of view; bring the one about to change into view.
|
|
175
|
+
function revealResult(container) {
|
|
176
|
+
const panel = container.closest(".result-panel") || container;
|
|
177
|
+
const { top } = panel.getBoundingClientRect();
|
|
178
|
+
if (top < 0 || top > window.innerHeight * 0.75) {
|
|
179
|
+
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
180
|
+
panel.scrollIntoView({ behavior: reduceMotion ? "auto" : "smooth", block: "start" });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function fileName(path) {
|
|
185
|
+
return String(path).split(/[\\/]/).pop() || String(path);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Print resolution and background cleaning. Kept separate from the video
|
|
189
|
+
// settings so the cover-collage payloads, which enumerate their fields rather
|
|
190
|
+
// than spreading everything, can pick these up without also sending video keys
|
|
191
|
+
// their endpoint does not accept.
|
|
192
|
+
function readOutputQualitySettings() {
|
|
193
|
+
const backgroundCleanMode = backgroundCleanModeEl?.value || "off";
|
|
194
|
+
return {
|
|
195
|
+
page_unit: pageUnitEl?.value || "px",
|
|
196
|
+
dpi: Number(dpiEl?.value) || 300,
|
|
197
|
+
background_clean_mode: backgroundCleanMode,
|
|
198
|
+
// Hidden while cleaning is off, so only sent when it is on: a value left
|
|
199
|
+
// out of range in a hidden field must not fail the request.
|
|
200
|
+
...(backgroundCleanMode === "off" ? {} : {
|
|
201
|
+
background_tolerance: Number(backgroundToleranceEl?.value ?? 240),
|
|
202
|
+
background_feather: Number(backgroundFeatherEl?.value ?? 1),
|
|
203
|
+
}),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function readCommonGenerationSettings() {
|
|
208
|
+
return {
|
|
209
|
+
...readOutputQualitySettings(),
|
|
210
|
+
include_partial_page: Boolean(includePartialPageEl?.checked),
|
|
211
|
+
page_width: Number(pageWidthEl.value),
|
|
212
|
+
page_height: Number(pageHeightEl.value),
|
|
213
|
+
gap: Number(gapEl.value),
|
|
214
|
+
margin: Number(marginEl.value),
|
|
215
|
+
format: formatEl.value,
|
|
216
|
+
create_preview_video: Boolean(createPreviewVideoEl?.checked),
|
|
217
|
+
// Likewise hidden while no video is wanted.
|
|
218
|
+
duration_seconds: createPreviewVideoEl?.checked ? Number(videoDurationSecondsEl?.value) : undefined,
|
|
219
|
+
transition: videoTransitionEl?.value || "none",
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Point at the field to fix: open the section it is in, then let the browser
|
|
224
|
+
// focus it and show its own range message beside it.
|
|
225
|
+
function reportSettingsProblem(message, field) {
|
|
226
|
+
renderResultCard(generationResultEl, { tone: "warning", badgeLabel: "Check settings", message });
|
|
227
|
+
const disclosure = field?.closest("details");
|
|
228
|
+
if (disclosure) {
|
|
229
|
+
disclosure.open = true;
|
|
230
|
+
}
|
|
231
|
+
if (field?.reportValidity()) {
|
|
232
|
+
field.focus();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function validateVideoGenerationSettings() {
|
|
237
|
+
if (!createPreviewVideoEl?.checked) {
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const commonSettings = readCommonGenerationSettings();
|
|
242
|
+
const maxDurationSeconds = Number(videoDurationSecondsEl?.max) || 120;
|
|
243
|
+
|
|
244
|
+
if (!(commonSettings.duration_seconds > 0) || commonSettings.duration_seconds > maxDurationSeconds) {
|
|
245
|
+
reportSettingsProblem(`The video must be 1 to ${maxDurationSeconds} seconds long.`, videoDurationSecondsEl);
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function validateOutputQualitySettings() {
|
|
253
|
+
const quality = readOutputQualitySettings();
|
|
254
|
+
|
|
255
|
+
if (!Number.isFinite(quality.dpi) || quality.dpi <= 0 || quality.dpi > 1200) {
|
|
256
|
+
reportSettingsProblem("DPI must be between 1 and 1200.", dpiEl);
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (quality.background_clean_mode === "off") {
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (!(quality.background_tolerance >= 0 && quality.background_tolerance <= 255)) {
|
|
265
|
+
reportSettingsProblem("Tolerance must be between 0 and 255.", backgroundToleranceEl);
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (!(quality.background_feather >= 0 && quality.background_feather <= 25)) {
|
|
270
|
+
reportSettingsProblem("Edge softening must be between 0 and 25.", backgroundFeatherEl);
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function readMainInputFolderPath() {
|
|
278
|
+
return inputFolderEl.value.trim();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function readWorkingFolderPath() {
|
|
282
|
+
return generationState.selectedWorkingFolder || readMainInputFolderPath();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function clearSelectedWorkingFolder() {
|
|
286
|
+
generationState.selectedWorkingFolder = "";
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function normalizeSelectedBatchFolders(selectedPaths = generationState.selectedBatchFolders, availableSubfolders = generationState.availableSubfolders) {
|
|
290
|
+
const availablePathSet = new Set((Array.isArray(availableSubfolders) ? availableSubfolders : []).map(({ fullPath }) => fullPath));
|
|
291
|
+
const seenPaths = new Set();
|
|
292
|
+
|
|
293
|
+
return (Array.isArray(selectedPaths) ? selectedPaths : [])
|
|
294
|
+
.map((path) => (typeof path === "string" ? path.trim() : ""))
|
|
295
|
+
.filter((path) => path && availablePathSet.has(path) && !seenPaths.has(path) && seenPaths.add(path));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function setSelectedBatchFolders(selectedPaths = []) {
|
|
299
|
+
generationState.selectedBatchFolders = normalizeSelectedBatchFolders(selectedPaths);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function resetBatchValidationState() {
|
|
303
|
+
generationState.batchParentScanComplete = false;
|
|
304
|
+
generationState.batchValidationReady = false;
|
|
305
|
+
generationState.validatedBatchFolders = [];
|
|
306
|
+
generationState.batchValidationResults = [];
|
|
307
|
+
generationState.validatedBatchSelectionSignature = "";
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function hasSelectedBatchFolders() {
|
|
311
|
+
return getSelectedBatchFolderPaths().length > 0;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function setBatchParentScanComplete(isComplete) {
|
|
315
|
+
generationState.batchParentScanComplete = Boolean(isComplete);
|
|
316
|
+
if (!generationState.batchParentScanComplete) {
|
|
317
|
+
generationState.batchValidationReady = false;
|
|
318
|
+
generationState.validatedBatchSelectionSignature = "";
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function getBatchSelectionSignature(selectedPaths = getSelectedBatchFolderPaths()) {
|
|
323
|
+
return [...selectedPaths].sort().join("||");
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function setBatchValidationResults(results = []) {
|
|
327
|
+
generationState.batchValidationResults = Array.isArray(results) ? results : [];
|
|
328
|
+
generationState.validatedBatchFolders = generationState.batchValidationResults
|
|
329
|
+
.filter((result) => result?.valid && typeof result.folderPath === "string")
|
|
330
|
+
.map((result) => result.folderPath);
|
|
331
|
+
refreshBatchValidationReadiness();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// A selection is ready when every picked bundle passed its last check, so
|
|
335
|
+
// unticking a bundle (say, one that failed) needs no new check; picking one
|
|
336
|
+
// that was never checked, or that failed, does.
|
|
337
|
+
function refreshBatchValidationReadiness() {
|
|
338
|
+
const selectedPaths = getSelectedBatchFolderPaths();
|
|
339
|
+
const validatedPathSet = new Set(generationState.validatedBatchFolders);
|
|
340
|
+
generationState.batchValidationReady = selectedPaths.length > 0
|
|
341
|
+
&& selectedPaths.every((folderPath) => validatedPathSet.has(folderPath));
|
|
342
|
+
generationState.validatedBatchSelectionSignature = generationState.batchValidationReady
|
|
343
|
+
? getBatchSelectionSignature(selectedPaths)
|
|
344
|
+
: "";
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// After the picks change, readiness and the check card follow the new picks.
|
|
348
|
+
function applyBatchSelectionChange() {
|
|
349
|
+
refreshBatchValidationReadiness();
|
|
350
|
+
if (generationState.batchValidationResults.length > 0) {
|
|
351
|
+
renderBatchCheckSummary();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function countFailedSelectedBatchFolders() {
|
|
356
|
+
const selectedPathSet = new Set(getSelectedBatchFolderPaths());
|
|
357
|
+
return generationState.batchValidationResults
|
|
358
|
+
.filter((result) => !result?.valid && selectedPathSet.has(result?.folderPath))
|
|
359
|
+
.length;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function hasValidatedBatchSelection() {
|
|
363
|
+
if (generationState.mode !== "batch") {
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const selectedPaths = getSelectedBatchFolderPaths();
|
|
368
|
+
if (selectedPaths.length === 0) {
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const validatedPathSet = new Set(generationState.validatedBatchFolders);
|
|
373
|
+
return generationState.batchValidationReady
|
|
374
|
+
&& generationState.validatedBatchSelectionSignature === getBatchSelectionSignature(selectedPaths)
|
|
375
|
+
&& selectedPaths.every((folderPath) => validatedPathSet.has(folderPath));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
function setAvailableSubfolders(subfolders) {
|
|
380
|
+
generationState.availableSubfolders = Array.isArray(subfolders)
|
|
381
|
+
? subfolders.map(normalizeSubfolderOption).filter(Boolean)
|
|
382
|
+
: [];
|
|
383
|
+
setSelectedBatchFolders(generationState.selectedBatchFolders);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function setScannedRootFolder(folderPath) {
|
|
387
|
+
generationState.scannedRootFolder = typeof folderPath === "string" ? folderPath.trim() : "";
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function setOutputDerivationSourceFolder(folderPath) {
|
|
391
|
+
generationState.outputDerivationSourceFolder = typeof folderPath === "string" ? folderPath.trim() : "";
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function readSingleFolderPayload() {
|
|
395
|
+
return {
|
|
396
|
+
folder_path: readWorkingFolderPath(),
|
|
397
|
+
output_path: outputFolderEl.value.trim(),
|
|
398
|
+
...readCommonGenerationSettings(),
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function readBatchFolderPayload() {
|
|
403
|
+
const selectedSubfolderPaths = getSelectedBatchFolderPaths();
|
|
404
|
+
|
|
405
|
+
return {
|
|
406
|
+
parent_folder_path: inputFolderEl.value.trim(),
|
|
407
|
+
created_output_root_path: outputFolderEl.value.trim(),
|
|
408
|
+
selected_subfolders: selectedSubfolderPaths,
|
|
409
|
+
...readCommonGenerationSettings(),
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function readSelectedCoverCollageTemplate() {
|
|
414
|
+
return coverCollageTemplateEl?.value || "randomized-grid";
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function readSelectedCoverCollageTemplateLabel() {
|
|
418
|
+
return coverCollageTemplateEl?.selectedOptions?.[0]?.textContent?.trim() || "selected template";
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function readSelectedCoverCollageImages() {
|
|
422
|
+
const rawValue = coverCollageIncludeListEl?.value || "";
|
|
423
|
+
const normalizedItems = rawValue
|
|
424
|
+
.split(/[\n,]+/u)
|
|
425
|
+
.map((value) => value.trim())
|
|
426
|
+
.filter(Boolean);
|
|
427
|
+
|
|
428
|
+
return normalizedItems.length > 0
|
|
429
|
+
? [...new Set(normalizedItems)]
|
|
430
|
+
: null;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function readCoverCollagePayload(folderPath = readWorkingFolderPath(), outputPath = outputFolderEl.value.trim()) {
|
|
434
|
+
const commonSettings = readCommonGenerationSettings();
|
|
435
|
+
return {
|
|
436
|
+
folder_path: folderPath,
|
|
437
|
+
output_path: outputPath,
|
|
438
|
+
page_width: commonSettings.page_width,
|
|
439
|
+
page_height: commonSettings.page_height,
|
|
440
|
+
format: commonSettings.format,
|
|
441
|
+
collage_template: readSelectedCoverCollageTemplate(),
|
|
442
|
+
selected_cover_images: readSelectedCoverCollageImages(),
|
|
443
|
+
...readOutputQualitySettings(),
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function buildBatchCoverCollagePayload() {
|
|
448
|
+
const commonSettings = readCommonGenerationSettings();
|
|
449
|
+
return {
|
|
450
|
+
parent_folder_path: inputFolderEl.value.trim(),
|
|
451
|
+
created_output_root_path: outputFolderEl.value.trim(),
|
|
452
|
+
selected_subfolders: getSelectedBatchFolderPaths(),
|
|
453
|
+
page_width: commonSettings.page_width,
|
|
454
|
+
page_height: commonSettings.page_height,
|
|
455
|
+
format: commonSettings.format,
|
|
456
|
+
collage_template: readSelectedCoverCollageTemplate(),
|
|
457
|
+
...readOutputQualitySettings(),
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
function splitFolderPath(folderPath) {
|
|
463
|
+
if (typeof folderPath !== "string") {
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const trimmedPath = folderPath.trim();
|
|
468
|
+
if (!trimmedPath) {
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const normalizedPath = trimmedPath.replace(/[\\/]+$/, "");
|
|
473
|
+
if (!normalizedPath) {
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const lastSeparatorIndex = Math.max(normalizedPath.lastIndexOf("/"), normalizedPath.lastIndexOf("\\"));
|
|
478
|
+
const baseName = normalizedPath.slice(lastSeparatorIndex + 1);
|
|
479
|
+
|
|
480
|
+
if (!baseName || baseName === "." || baseName === "..") {
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const parentDirectory = lastSeparatorIndex >= 0 ? normalizedPath.slice(0, lastSeparatorIndex) : "";
|
|
485
|
+
return { parentDirectory, baseName };
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function buildDerivedOutputPath(folderPath) {
|
|
489
|
+
const pathParts = splitFolderPath(folderPath);
|
|
490
|
+
if (!pathParts) {
|
|
491
|
+
return "";
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const { parentDirectory, baseName } = pathParts;
|
|
495
|
+
const includeBaseName = generationState.mode !== "batch";
|
|
496
|
+
const derivedFolderName = includeBaseName ? `---Created---/${baseName}` : "---Created---";
|
|
497
|
+
|
|
498
|
+
if (!parentDirectory) {
|
|
499
|
+
return derivedFolderName;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const separator = parentDirectory.includes("\\") && !parentDirectory.includes("/") ? "\\" : "/";
|
|
503
|
+
return `${parentDirectory}${separator}${derivedFolderName.replaceAll("/", separator)}`;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function syncDerivedOutputPath() {
|
|
507
|
+
const sourceFolderPath = generationState.outputDerivationSourceFolder || (
|
|
508
|
+
generationState.mode === "single"
|
|
509
|
+
? generationState.scannedRootFolder || readMainInputFolderPath()
|
|
510
|
+
: readMainInputFolderPath()
|
|
511
|
+
);
|
|
512
|
+
outputFolderEl.value = buildDerivedOutputPath(sourceFolderPath);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function resetCoverCollageIncludeList() {
|
|
516
|
+
if (coverCollageIncludeListEl) {
|
|
517
|
+
coverCollageIncludeListEl.value = "";
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function getSelectedGenerationModeConfig() {
|
|
522
|
+
return GENERATION_MODES[generationState.mode] || GENERATION_MODES.single;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function getSelectedBatchFolderPaths() {
|
|
526
|
+
return normalizeSelectedBatchFolders(generationState.selectedBatchFolders);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function getBatchGenerationSelectionDetails() {
|
|
530
|
+
const availableCount = generationState.availableSubfolders.length;
|
|
531
|
+
const selectedBatchFolders = getSelectedBatchFolderPaths();
|
|
532
|
+
const selectedCount = selectedBatchFolders.length;
|
|
533
|
+
const isAllSelected = availableCount > 0 && selectedCount === availableCount;
|
|
534
|
+
|
|
535
|
+
return {
|
|
536
|
+
availableCount,
|
|
537
|
+
selectedCount,
|
|
538
|
+
isAllSelected,
|
|
539
|
+
selectedBatchFolders,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function recomputeStepFiveState() {
|
|
544
|
+
syncBatchWorkingFolderSelection();
|
|
545
|
+
updateBatchSelectionSummary();
|
|
546
|
+
updateOutputActionState();
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function getBatchSelectionValidationMessage() {
|
|
550
|
+
if (generationState.mode !== "batch") {
|
|
551
|
+
return "";
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const { availableCount, selectedCount, isAllSelected, selectedBatchFolders } = getBatchGenerationSelectionDetails();
|
|
555
|
+
if (!generationState.batchParentScanComplete) {
|
|
556
|
+
return "Scan the folder of bundles to list them.";
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (availableCount === 0) {
|
|
560
|
+
return "No bundle folders were found inside it.";
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (selectedCount === 0) {
|
|
564
|
+
return `Pick at least one of the ${pluralize(availableCount, "bundle")}.`;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (!hasValidatedBatchSelection()) {
|
|
568
|
+
const failedCount = countFailedSelectedBatchFolders();
|
|
569
|
+
if (failedCount > 0) {
|
|
570
|
+
return failedCount === 1
|
|
571
|
+
? "1 picked bundle needs attention. Untick it, or fix it and check again."
|
|
572
|
+
: `${failedCount} picked bundles need attention. Untick them, or fix them and check again.`;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return `${pluralize(selectedCount, "bundle")} picked. Check ${selectedCount === 1 ? "it" : "them"} before making images.`;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const validatedSelectedCount = selectedCount;
|
|
579
|
+
|
|
580
|
+
if (isAllSelected && availableCount > 1) {
|
|
581
|
+
return `All ${availableCount} bundles are checked and ready.`;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return `${pluralize(validatedSelectedCount, "bundle")} checked and ready.`;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function updateBatchSelectionStatus() {
|
|
588
|
+
if (!batchSelectionStatusEl) {
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const message = getBatchSelectionValidationMessage();
|
|
593
|
+
batchSelectionStatusEl.textContent = message || "";
|
|
594
|
+
batchSelectionStatusEl.classList.toggle("status-error", generationState.mode === "batch" && generationState.batchParentScanComplete && getBatchGenerationSelectionDetails().selectedCount === 0);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function isSingleGenerationReady() {
|
|
598
|
+
return generationState.singleFolderReady;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function isBatchGenerationReady() {
|
|
602
|
+
if (generationState.mode !== "batch") {
|
|
603
|
+
return isSingleGenerationReady();
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (!generationState.batchParentScanComplete || !hasSelectedBatchFolders() || !generationState.batchValidationReady) {
|
|
607
|
+
return false;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return hasValidatedBatchSelection();
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function syncBatchWorkingFolderSelection() {
|
|
614
|
+
if (generationState.mode !== "batch") {
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const { selectedBatchFolders: selectedBatchPaths } = getBatchGenerationSelectionDetails();
|
|
619
|
+
generationState.selectedWorkingFolder = selectedBatchPaths.length === 1 ? selectedBatchPaths[0] : "";
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function isCoverCollageReady() {
|
|
623
|
+
if (generationState.mode !== "batch") {
|
|
624
|
+
return generationState.singleFolderReady;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (!generationState.batchParentScanComplete || !generationState.batchValidationReady || !hasValidatedBatchSelection()) {
|
|
628
|
+
return false;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const { selectedCount } = getBatchGenerationSelectionDetails();
|
|
632
|
+
|
|
633
|
+
return selectedCount > 0;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function getGenerationButtonLabel() {
|
|
637
|
+
const previewVideoEnabled = Boolean(createPreviewVideoEl?.checked);
|
|
638
|
+
|
|
639
|
+
if (generationState.mode !== "batch") {
|
|
640
|
+
return previewVideoEnabled ? "Make sheets + video" : getSelectedGenerationModeConfig().buttonText;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const { selectedCount } = getBatchGenerationSelectionDetails();
|
|
644
|
+
|
|
645
|
+
if (selectedCount === 0) {
|
|
646
|
+
return previewVideoEnabled ? "Make sheets + videos" : "Make preview sheets";
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
return previewVideoEnabled
|
|
650
|
+
? `Make sheets + videos for ${pluralize(selectedCount, "bundle")}`
|
|
651
|
+
: `Make sheets for ${pluralize(selectedCount, "bundle")}`;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function updatePreviewVideoControls() {
|
|
655
|
+
const previewVideoEnabled = Boolean(createPreviewVideoEl?.checked);
|
|
656
|
+
|
|
657
|
+
if (videoSettingsEl) {
|
|
658
|
+
videoSettingsEl.hidden = !previewVideoEnabled;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (videoDurationSecondsEl) {
|
|
662
|
+
videoDurationSecondsEl.disabled = !previewVideoEnabled;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (videoTransitionEl) {
|
|
666
|
+
videoTransitionEl.disabled = !previewVideoEnabled;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (createPreviewVideoHelpEl) {
|
|
670
|
+
createPreviewVideoHelpEl.textContent = previewVideoEnabled
|
|
671
|
+
? "Made with the sheets. Etsy takes videos of 5 to 15 seconds."
|
|
672
|
+
: "An MP4 slideshow of the sheets, made at the same time.";
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Tolerance and edge softening only mean something once cleaning is on.
|
|
677
|
+
function updateBackgroundCleanControls() {
|
|
678
|
+
if (backgroundCleanSettingsEl) {
|
|
679
|
+
backgroundCleanSettingsEl.hidden = (backgroundCleanModeEl?.value || "off") === "off";
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function getCoverCollageButtonLabel() {
|
|
684
|
+
if (generationState.mode !== "batch") {
|
|
685
|
+
return "Make cover";
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const { selectedCount } = getBatchGenerationSelectionDetails();
|
|
689
|
+
|
|
690
|
+
if (selectedCount === 0) {
|
|
691
|
+
return "Make covers";
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
return selectedCount === 1 ? "Make cover for 1 bundle" : `Make covers for ${selectedCount} bundles`;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function getGenerationStatusMessage() {
|
|
698
|
+
if (generationState.mode !== "batch") {
|
|
699
|
+
return getSelectedGenerationModeConfig().statusMessage;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const { availableCount, selectedCount } = getBatchGenerationSelectionDetails();
|
|
703
|
+
if (availableCount === 0 || selectedCount === 0) {
|
|
704
|
+
return getSelectedGenerationModeConfig().statusMessage;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
return `Making preview sheets for ${pluralize(selectedCount, "bundle")}…`;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function getOutputActionsHint() {
|
|
711
|
+
if (generationState.mode !== "batch") {
|
|
712
|
+
if (!generationState.singleFolderReady) {
|
|
713
|
+
return "Scan a folder first. These unlock once it has images.";
|
|
714
|
+
}
|
|
715
|
+
return getSelectedGenerationModeConfig().outputHintReady;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const { availableCount, selectedCount, isAllSelected } = getBatchGenerationSelectionDetails();
|
|
719
|
+
if (!generationState.batchParentScanComplete) {
|
|
720
|
+
return "Scan the folder of bundles first.";
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (availableCount === 0) {
|
|
724
|
+
return "No bundle folders were found to make images for.";
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
if (selectedCount === 0) {
|
|
728
|
+
return "Pick bundles in step 1, then check them.";
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (!hasValidatedBatchSelection()) {
|
|
732
|
+
return countFailedSelectedBatchFolders() > 0
|
|
733
|
+
? "Some picked bundles need attention. See step 1."
|
|
734
|
+
: `Check the ${pluralize(selectedCount, "picked bundle")} in step 1 first.`;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if (isAllSelected && availableCount > 1) {
|
|
738
|
+
return `Ready! Make images for all ${availableCount} bundles.`;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
return `Ready! Make images for ${pluralize(selectedCount, "bundle")}.`;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function updateOutputActionState() {
|
|
745
|
+
const isBatchMode = generationState.mode === "batch";
|
|
746
|
+
const generationReady = isBatchMode ? isBatchGenerationReady() : isSingleGenerationReady();
|
|
747
|
+
const batchCoverCollageReady = generationState.batchParentScanComplete
|
|
748
|
+
&& hasSelectedBatchFolders()
|
|
749
|
+
&& hasValidatedBatchSelection();
|
|
750
|
+
const coverCollageReady = isBatchMode ? batchCoverCollageReady : isCoverCollageReady();
|
|
751
|
+
const outputActionsEnabled = isBatchMode
|
|
752
|
+
? generationReady || coverCollageReady
|
|
753
|
+
: generationState.singleFolderReady;
|
|
754
|
+
|
|
755
|
+
generateButton.textContent = getGenerationButtonLabel();
|
|
756
|
+
coverCollageButton.textContent = getCoverCollageButtonLabel();
|
|
757
|
+
generateButton.disabled = !generationReady || generationState.busy;
|
|
758
|
+
coverCollageButton.disabled = !coverCollageReady || generationState.busy;
|
|
759
|
+
[scanButton, batchScanButton].forEach((button) => {
|
|
760
|
+
if (button) {
|
|
761
|
+
button.disabled = generationState.busy;
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
generateButton.classList.toggle("primary-button", generationReady);
|
|
765
|
+
generateButton.classList.toggle("secondary-button", !generationReady);
|
|
766
|
+
coverCollageButton.classList.toggle("primary-button", coverCollageReady);
|
|
767
|
+
coverCollageButton.classList.toggle("secondary-button", !coverCollageReady);
|
|
768
|
+
outputActionsSectionEl.classList.toggle("workflow-step--disabled", !outputActionsEnabled);
|
|
769
|
+
outputActionsHintEl.textContent = getOutputActionsHint();
|
|
770
|
+
updateBatchSelectionStatus();
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function updateBatchSelectionSummary() {
|
|
774
|
+
if (!batchSelectionSummaryEl) {
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const { selectedCount } = getBatchGenerationSelectionDetails();
|
|
779
|
+
const availableCount = generationState.availableSubfolders.length;
|
|
780
|
+
const selectedLabels = generationState.availableSubfolders
|
|
781
|
+
.filter(({ fullPath }) => generationState.selectedBatchFolders.includes(fullPath))
|
|
782
|
+
.map(({ displayName }) => displayName);
|
|
783
|
+
|
|
784
|
+
if (!generationState.batchParentScanComplete) {
|
|
785
|
+
batchSelectionSummaryEl.textContent = "Scan the folder of bundles to list them.";
|
|
786
|
+
updateBatchSelectionStatus();
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
if (availableCount === 0) {
|
|
791
|
+
batchSelectionSummaryEl.textContent = "No bundle folders found yet.";
|
|
792
|
+
updateBatchSelectionStatus();
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if (selectedCount === 0) {
|
|
797
|
+
batchSelectionSummaryEl.textContent = `0 of ${pluralize(availableCount, "bundle")} picked.`;
|
|
798
|
+
updateBatchSelectionStatus();
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
const summarySuffix = selectedLabels.length > 0
|
|
803
|
+
? `: ${selectedLabels.join(", ")}`
|
|
804
|
+
: "";
|
|
805
|
+
batchSelectionSummaryEl.textContent = `${selectedCount} of ${pluralize(availableCount, "bundle")} picked${summarySuffix}.`;
|
|
806
|
+
updateBatchSelectionStatus();
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function renderBatchSubfolderChecklist() {
|
|
810
|
+
if (!batchSubfolderChecklistEl) {
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
batchSubfolderChecklistEl.innerHTML = "";
|
|
815
|
+
|
|
816
|
+
if (generationState.availableSubfolders.length === 0) {
|
|
817
|
+
const emptyState = document.createElement("p");
|
|
818
|
+
emptyState.className = "muted-text";
|
|
819
|
+
emptyState.textContent = "Scan a folder of bundles to list them here.";
|
|
820
|
+
batchSubfolderChecklistEl.append(emptyState);
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const selectedPaths = new Set(generationState.selectedBatchFolders);
|
|
825
|
+
generationState.availableSubfolders.forEach(({ displayName, fullPath }) => {
|
|
826
|
+
const label = document.createElement("label");
|
|
827
|
+
label.className = "subfolder-checklist__item";
|
|
828
|
+
|
|
829
|
+
const checkbox = document.createElement("input");
|
|
830
|
+
checkbox.type = "checkbox";
|
|
831
|
+
checkbox.value = fullPath;
|
|
832
|
+
checkbox.checked = selectedPaths.has(fullPath);
|
|
833
|
+
checkbox.addEventListener("change", () => {
|
|
834
|
+
const nextSelection = generationState.availableSubfolders
|
|
835
|
+
.filter(({ fullPath: availablePath }) => {
|
|
836
|
+
if (availablePath === fullPath) {
|
|
837
|
+
return checkbox.checked;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
return batchSubfolderChecklistEl.querySelector(`input[value="${CSS.escape(availablePath)}"]`)?.checked;
|
|
841
|
+
})
|
|
842
|
+
.map(({ fullPath: availablePath }) => availablePath);
|
|
843
|
+
setSelectedBatchFolders(nextSelection);
|
|
844
|
+
applyBatchSelectionChange();
|
|
845
|
+
renderBatchSubfolderChecklist();
|
|
846
|
+
recomputeStepFiveState();
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
const textNode = document.createElement("span");
|
|
850
|
+
textNode.textContent = displayName;
|
|
851
|
+
|
|
852
|
+
label.append(checkbox, textNode);
|
|
853
|
+
batchSubfolderChecklistEl.append(label);
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function syncBatchSelectionUI() {
|
|
858
|
+
renderBatchSubfolderChecklist();
|
|
859
|
+
recomputeStepFiveState();
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function updateSubfolderControlVisibility() {
|
|
863
|
+
const isBatchMode = generationState.mode === "batch";
|
|
864
|
+
subfolderSingleControlsEl?.classList.toggle("hidden", isBatchMode);
|
|
865
|
+
subfolderSingleControlsEl?.toggleAttribute("hidden", isBatchMode);
|
|
866
|
+
subfolderBatchControlsEl?.classList.toggle("hidden", !isBatchMode);
|
|
867
|
+
subfolderBatchControlsEl?.toggleAttribute("hidden", !isBatchMode);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function applyGenerationMode() {
|
|
871
|
+
const config = getSelectedGenerationModeConfig();
|
|
872
|
+
const outputFolderIsReadOnly = true;
|
|
873
|
+
|
|
874
|
+
inputFolderLabelEl.textContent = config.inputLabel;
|
|
875
|
+
outputFolderLabelEl.textContent = config.outputLabel;
|
|
876
|
+
modeHelpEl.textContent = config.helpText;
|
|
877
|
+
modeStepCopyEl.textContent = config.modeStepCopy;
|
|
878
|
+
folderStepCopyEl.textContent = config.folderStepCopy;
|
|
879
|
+
outputStepCopyEl.textContent = config.outputStepCopy;
|
|
880
|
+
generateButton.textContent = getGenerationButtonLabel();
|
|
881
|
+
outputFolderEl.readOnly = outputFolderIsReadOnly;
|
|
882
|
+
outputFolderEl.setAttribute("aria-readonly", String(outputFolderIsReadOnly));
|
|
883
|
+
updateSubfolderControlVisibility();
|
|
884
|
+
updatePreviewVideoControls();
|
|
885
|
+
recomputeStepFiveState();
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function resetScanReadyState() {
|
|
889
|
+
generationState.singleFolderReady = false;
|
|
890
|
+
if (generationState.mode === "batch") {
|
|
891
|
+
resetBatchValidationState();
|
|
892
|
+
}
|
|
893
|
+
recomputeStepFiveState();
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function resetSingleFolderSelectionState() {
|
|
897
|
+
setScannedRootFolder("");
|
|
898
|
+
setOutputDerivationSourceFolder("");
|
|
899
|
+
clearSelectedWorkingFolder();
|
|
900
|
+
setAvailableSubfolders([]);
|
|
901
|
+
setSelectedBatchFolders([]);
|
|
902
|
+
resetBatchValidationState();
|
|
903
|
+
hideSubfolderPicker();
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function setScanReadyState(isReady) {
|
|
907
|
+
generationState.singleFolderReady = Boolean(isReady);
|
|
908
|
+
if (generationState.mode === "batch") {
|
|
909
|
+
setBatchParentScanComplete(true);
|
|
910
|
+
}
|
|
911
|
+
recomputeStepFiveState();
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function hideSubfolderPicker({ preserveOptions = false } = {}) {
|
|
915
|
+
subfolderSectionEl.hidden = true;
|
|
916
|
+
subfolderSectionEl.classList.add("hidden");
|
|
917
|
+
|
|
918
|
+
if (!preserveOptions) {
|
|
919
|
+
subfolderPickerEl.innerHTML = "";
|
|
920
|
+
batchSubfolderChecklistEl.innerHTML = "";
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
updateBatchSelectionSummary();
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function normalizeSubfolderOption(subfolder) {
|
|
927
|
+
if (subfolder && typeof subfolder === "object") {
|
|
928
|
+
const fullPath = typeof subfolder.full_path === "string" ? subfolder.full_path : "";
|
|
929
|
+
const displayName = typeof subfolder.display_name === "string" && subfolder.display_name.trim()
|
|
930
|
+
? subfolder.display_name.trim()
|
|
931
|
+
: fullPath;
|
|
932
|
+
|
|
933
|
+
if (fullPath) {
|
|
934
|
+
return { displayName, fullPath };
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
if (typeof subfolder === "string" && subfolder) {
|
|
939
|
+
return { displayName: subfolder, fullPath: subfolder };
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
return null;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function showSubfolderPicker(availableSubfolders = generationState.availableSubfolders, selectedSubfolder = "") {
|
|
946
|
+
const normalizedSubfolders = Array.isArray(availableSubfolders)
|
|
947
|
+
? availableSubfolders.map(normalizeSubfolderOption).filter(Boolean)
|
|
948
|
+
: [];
|
|
949
|
+
const preferredSelection = typeof selectedSubfolder === "string" ? selectedSubfolder : "";
|
|
950
|
+
const preservedBatchSelection = normalizeSelectedBatchFolders(
|
|
951
|
+
generationState.selectedBatchFolders,
|
|
952
|
+
normalizedSubfolders,
|
|
953
|
+
);
|
|
954
|
+
|
|
955
|
+
generationState.availableSubfolders = normalizedSubfolders;
|
|
956
|
+
generationState.selectedBatchFolders = preservedBatchSelection;
|
|
957
|
+
subfolderPickerEl.innerHTML = "";
|
|
958
|
+
batchSubfolderChecklistEl.innerHTML = "";
|
|
959
|
+
|
|
960
|
+
normalizedSubfolders.forEach(({ displayName, fullPath }) => {
|
|
961
|
+
const singleOption = document.createElement("option");
|
|
962
|
+
singleOption.value = fullPath;
|
|
963
|
+
singleOption.textContent = displayName;
|
|
964
|
+
singleOption.selected = preferredSelection ? fullPath === preferredSelection : false;
|
|
965
|
+
subfolderPickerEl.append(singleOption);
|
|
966
|
+
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
if (preferredSelection && normalizedSubfolders.some(({ fullPath }) => fullPath === preferredSelection)) {
|
|
970
|
+
subfolderPickerEl.value = preferredSelection;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if (!preferredSelection && normalizedSubfolders.length > 0) {
|
|
974
|
+
subfolderPickerEl.value = normalizedSubfolders[0].fullPath;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
syncBatchSelectionUI();
|
|
978
|
+
updateSubfolderControlVisibility();
|
|
979
|
+
subfolderSectionEl.hidden = false;
|
|
980
|
+
subfolderSectionEl.classList.remove("hidden");
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function escapeHtml(value) {
|
|
984
|
+
return String(value)
|
|
985
|
+
.replaceAll("&", "&")
|
|
986
|
+
.replaceAll("<", "<")
|
|
987
|
+
.replaceAll(">", ">")
|
|
988
|
+
.replaceAll('"', """)
|
|
989
|
+
.replaceAll("'", "'");
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function pluralize(count, noun) {
|
|
993
|
+
return `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// Long lists (every file of a big bundle) end in "and N more" rather than
|
|
997
|
+
// stretching the card far past the screen.
|
|
998
|
+
function renderListSection(title, items, emptyMessage = "", { limit = 8 } = {}) {
|
|
999
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
1000
|
+
return emptyMessage
|
|
1001
|
+
? `<section class="result-section"><h3>${escapeHtml(title)}</h3><p class="result-empty">${escapeHtml(emptyMessage)}</p></section>`
|
|
1002
|
+
: "";
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
const shownItems = items.slice(0, limit);
|
|
1006
|
+
const hiddenCount = items.length - shownItems.length;
|
|
1007
|
+
const listItems = shownItems
|
|
1008
|
+
.map((item) => `<li class="result-list__item"><span>${escapeHtml(item)}</span></li>`)
|
|
1009
|
+
.join("")
|
|
1010
|
+
+ (hiddenCount > 0 ? `<li class="result-list__more">and ${hiddenCount} more</li>` : "");
|
|
1011
|
+
|
|
1012
|
+
return `
|
|
1013
|
+
<section class="result-section">
|
|
1014
|
+
<h3>${escapeHtml(title)}</h3>
|
|
1015
|
+
<ul class="result-list">${listItems}</ul>
|
|
1016
|
+
</section>
|
|
1017
|
+
`;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
function renderPathSection(title, pathValue, copyLabel) {
|
|
1021
|
+
if (!pathValue) {
|
|
1022
|
+
return "";
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
return `
|
|
1026
|
+
<section class="result-section">
|
|
1027
|
+
<h3>${escapeHtml(title)}</h3>
|
|
1028
|
+
<div class="result-path">
|
|
1029
|
+
<code>${escapeHtml(pathValue)}</code>
|
|
1030
|
+
<button class="copy-button" type="button" data-copy="${escapeHtml(pathValue)}" aria-label="${escapeHtml(copyLabel)}">Copy</button>
|
|
1031
|
+
</div>
|
|
1032
|
+
</section>
|
|
1033
|
+
`;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function renderSummaryRows(rows) {
|
|
1037
|
+
const populatedRows = rows.filter((row) => row.value !== null && row.value !== undefined && row.value !== "");
|
|
1038
|
+
|
|
1039
|
+
if (populatedRows.length === 0) {
|
|
1040
|
+
return "";
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
return `
|
|
1044
|
+
<dl class="result-summary-grid">
|
|
1045
|
+
${populatedRows
|
|
1046
|
+
.map(
|
|
1047
|
+
(row) => `
|
|
1048
|
+
<div class="result-summary-row">
|
|
1049
|
+
<dt>${escapeHtml(row.label)}</dt>
|
|
1050
|
+
<dd>${escapeHtml(row.value)}</dd>
|
|
1051
|
+
</div>
|
|
1052
|
+
`,
|
|
1053
|
+
)
|
|
1054
|
+
.join("")}
|
|
1055
|
+
</dl>
|
|
1056
|
+
`;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function renderResultCard(container, {
|
|
1060
|
+
tone = "neutral",
|
|
1061
|
+
badgeLabel = "Status",
|
|
1062
|
+
message = "",
|
|
1063
|
+
notes = [],
|
|
1064
|
+
summaryRows = [],
|
|
1065
|
+
sections = [],
|
|
1066
|
+
} = {}) {
|
|
1067
|
+
const noteItems = notes
|
|
1068
|
+
.filter(Boolean)
|
|
1069
|
+
.map((note) => `<li>${escapeHtml(note)}</li>`)
|
|
1070
|
+
.join("");
|
|
1071
|
+
|
|
1072
|
+
container.innerHTML = `
|
|
1073
|
+
<div class="result-card__header">
|
|
1074
|
+
<span class="result-badge result-badge--${escapeHtml(tone)}">${escapeHtml(badgeLabel)}</span>
|
|
1075
|
+
<p class="result-message">${escapeHtml(message)}</p>
|
|
1076
|
+
</div>
|
|
1077
|
+
${noteItems ? `<ul class="result-notes">${noteItems}</ul>` : ""}
|
|
1078
|
+
${renderSummaryRows(summaryRows)}
|
|
1079
|
+
<div class="result-sections">${sections.filter(Boolean).join("")}</div>
|
|
1080
|
+
`;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function describeFoundImages(imageCount, fullSheets) {
|
|
1084
|
+
if (fullSheets === 0) {
|
|
1085
|
+
return `Found ${pluralize(imageCount, "image")}: not enough to fill a sheet of ${IMAGES_PER_SHEET}.`;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
const leftOver = imageCount - fullSheets * IMAGES_PER_SHEET;
|
|
1089
|
+
return `Found ${pluralize(imageCount, "image")}: enough for ${pluralize(fullSheets, "full sheet")}${leftOver > 0 ? `, with ${leftOver} left over` : ""}.`;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// Said before anything is made, because by default left-over images are skipped.
|
|
1093
|
+
function describeLeftOverTip(imageCount, fullSheets) {
|
|
1094
|
+
if (imageCount - fullSheets * IMAGES_PER_SHEET <= 0) {
|
|
1095
|
+
return "";
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
return fullSheets === 0
|
|
1099
|
+
? `Tick ${PART_FILLED_OPTION} in step 3 to make a sheet anyway.`
|
|
1100
|
+
: `Left-over images are skipped unless you tick ${PART_FILLED_OPTION} in step 3.`;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function renderScanSummary(result) {
|
|
1104
|
+
const imageCount = result.image_count ?? 0;
|
|
1105
|
+
const fullSheets = result.estimated_pages ?? 0;
|
|
1106
|
+
const availableSubfolders = Array.isArray(result.available_subfolders)
|
|
1107
|
+
? result.available_subfolders.map(normalizeSubfolderOption).filter(Boolean)
|
|
1108
|
+
: [];
|
|
1109
|
+
const folderNames = availableSubfolders.map(({ displayName }) => displayName);
|
|
1110
|
+
const sampleFiles = Array.isArray(result.sample_files) ? result.sample_files : [];
|
|
1111
|
+
const isBatchMode = generationState.mode === "batch";
|
|
1112
|
+
|
|
1113
|
+
// A folder of bundles has no images of its own: finding bundles inside is
|
|
1114
|
+
// the good outcome there, not a problem.
|
|
1115
|
+
if (isBatchMode && availableSubfolders.length > 0) {
|
|
1116
|
+
renderResultCard(scanResultEl, {
|
|
1117
|
+
tone: "success",
|
|
1118
|
+
badgeLabel: "Bundles found",
|
|
1119
|
+
message: `Found ${pluralize(availableSubfolders.length, "bundle folder")}.`,
|
|
1120
|
+
sections: [renderListSection("Bundles", folderNames)],
|
|
1121
|
+
});
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// Each mode pointed at the other mode's kind of folder: say which mode fits.
|
|
1126
|
+
if (!isBatchMode && !result.valid && availableSubfolders.length > 0) {
|
|
1127
|
+
renderResultCard(scanResultEl, {
|
|
1128
|
+
tone: "warning",
|
|
1129
|
+
badgeLabel: "Pick a folder",
|
|
1130
|
+
message: `No images here, but there ${availableSubfolders.length === 1 ? "is 1 folder" : `are ${availableSubfolders.length} folders`} inside.`,
|
|
1131
|
+
notes: ["Pick one in step 1, or switch to “Many bundles” to make images for each of them."],
|
|
1132
|
+
sections: [renderListSection("Folders inside", folderNames)],
|
|
1133
|
+
});
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
if (isBatchMode && result.valid) {
|
|
1138
|
+
renderResultCard(scanResultEl, {
|
|
1139
|
+
tone: "warning",
|
|
1140
|
+
badgeLabel: "Needs attention",
|
|
1141
|
+
message: `This folder has ${pluralize(imageCount, "image")} but no bundle folders inside.`,
|
|
1142
|
+
notes: ["Switch to “One bundle” to make images from it."],
|
|
1143
|
+
});
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
if (!result.valid) {
|
|
1148
|
+
renderResultCard(scanResultEl, {
|
|
1149
|
+
tone: "warning",
|
|
1150
|
+
badgeLabel: "Needs attention",
|
|
1151
|
+
message: result.message || "This folder can't be used yet.",
|
|
1152
|
+
});
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
renderResultCard(scanResultEl, {
|
|
1157
|
+
tone: "success",
|
|
1158
|
+
badgeLabel: "Ready",
|
|
1159
|
+
message: describeFoundImages(imageCount, fullSheets),
|
|
1160
|
+
notes: [describeLeftOverTip(imageCount, fullSheets)],
|
|
1161
|
+
sections: [renderListSection("First few files", sampleFiles)],
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
function describeLeftOutImages(count) {
|
|
1166
|
+
if (!(count > 0)) {
|
|
1167
|
+
return "";
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
return count === 1
|
|
1171
|
+
? `1 image didn't fill a sheet, so it was left out. Tick ${PART_FILLED_OPTION} to keep it.`
|
|
1172
|
+
: `${count} images didn't fill a sheet, so they were left out. Tick ${PART_FILLED_OPTION} to keep them.`;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// A video that fails does not fail the sheets: the server explains it at the
|
|
1176
|
+
// end of its message, which is the part worth showing.
|
|
1177
|
+
function describeVideoProblem(message) {
|
|
1178
|
+
const start = typeof message === "string" ? message.indexOf("Preview video was requested") : -1;
|
|
1179
|
+
return start >= 0 ? message.slice(start) : "The preview video couldn't be made.";
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function renderBatchFolderOutcomeSection(results, kind) {
|
|
1183
|
+
if (!Array.isArray(results) || results.length === 0) {
|
|
1184
|
+
return "";
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
const requestedPreviewVideo = kind !== "cover" && Boolean(createPreviewVideoEl?.checked);
|
|
1188
|
+
const lines = results.map((result) => {
|
|
1189
|
+
const folderName = result.folder_name || result.displayName || fileName(result.output_path || "") || "Bundle";
|
|
1190
|
+
if (!result.success) {
|
|
1191
|
+
return `${folderName}: ${result.message || "failed."}`;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
if (kind === "cover") {
|
|
1195
|
+
const outputFiles = Array.isArray(result.output_files) ? result.output_files : [];
|
|
1196
|
+
return `${folderName}: ${outputFiles.length > 0 ? outputFiles.map(fileName).join(", ") : "cover made"}`;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
if ((result.pages_generated ?? 0) === 0) {
|
|
1200
|
+
return `${folderName}: no sheets made. ${result.message || ""}`.trim();
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
const sheets = pluralize(result.pages_generated, "sheet");
|
|
1204
|
+
if (result.video_created) {
|
|
1205
|
+
return `${folderName}: ${sheets} and a video`;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
return requestedPreviewVideo ? `${folderName}: ${sheets}, but no video. ${describeVideoProblem(result.message)}` : `${folderName}: ${sheets}`;
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
return renderListSection("Each bundle", lines, "", { limit: Infinity });
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
function renderBatchSummary(result, kind) {
|
|
1215
|
+
const bundleResults = Array.isArray(result.bundle_results) ? result.bundle_results : [];
|
|
1216
|
+
const isCover = kind === "cover";
|
|
1217
|
+
// A bundle counts as done when something was made for it.
|
|
1218
|
+
const doneResults = bundleResults.filter((bundleResult) => bundleResult?.success && (isCover || (bundleResult.pages_generated ?? 0) > 0));
|
|
1219
|
+
const doneCount = doneResults.length;
|
|
1220
|
+
const totalCount = result.processed_folders ?? bundleResults.length;
|
|
1221
|
+
const outputName = isCover ? "covers" : "preview sheets";
|
|
1222
|
+
const requestedPreviewVideo = !isCover && Boolean(createPreviewVideoEl?.checked);
|
|
1223
|
+
const sum = (field) => doneResults.reduce((total, bundleResult) => total + (bundleResult[field] ?? 0), 0);
|
|
1224
|
+
|
|
1225
|
+
let badgeLabel = "Done";
|
|
1226
|
+
let message = `Made ${outputName} for ${pluralize(doneCount, "bundle")}.`;
|
|
1227
|
+
if (doneCount === 0) {
|
|
1228
|
+
badgeLabel = "Nothing made";
|
|
1229
|
+
message = `Couldn't make ${outputName} for ${totalCount === 1 ? "the bundle" : `any of the ${totalCount} bundles`}.`;
|
|
1230
|
+
} else if (doneCount < totalCount) {
|
|
1231
|
+
badgeLabel = "Partly done";
|
|
1232
|
+
message = `Made ${outputName} for ${doneCount} of ${pluralize(totalCount, "bundle")}.`;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
renderResultCard(generationResultEl, {
|
|
1236
|
+
tone: doneCount > 0 && doneCount === totalCount ? "success" : "warning",
|
|
1237
|
+
badgeLabel,
|
|
1238
|
+
message,
|
|
1239
|
+
summaryRows: isCover
|
|
1240
|
+
? [
|
|
1241
|
+
{ label: "Bundles done", value: `${doneCount} of ${totalCount}` },
|
|
1242
|
+
{ label: "Cover files", value: pluralize(doneResults.flatMap((bundleResult) => bundleResult.output_files ?? []).length, "file") },
|
|
1243
|
+
]
|
|
1244
|
+
: [
|
|
1245
|
+
{ label: "Bundles done", value: `${doneCount} of ${totalCount}` },
|
|
1246
|
+
{ label: "Sheets", value: pluralize(sum("pages_generated"), "sheet") },
|
|
1247
|
+
{ label: "Images used", value: pluralize(sum("total_images"), "image") },
|
|
1248
|
+
{ label: "Videos", value: requestedPreviewVideo ? pluralize(bundleResults.filter((bundleResult) => bundleResult.video_created).length, "video") : null },
|
|
1249
|
+
],
|
|
1250
|
+
sections: [
|
|
1251
|
+
renderBatchFolderOutcomeSection(bundleResults, kind),
|
|
1252
|
+
renderPathSection("Saved in", result.created_output_root_path || "", "Copy the folder path"),
|
|
1253
|
+
],
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
function renderGenerationSummary(result, kind = "sheets") {
|
|
1258
|
+
if (Array.isArray(result.bundle_results)) {
|
|
1259
|
+
renderBatchSummary(result, kind);
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
const outputFiles = Array.isArray(result.output_files) ? result.output_files : [];
|
|
1264
|
+
const pagesGenerated = result.pages_generated ?? 0;
|
|
1265
|
+
const totalImages = result.total_images ?? 0;
|
|
1266
|
+
const outputPath = result.output_path || "";
|
|
1267
|
+
const pageSizeRow = { label: "Page size", value: result.page_geometry || null };
|
|
1268
|
+
|
|
1269
|
+
if (kind === "cover") {
|
|
1270
|
+
const coverMade = outputFiles.length > 0;
|
|
1271
|
+
renderResultCard(generationResultEl, {
|
|
1272
|
+
tone: coverMade ? "success" : "warning",
|
|
1273
|
+
badgeLabel: coverMade ? "Done" : "Nothing made",
|
|
1274
|
+
message: coverMade ? `Made a cover from ${pluralize(totalImages, "image")}.` : result.message || "No cover was made.",
|
|
1275
|
+
summaryRows: [pageSizeRow],
|
|
1276
|
+
sections: [
|
|
1277
|
+
renderListSection("Files", outputFiles.map(fileName)),
|
|
1278
|
+
renderPathSection("Saved in", outputPath, "Copy the folder path"),
|
|
1279
|
+
],
|
|
1280
|
+
});
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
const videoFilePath = result.video_file || "";
|
|
1285
|
+
const requestedPreviewVideo = Boolean(createPreviewVideoEl?.checked);
|
|
1286
|
+
const notes = [
|
|
1287
|
+
describeLeftOutImages(result.images_dropped ?? 0),
|
|
1288
|
+
result.images_skipped > 0 ? `Skipped ${pluralize(result.images_skipped, "file")} that couldn't be read.` : "",
|
|
1289
|
+
result.backgrounds_cleaned > 0 ? `Cleaned the background on ${pluralize(result.backgrounds_cleaned, "image")}.` : "",
|
|
1290
|
+
];
|
|
1291
|
+
if (requestedPreviewVideo && !videoFilePath) {
|
|
1292
|
+
notes.push(pagesGenerated > 0 ? describeVideoProblem(result.message) : "With no sheets, there was nothing to put in a video.");
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
renderResultCard(generationResultEl, {
|
|
1296
|
+
tone: pagesGenerated > 0 ? "success" : "warning",
|
|
1297
|
+
badgeLabel: pagesGenerated > 0 ? "Done" : "Nothing made",
|
|
1298
|
+
message: pagesGenerated > 0
|
|
1299
|
+
? `Made ${pluralize(pagesGenerated, "preview sheet")} from ${pluralize(totalImages, "image")}${videoFilePath ? ", plus a preview video" : ""}.`
|
|
1300
|
+
: "No sheets were made.",
|
|
1301
|
+
notes,
|
|
1302
|
+
summaryRows: pagesGenerated > 0 ? [pageSizeRow] : [],
|
|
1303
|
+
sections: [
|
|
1304
|
+
renderListSection("Files", outputFiles.map(fileName)),
|
|
1305
|
+
renderPathSection("Preview video", videoFilePath, "Copy the video path"),
|
|
1306
|
+
pagesGenerated > 0 ? renderPathSection("Saved in", outputPath, "Copy the folder path") : "",
|
|
1307
|
+
],
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
function renderPendingResult(container, badgeLabel, message) {
|
|
1312
|
+
renderResultCard(container, {
|
|
1313
|
+
tone: "neutral",
|
|
1314
|
+
badgeLabel,
|
|
1315
|
+
message,
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
function renderFailedResult(container, message) {
|
|
1320
|
+
renderResultCard(container, {
|
|
1321
|
+
tone: "warning",
|
|
1322
|
+
badgeLabel: "Failed",
|
|
1323
|
+
message,
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
// Lets a scan that never got an answer flow through the normal "not ready" path.
|
|
1328
|
+
function buildFailedScanResult(message) {
|
|
1329
|
+
return {
|
|
1330
|
+
valid: false,
|
|
1331
|
+
image_count: 0,
|
|
1332
|
+
sample_files: [],
|
|
1333
|
+
estimated_pages: 0,
|
|
1334
|
+
available_subfolders: [],
|
|
1335
|
+
message,
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
generationModeEls.forEach((modeEl) => {
|
|
1340
|
+
modeEl.addEventListener("change", (event) => {
|
|
1341
|
+
generationState.mode = event.target.value;
|
|
1342
|
+
resetSingleFolderSelectionState();
|
|
1343
|
+
syncDerivedOutputPath();
|
|
1344
|
+
resetScanReadyState();
|
|
1345
|
+
renderPendingResult(scanResultEl, "Waiting", generationState.mode === "batch"
|
|
1346
|
+
? "Scan the folder of bundles to continue."
|
|
1347
|
+
: "Scan the folder to continue.");
|
|
1348
|
+
renderPendingResult(generationResultEl, "Waiting", "Nothing made yet. Your files will be listed here.");
|
|
1349
|
+
applyGenerationMode();
|
|
1350
|
+
});
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
backgroundCleanModeEl?.addEventListener("change", updateBackgroundCleanControls);
|
|
1354
|
+
|
|
1355
|
+
// Paths are long and fiddly to select by hand.
|
|
1356
|
+
document.addEventListener("click", async (event) => {
|
|
1357
|
+
const button = event.target.closest?.(".copy-button");
|
|
1358
|
+
if (!button) {
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
const idleText = "Copy";
|
|
1363
|
+
try {
|
|
1364
|
+
await navigator.clipboard.writeText(button.dataset.copy || "");
|
|
1365
|
+
button.textContent = "Copied!";
|
|
1366
|
+
} catch {
|
|
1367
|
+
// No clipboard on plain http:// other than localhost: select the path
|
|
1368
|
+
// instead, ready to copy.
|
|
1369
|
+
const pathEl = button.parentElement?.querySelector("code");
|
|
1370
|
+
if (pathEl) {
|
|
1371
|
+
window.getSelection()?.selectAllChildren(pathEl);
|
|
1372
|
+
}
|
|
1373
|
+
button.textContent = "Selected";
|
|
1374
|
+
}
|
|
1375
|
+
setTimeout(() => {
|
|
1376
|
+
button.textContent = idleText;
|
|
1377
|
+
}, 1800);
|
|
1378
|
+
});
|
|
1379
|
+
|
|
1380
|
+
createPreviewVideoEl?.addEventListener("change", () => {
|
|
1381
|
+
updatePreviewVideoControls();
|
|
1382
|
+
recomputeStepFiveState();
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
async function scanCurrentFolder({ preserveSubfolderPicker = false } = {}) {
|
|
1386
|
+
const selectedSubfolder = preserveSubfolderPicker && generationState.mode === "single" ? generationState.selectedWorkingFolder : "";
|
|
1387
|
+
const selectedBatchFolders = preserveSubfolderPicker && generationState.mode === "batch" ? [...generationState.selectedBatchFolders] : [];
|
|
1388
|
+
|
|
1389
|
+
if (!preserveSubfolderPicker) {
|
|
1390
|
+
clearSelectedWorkingFolder();
|
|
1391
|
+
setAvailableSubfolders([]);
|
|
1392
|
+
setSelectedBatchFolders([]);
|
|
1393
|
+
hideSubfolderPicker({ preserveOptions: false });
|
|
1394
|
+
resetCoverCollageIncludeList();
|
|
1395
|
+
}
|
|
1396
|
+
resetBatchValidationState();
|
|
1397
|
+
resetScanReadyState();
|
|
1398
|
+
renderPendingResult(scanResultEl, "Scanning", generationState.mode === "batch" ? "Looking for bundle folders…" : "Counting images…");
|
|
1399
|
+
|
|
1400
|
+
const { ok, data, errorMessage } = await postJson("/api/jobs/scan-folder", { folder_path: readMainInputFolderPath() });
|
|
1401
|
+
const result = ok ? data : buildFailedScanResult(errorMessage);
|
|
1402
|
+
renderScanSummary(result);
|
|
1403
|
+
const scannedFolderPath = readMainInputFolderPath();
|
|
1404
|
+
setScannedRootFolder(scannedFolderPath);
|
|
1405
|
+
setOutputDerivationSourceFolder(scannedFolderPath);
|
|
1406
|
+
syncDerivedOutputPath();
|
|
1407
|
+
setScanReadyState(Boolean(result.valid));
|
|
1408
|
+
if (generationState.mode === "batch") {
|
|
1409
|
+
setBatchParentScanComplete(true);
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
if (Array.isArray(result.available_subfolders) && result.available_subfolders.length > 0) {
|
|
1413
|
+
// Making images for every bundle is the usual case, so a fresh list starts
|
|
1414
|
+
// with all of them picked; a re-scan keeps the user's picks.
|
|
1415
|
+
generationState.selectedBatchFolders = generationState.mode === "batch" && selectedBatchFolders.length === 0
|
|
1416
|
+
? result.available_subfolders.map(normalizeSubfolderOption).filter(Boolean).map(({ fullPath }) => fullPath)
|
|
1417
|
+
: selectedBatchFolders;
|
|
1418
|
+
showSubfolderPicker(result.available_subfolders, selectedSubfolder);
|
|
1419
|
+
} else {
|
|
1420
|
+
clearSelectedWorkingFolder();
|
|
1421
|
+
setAvailableSubfolders([]);
|
|
1422
|
+
setSelectedBatchFolders([]);
|
|
1423
|
+
hideSubfolderPicker({ preserveOptions: false });
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
return result;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
scanButton?.addEventListener("click", async () => {
|
|
1430
|
+
await runExclusive(async () => {
|
|
1431
|
+
await scanCurrentFolder({ preserveSubfolderPicker: true });
|
|
1432
|
+
// Check the picked bundles straight away, so making images for all the
|
|
1433
|
+
// bundles in a folder takes one click.
|
|
1434
|
+
if (generationState.mode === "batch" && hasSelectedBatchFolders()) {
|
|
1435
|
+
await validateSelectedBatchFolders();
|
|
1436
|
+
}
|
|
1437
|
+
});
|
|
1438
|
+
});
|
|
1439
|
+
|
|
1440
|
+
// The check card describes the picked bundles, so it follows the picks:
|
|
1441
|
+
// unticking the bundle that failed turns it ready without a new check.
|
|
1442
|
+
function renderBatchCheckSummary() {
|
|
1443
|
+
const selectedPaths = getSelectedBatchFolderPaths();
|
|
1444
|
+
if (selectedPaths.length === 0) {
|
|
1445
|
+
renderPendingResult(scanResultEl, "Pick bundles", "Pick at least one bundle to check.");
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
const selectedPathSet = new Set(selectedPaths);
|
|
1450
|
+
const results = generationState.batchValidationResults.filter((result) => selectedPathSet.has(result.folderPath));
|
|
1451
|
+
const readyResults = results.filter((result) => result.valid);
|
|
1452
|
+
const invalidResults = results.filter((result) => !result.valid);
|
|
1453
|
+
const uncheckedCount = selectedPaths.length - results.length;
|
|
1454
|
+
|
|
1455
|
+
let tone = "success";
|
|
1456
|
+
let badgeLabel = "Ready";
|
|
1457
|
+
let message = readyResults.length === 1 ? "The picked bundle is ready." : `All ${readyResults.length} picked bundles are ready.`;
|
|
1458
|
+
if (invalidResults.length > 0) {
|
|
1459
|
+
tone = "warning";
|
|
1460
|
+
badgeLabel = "Needs attention";
|
|
1461
|
+
message = `${invalidResults.length} of ${pluralize(selectedPaths.length, "picked bundle")} ${invalidResults.length === 1 ? "needs" : "need"} attention.`;
|
|
1462
|
+
} else if (uncheckedCount > 0) {
|
|
1463
|
+
tone = "neutral";
|
|
1464
|
+
badgeLabel = "Not checked";
|
|
1465
|
+
message = `${pluralize(uncheckedCount, "picked bundle")} ${uncheckedCount === 1 ? "hasn't" : "haven't"} been checked yet.`;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
renderResultCard(scanResultEl, {
|
|
1469
|
+
tone,
|
|
1470
|
+
badgeLabel,
|
|
1471
|
+
message,
|
|
1472
|
+
notes: [invalidResults.length > 0 && invalidResults.length < selectedPaths.length
|
|
1473
|
+
? "Untick the ones that need attention to go ahead with the rest."
|
|
1474
|
+
: ""],
|
|
1475
|
+
summaryRows: [
|
|
1476
|
+
{ label: "Ready", value: pluralize(readyResults.length, "bundle") },
|
|
1477
|
+
{ label: "Images", value: pluralize(readyResults.reduce((sum, result) => sum + (result.image_count ?? 0), 0), "image") },
|
|
1478
|
+
{ label: "Full sheets", value: pluralize(readyResults.reduce((sum, result) => sum + (result.estimated_pages ?? 0), 0), "sheet") },
|
|
1479
|
+
],
|
|
1480
|
+
sections: [
|
|
1481
|
+
renderListSection(
|
|
1482
|
+
"Ready",
|
|
1483
|
+
readyResults.map((result) => `${result.displayName} (${pluralize(result.image_count ?? 0, "image")})`),
|
|
1484
|
+
"None of them are ready yet.",
|
|
1485
|
+
),
|
|
1486
|
+
renderListSection(
|
|
1487
|
+
"Need attention",
|
|
1488
|
+
invalidResults.map((result) => `${result.displayName}: ${result.message || "Not ready"}`),
|
|
1489
|
+
"",
|
|
1490
|
+
{ limit: Infinity },
|
|
1491
|
+
),
|
|
1492
|
+
],
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
async function validateSelectedBatchFolders() {
|
|
1497
|
+
if (generationState.mode !== "batch") {
|
|
1498
|
+
return [];
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
const selectedBatchFolders = getSelectedBatchFolderPaths();
|
|
1502
|
+
if (!generationState.batchParentScanComplete) {
|
|
1503
|
+
renderPendingResult(scanResultEl, "Scan first", "Scan the folder of bundles first.");
|
|
1504
|
+
return [];
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
if (selectedBatchFolders.length === 0) {
|
|
1508
|
+
setBatchValidationResults([]);
|
|
1509
|
+
recomputeStepFiveState();
|
|
1510
|
+
renderPendingResult(scanResultEl, "Pick bundles", "Pick at least one bundle to check.");
|
|
1511
|
+
return [];
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
renderPendingResult(scanResultEl, "Scanning", `Checking ${pluralize(selectedBatchFolders.length, "bundle")}…`);
|
|
1515
|
+
|
|
1516
|
+
const results = await Promise.all(selectedBatchFolders.map(async (folderPath) => {
|
|
1517
|
+
const { ok, data, errorMessage } = await postJson("/api/jobs/scan-folder", { folder_path: folderPath });
|
|
1518
|
+
const result = ok ? data : buildFailedScanResult(errorMessage);
|
|
1519
|
+
return {
|
|
1520
|
+
folderPath,
|
|
1521
|
+
displayName: generationState.availableSubfolders.find((subfolder) => subfolder.fullPath === folderPath)?.displayName || folderPath,
|
|
1522
|
+
...result,
|
|
1523
|
+
};
|
|
1524
|
+
}));
|
|
1525
|
+
|
|
1526
|
+
setBatchValidationResults(results);
|
|
1527
|
+
renderBatchCheckSummary();
|
|
1528
|
+
|
|
1529
|
+
recomputeStepFiveState();
|
|
1530
|
+
return results;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
batchScanButton?.addEventListener("click", async () => {
|
|
1534
|
+
await runExclusive(validateSelectedBatchFolders);
|
|
1535
|
+
});
|
|
1536
|
+
|
|
1537
|
+
subfolderPickerEl?.addEventListener("change", () => {
|
|
1538
|
+
if (generationState.mode === "single") {
|
|
1539
|
+
clearSelectedWorkingFolder();
|
|
1540
|
+
recomputeStepFiveState();
|
|
1541
|
+
}
|
|
1542
|
+
});
|
|
1543
|
+
|
|
1544
|
+
selectAllSubfoldersButton?.addEventListener("click", () => {
|
|
1545
|
+
setSelectedBatchFolders(generationState.availableSubfolders.map(({ fullPath }) => fullPath));
|
|
1546
|
+
applyBatchSelectionChange();
|
|
1547
|
+
syncBatchSelectionUI();
|
|
1548
|
+
});
|
|
1549
|
+
|
|
1550
|
+
clearSubfolderSelectionButton?.addEventListener("click", () => {
|
|
1551
|
+
setSelectedBatchFolders([]);
|
|
1552
|
+
applyBatchSelectionChange();
|
|
1553
|
+
syncBatchSelectionUI();
|
|
1554
|
+
});
|
|
1555
|
+
|
|
1556
|
+
useSubfolderButton?.addEventListener("click", async () => {
|
|
1557
|
+
if (!subfolderPickerEl.value || generationState.busy) {
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
// Scan the chosen folder as if it had been typed in: the scan is what makes
|
|
1562
|
+
// it ready, and the input then shows where the images come from.
|
|
1563
|
+
inputFolderEl.value = subfolderPickerEl.value;
|
|
1564
|
+
resetSingleFolderSelectionState();
|
|
1565
|
+
syncDerivedOutputPath();
|
|
1566
|
+
await runExclusive(() => scanCurrentFolder());
|
|
1567
|
+
});
|
|
1568
|
+
|
|
1569
|
+
async function runCoverCollage() {
|
|
1570
|
+
const selectedTemplateLabel = readSelectedCoverCollageTemplateLabel();
|
|
1571
|
+
|
|
1572
|
+
if (generationState.mode !== "batch") {
|
|
1573
|
+
renderPendingResult(generationResultEl, "Working", `Making a ${selectedTemplateLabel.toLowerCase()} cover…`);
|
|
1574
|
+
revealResult(generationResultEl);
|
|
1575
|
+
|
|
1576
|
+
const { ok, data: result, errorMessage } = await postJson("/api/jobs/generate-cover-collage", readCoverCollagePayload());
|
|
1577
|
+
if (!ok) {
|
|
1578
|
+
renderFailedResult(generationResultEl, errorMessage);
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
renderGenerationSummary(result, "cover");
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
const payload = buildBatchCoverCollagePayload();
|
|
1587
|
+
if (payload.selected_subfolders.length === 0 || !hasValidatedBatchSelection()) {
|
|
1588
|
+
renderPendingResult(generationResultEl, "Check bundles", "Check the picked bundles before making covers.");
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
renderPendingResult(generationResultEl, "Working", `Making ${selectedTemplateLabel.toLowerCase()} covers for ${pluralize(payload.selected_subfolders.length, "bundle")}…`);
|
|
1593
|
+
revealResult(generationResultEl);
|
|
1594
|
+
|
|
1595
|
+
const { ok, data: batchResult, errorMessage } = await postJson("/api/jobs/generate-cover-collage-batch", payload);
|
|
1596
|
+
if (!ok) {
|
|
1597
|
+
renderFailedResult(generationResultEl, errorMessage);
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
renderGenerationSummary(batchResult, "cover");
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
coverCollageButton?.addEventListener("click", async () => {
|
|
1605
|
+
if (coverCollageButton.disabled) {
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
await runExclusive(runCoverCollage);
|
|
1610
|
+
});
|
|
1611
|
+
|
|
1612
|
+
generateButton?.addEventListener("click", async () => {
|
|
1613
|
+
if (generateButton.disabled) {
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
if (!validateVideoGenerationSettings()) {
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
if (!validateOutputQualitySettings()) {
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
if (generationState.mode === "batch") {
|
|
1626
|
+
const { availableCount, selectedCount } = getBatchGenerationSelectionDetails();
|
|
1627
|
+
if (availableCount === 0) {
|
|
1628
|
+
renderPendingResult(generationResultEl, "Scan first", "Scan a folder that has bundle folders inside.");
|
|
1629
|
+
updateBatchSelectionStatus();
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
if (selectedCount === 0) {
|
|
1634
|
+
renderPendingResult(generationResultEl, "Pick bundles", "Pick at least one bundle, then check it.");
|
|
1635
|
+
updateBatchSelectionStatus();
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
if (!generationState.batchValidationReady || !hasValidatedBatchSelection()) {
|
|
1640
|
+
renderPendingResult(generationResultEl, "Check bundles", "Check the picked bundles before making images.");
|
|
1641
|
+
updateBatchSelectionStatus();
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
await runExclusive(runPageGeneration);
|
|
1647
|
+
});
|
|
1648
|
+
|
|
1649
|
+
async function runPageGeneration() {
|
|
1650
|
+
const config = getSelectedGenerationModeConfig();
|
|
1651
|
+
renderPendingResult(generationResultEl, "Working", getGenerationStatusMessage());
|
|
1652
|
+
revealResult(generationResultEl);
|
|
1653
|
+
|
|
1654
|
+
const { ok, data: result, errorMessage } = await postJson(config.endpoint, config.payload());
|
|
1655
|
+
if (!ok) {
|
|
1656
|
+
renderFailedResult(generationResultEl, errorMessage);
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
renderGenerationSummary(result, "sheets");
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
updatePreviewVideoControls();
|
|
1664
|
+
updateBackgroundCleanControls();
|
|
1665
|
+
hideSubfolderPicker();
|
|
1666
|
+
applyGenerationMode();
|
|
1667
|
+
syncDerivedOutputPath();
|
|
1668
|
+
|
|
1669
|
+
inputFolderEl?.addEventListener("input", () => {
|
|
1670
|
+
resetSingleFolderSelectionState();
|
|
1671
|
+
syncDerivedOutputPath();
|
|
1672
|
+
resetScanReadyState();
|
|
1673
|
+
resetCoverCollageIncludeList();
|
|
1674
|
+
});
|
|
1675
|
+
inputFolderEl?.addEventListener("change", () => {
|
|
1676
|
+
resetSingleFolderSelectionState();
|
|
1677
|
+
syncDerivedOutputPath();
|
|
1678
|
+
resetCoverCollageIncludeList();
|
|
1679
|
+
});
|
|
1680
|
+
outputFolderEl?.addEventListener("input", resetScanReadyState);
|