diff --git a/src/network/html/FilesPage.html b/src/network/html/FilesPage.html
index 6e4b1c37..6f63fdda 100644
--- a/src/network/html/FilesPage.html
+++ b/src/network/html/FilesPage.html
@@ -1901,7 +1901,7 @@
↻ Rotation Direction
@@ -2829,13 +2842,13 @@
const isTiny = (dims.width < 200 && dims.height < 200);
// Images that fit screen can only rotate, not split
- const fitsScreen = (dims.width <= 480 && dims.height <= 800);
+ const fitsScreen = (dims.width <= MAX_WIDTH && dims.height <= MAX_HEIGHT);
// Split capability - no upscaling allowed
- // H-Split scales width to 800, so needs width >= 800
- // V-Split scales height to 800, so needs height >= 800
- const canHSplit = dims.width >= 800;
- const canVSplit = dims.height >= 800;
+ // H-Split scales width to MAX_HEIGHT (long edge), so needs width >= MAX_HEIGHT
+ // V-Split scales height to MAX_HEIGHT, so needs height >= MAX_HEIGHT
+ const canHSplit = dims.width >= MAX_HEIGHT;
+ const canVSplit = dims.height >= MAX_HEIGHT;
images.push({
path: path,
@@ -3073,16 +3086,16 @@
if (showSplitLines) {
let finalWidth;
if (state === 1) {
- // H-Split: scale width to 800, rotate, then check width
- const scaledH = Math.round(img.height * (800 / img.width));
+ // H-Split: scale width to MAX_HEIGHT, rotate, then check width
+ const scaledH = Math.round(img.height * (MAX_HEIGHT / img.width));
finalWidth = scaledH; // After rotation, height becomes width
} else {
- // V-Split: scale height to 800, then check width
- finalWidth = Math.round(img.width * (800 / img.height));
+ // V-Split: scale height to MAX_HEIGHT, then check width
+ finalWidth = Math.round(img.width * (MAX_HEIGHT / img.height));
}
- if (finalWidth > 480) {
- const minOverlapPx = Math.round(480 * (OVERLAP_PERCENT / 100));
- const maxStep = 480 - minOverlapPx;
+ if (finalWidth > MAX_WIDTH) {
+ const minOverlapPx = Math.round(MAX_WIDTH * (OVERLAP_PERCENT / 100));
+ const maxStep = MAX_WIDTH - minOverlapPx;
numParts = Math.ceil((finalWidth - minOverlapPx) / maxStep);
if (numParts < 2) numParts = 2;
}
@@ -3459,11 +3472,20 @@
const convertOptions = document.getElementById('convertOptions');
fileInput.classList.toggle('has-files', files.length > 0);
- // Show/hide convert options based on file selection
- if (files.length > 0) {
+ // Show convert options only when at least one selected file is an EPUB.
+ const hasEpub = Array.from(files).some(f => f.name.toLowerCase().endsWith('.epub'));
+ if (files.length > 0 && hasEpub) {
convertOptions.style.display = 'block';
} else {
- clearImagePicker();
+ convertOptions.style.display = 'none';
+ // Clear stale checkbox state so the "Optimize & Upload" button doesn't linger
+ // when the user re-picks a non-EPUB after having ticked Optimize for an EPUB.
+ const cb = document.getElementById('convertBeforeUpload');
+ if (cb && cb.checked) {
+ cb.checked = false;
+ toggleConvertOptions();
+ }
+ if (files.length === 0) clearImagePicker();
}
if (files.length > 0) {
@@ -3472,6 +3494,11 @@
const convertEnabled = document.getElementById('convertBeforeUpload').checked;
if (advancedContent.classList.contains('visible') && files.length === 1 && convertEnabled && files[0].name.toLowerCase().endsWith('.epub')) {
showImagePicker(files[0]).catch(err => console.error('Image picker error:', err));
+ } else {
+ // Selection no longer matches single-EPUB-with-advanced-expanded —
+ // tear down any picker from a previous file so the grid and picker-mode
+ // layout don't linger. clearImagePicker is idempotent.
+ clearImagePicker();
}
// If multiple files with conversion, inform user about batch mode
@@ -3504,14 +3531,24 @@
// EPUB Image Conversion Functions (from Baseline JPEG Converter)
// ============================================================================
+ // Device profiles (short edge × long edge in portrait orientation)
+ const DEVICE_PROFILES = {
+ X4: { width: 480, height: 800, label: 'X4' },
+ X3: { width: 528, height: 792, label: 'X3' },
+ };
+
// Default conversion settings
- const DEFAULT_MAX_WIDTH = 480;
- const DEFAULT_MAX_HEIGHT = 800;
+ const DEFAULT_DEVICE = 'X4';
+ const DEFAULT_MAX_WIDTH = DEVICE_PROFILES[DEFAULT_DEVICE].width;
+ const DEFAULT_MAX_HEIGHT = DEVICE_PROFILES[DEFAULT_DEVICE].height;
const DEFAULT_JPEG_QUALITY = 85;
const DEFAULT_ENABLE_GRAYSCALE = true;
// Note: Overlap is now always centered distribution (min 5%)
// Dynamic conversion settings (updated by UI)
+ let DEVICE_TARGET = 'auto'; // 'auto' | 'X4' | 'X3'
+ let DETECTED_DEVICE = null; // populated from /api/status
+ let ACTIVE_DEVICE = DEFAULT_DEVICE;
let MAX_WIDTH = DEFAULT_MAX_WIDTH;
let MAX_HEIGHT = DEFAULT_MAX_HEIGHT;
let JPEG_QUALITY = DEFAULT_JPEG_QUALITY;
@@ -3569,12 +3606,55 @@
if (response.ok) {
const data = await response.json();
crosspointVersion = data.version || 'Unknown';
+ if (data.deviceType === 'X3' || data.deviceType === 'X4') {
+ DETECTED_DEVICE = data.deviceType;
+ applyDeviceTarget();
+ }
}
} catch (e) {
console.error('Failed to fetch version:', e);
}
}
+ // Resolve DEVICE_TARGET ('auto' | 'X4' | 'X3') to a concrete profile and update UI.
+ function applyDeviceTarget() {
+ const resolved = DEVICE_TARGET === 'auto' ? (DETECTED_DEVICE || DEFAULT_DEVICE) : DEVICE_TARGET;
+ const profile = DEVICE_PROFILES[resolved] || DEVICE_PROFILES[DEFAULT_DEVICE];
+ ACTIVE_DEVICE = resolved;
+ MAX_WIDTH = profile.width;
+ MAX_HEIGHT = profile.height;
+
+ const summary = document.getElementById('convertSizeSummary');
+ if (summary) {
+ summary.textContent = `📏 Max ${profile.width}×${profile.height}px`;
+ }
+ document.querySelectorAll('.device-btn').forEach(btn => {
+ btn.classList.toggle('active', btn.dataset.value === DEVICE_TARGET);
+ });
+ const autoLabel = document.getElementById('deviceAutoLabel');
+ if (autoLabel) {
+ autoLabel.textContent = DETECTED_DEVICE ? `Auto (${DETECTED_DEVICE})` : 'Auto';
+ }
+
+ // Recompute picker classification with new dimensions, then refresh grid.
+ if (Array.isArray(epubImagesCache) && epubImagesCache.length > 0) {
+ for (const img of epubImagesCache) {
+ img.fitsScreen = (img.width <= MAX_WIDTH && img.height <= MAX_HEIGHT);
+ img.canHSplit = img.width >= MAX_HEIGHT;
+ img.canVSplit = img.height >= MAX_HEIGHT;
+ }
+ const pickerSection = document.getElementById('imagePickerSection');
+ if (pickerSection && pickerSection.style.display !== 'none' && typeof renderImageGrid === 'function') {
+ renderImageGrid();
+ }
+ }
+ }
+
+ function setDeviceTarget(value) {
+ DEVICE_TARGET = value;
+ applyDeviceTarget();
+ }
+
// Batch logging system for multiple files
let batchLogEntries = [];
let batchStats = { filesProcessed: 0, filesSucceeded: 0, filesFailed: 0, totalImages: 0, totalSplits: 0, totalFixes: 0, totalErrors: 0, totalOriginalSize: 0, totalNewSize: 0 };
@@ -3731,7 +3811,7 @@
}
// Save current file's log to batch entries
- function saveToFileBatchLog(fileName, succeeded) {
+ function saveToFileBatchLog(fileName, succeeded, originalSize = 0, newSize = 0) {
if (!isBatchMode) return;
const entries = Array.from(logContainer.querySelectorAll('.log-entry'));
@@ -3753,6 +3833,10 @@
batchStats.totalSplits += conversionStats.splits;
batchStats.totalFixes += conversionStats.fixes;
batchStats.totalErrors += conversionStats.errors;
+ // Defaults of 0 keep failure-path callers safe — files that never
+ // produced a converted blob contribute nothing to the totals.
+ batchStats.totalOriginalSize += originalSize;
+ batchStats.totalNewSize += newSize;
// Clear for next file
logContainer.innerHTML = '';
@@ -3785,6 +3869,22 @@
});
});
+ // Aggregate size totals: only emit rows when at least one file was successfully
+ // converted (totalOriginalSize stays 0 for batches where conversion was off or
+ // every file fell back to original upload).
+ const totalSaved = batchStats.totalOriginalSize - batchStats.totalNewSize;
+ const totalSavedPct = batchStats.totalOriginalSize > 0
+ ? ((totalSaved / batchStats.totalOriginalSize) * 100).toFixed(1)
+ : '0.0';
+ const sizeRowsHtml = batchStats.totalOriginalSize > 0 ? `
+
| Total original | ${formatBytes(batchStats.totalOriginalSize)} |
+
| Total optimised | ${formatBytes(batchStats.totalNewSize)} |
+
| Total saved | ${
+ totalSaved > 0
+ ? `${formatBytes(totalSaved)} (${totalSavedPct}%)`
+ : `+${formatBytes(-totalSaved)}`
+ } |
` : '';
+
// Add batch summary
const batchSummaryHtml = `
@@ -3796,7 +3896,7 @@
| Total images processed | ${batchStats.totalImages} |
| Total splits | ${batchStats.totalSplits} |
| Total fixes applied | ${batchStats.totalFixes} |
- ${batchStats.totalErrors > 0 ? `| Total errors | ${batchStats.totalErrors} |
` : ''}
+ ${batchStats.totalErrors > 0 ? `| Total errors | ${batchStats.totalErrors} |
` : ''}${sizeRowsHtml}
| Total time | ${batchTime.toFixed(1)}s |
@@ -4732,8 +4832,6 @@
async function convertEpubFile(file, progressCallback) {
const startTime = Date.now();
const originalSize = file.size;
- let totalImageSize = 0;
- let totalNewSize = 0;
// Initialize logging
clearLog();
@@ -4817,9 +4915,6 @@
const origFormat = path.split('.').pop();
logImage(imgName, meta.origW, meta.origH, origFormat, meta.origSize, meta.finalW, meta.finalH, meta.finalSize, meta.wasSplit, meta.splitCount || 0, parts, meta.imageState || 0);
- totalImageSize += meta.origSize;
- totalNewSize += meta.finalSize;
-
if (parts.length === 1 && parts[0].suffix === '') {
const newPath = renamed[path] || path.replace(/\.[^.]+$/, newExt);
out.file(newPath, parts[0].data, { compression: 'STORE', createFolders: false });
@@ -4868,10 +4963,6 @@
const r2 = fixSvgWrappedImages(t);
if (r2.fixed) { t = r2.c; logFix(`SVG images (${r2.count})`, xhtmlPath.split('/').pop()); }
- for (const [o, n] of Object.entries(renamed)) {
- t = t.split(o.split('/').pop()).join(n.split('/').pop());
- }
-
// Use DOMParser for all img modifications: remove width/height and handle split images
try {
const parser = new DOMParser();
@@ -4887,6 +4978,21 @@
for (const img of allImgElements) {
if (img.hasAttribute('width')) { img.removeAttribute('width'); modified = true; }
if (img.hasAttribute('height')) { img.removeAttribute('height'); modified = true; }
+
+ // Rewrite src for renamed images, handling URL-encoded paths (e.g. %20 spaces).
+ const src = img.getAttribute('src');
+ if (src) {
+ const decodedSrc = decodeHref(src);
+ const resolvedSrc = resolvePath(xhtmlPath, decodedSrc);
+
+ const match = Object.entries(renamed).find(([oldPath]) => resolvedSrc === oldPath);
+
+ if (match) {
+ const [oldPath, newPath] = match;
+ img.setAttribute('src', decodedSrc.replace(oldPath.split('/').pop(), newPath.split('/').pop()));
+ modified = true;
+ }
+ }
}
// Handle split images with path collision prevention
@@ -5060,7 +5166,7 @@
// Log completion
log('Conversion complete!', 'success', 'DONE');
- logSummary(totalImageSize > 0 ? totalImageSize : originalSize, totalNewSize > 0 ? totalNewSize : newSize, timeElapsed);
+ logSummary(originalSize, newSize, timeElapsed);
// Auto-export only if NOT in batch mode (batch mode exports at the end)
if (!isBatchMode && exportLogCheckbox && exportLogCheckbox.checked) {
@@ -5338,6 +5444,8 @@
const needsConversion = isEpub && convertEnabled;
let conversionSucceeded = false;
let conversionFailed = false; // Track if conversion actually failed
+ let convOriginalSize = 0; // Picked-file size; 0 unless conversion succeeded
+ let convNewSize = 0; // Generated blob size; 0 unless conversion succeeded
const uploadMethod = useWebSocket ? 'WS' : 'HTTP';
const stageText = needsConversion ? 'Converting & uploading' : 'Uploading';
@@ -5370,7 +5478,8 @@
// Save file log to batch if in batch mode and this file was converted
// Consider it successful only if conversion didn't fail
if (useBatchLog && needsConversion) {
- saveToFileBatchLog(file.name, !conversionFailed && conversionSucceeded);
+ const ok = !conversionFailed && conversionSucceeded;
+ saveToFileBatchLog(file.name, ok, ok ? convOriginalSize : 0, ok ? convNewSize : 0);
}
currentIndex++;
@@ -5381,7 +5490,9 @@
// Save failed file log to batch if in batch mode
if (useBatchLog && needsConversion) {
logError(`Upload failed: ${error}`);
- saveToFileBatchLog(file.name, false);
+ // Preserve conversion size totals when convert succeeded but upload failed;
+ // otherwise nothing was produced and 0/0 is correct.
+ saveToFileBatchLog(file.name, false, convOriginalSize, convNewSize);
}
failedFiles.push({ name: file.name, error: error, file: originalFile });
@@ -5426,6 +5537,10 @@
showLog();
}
+ // Snapshot before the `file =` reassignment below, which swaps in the
+ // converted blob and makes file.size point at the optimised size.
+ const origFileSize = file.size;
+
try {
const convertedBlob = await convertEpubFile(file, (percent) => {
// Pass current quality setting to converter
@@ -5436,6 +5551,8 @@
file = new File([convertedBlob], file.name, { type: 'application/epub+zip' });
progressFill.style.backgroundColor = '#27ae60'; // Back to green for upload
conversionSucceeded = true;
+ convOriginalSize = origFileSize;
+ convNewSize = convertedBlob.size;
} catch (convError) {
if (operationCancelled) { if (uploadGeneration === myGeneration) restoreAfterCancel(); return; }
console.error('Conversion error:', convError);