@spark-apps/piclet 1.0.3 → 1.0.5
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.
- package/dist/cli.js +493 -192
- package/dist/cli.js.map +1 -1
- package/dist/gui/css/theme.css +24 -10
- package/dist/gui/piclet.html +377 -50
- package/package.json +1 -1
- package/dist/gui/border.html +0 -229
- package/dist/gui/extract-frames.html +0 -156
- package/dist/gui/filter.html +0 -180
- package/dist/gui/iconpack.html +0 -113
- package/dist/gui/makeicon.html +0 -165
- package/dist/gui/recolor.html +0 -243
- package/dist/gui/remove-bg.html +0 -178
- package/dist/gui/rescale.html +0 -195
- package/dist/gui/storepack.html +0 -179
- package/dist/gui/transform.html +0 -202
package/dist/gui/css/theme.css
CHANGED
|
@@ -58,21 +58,35 @@ input[type="range"]::-webkit-slider-thumb{-webkit-appearance:none;width:14px;hei
|
|
|
58
58
|
@keyframes s{to{transform:rotate(360deg)}}
|
|
59
59
|
.ld span{font-size:12px;color:var(--txt3)}
|
|
60
60
|
|
|
61
|
-
/* Log -
|
|
62
|
-
.log{display:none;
|
|
63
|
-
.log.on{display:
|
|
61
|
+
/* Log with preview - 6 lines high with thumbnail */
|
|
62
|
+
.log-container{display:none;gap:8px;height:calc(6 * 1.4em + 16px);flex-shrink:0}
|
|
63
|
+
.log-container.on{display:flex}
|
|
64
|
+
.log{flex:1;font:10px/1.4 Consolas,monospace;background:var(--bg2);border-radius:5px;padding:8px;overflow-y:auto;min-width:0}
|
|
64
65
|
.log p{margin:2px 0}
|
|
65
66
|
.log .i{color:var(--txt3)}
|
|
66
67
|
.log .s{color:var(--ok)}
|
|
67
68
|
.log .e{color:var(--err)}
|
|
69
|
+
.log-preview{width:80px;height:80px;object-fit:contain;background:var(--bg2);border-radius:5px;border:1px solid var(--brd);flex-shrink:0;display:none;align-self:center}
|
|
70
|
+
.log-preview.on{display:block}
|
|
68
71
|
|
|
69
|
-
/* Done
|
|
70
|
-
.
|
|
71
|
-
.
|
|
72
|
-
.
|
|
73
|
-
.
|
|
74
|
-
.
|
|
75
|
-
.
|
|
72
|
+
/* Done container (replaces log area) */
|
|
73
|
+
.done-container{display:none;flex-direction:column;gap:8px;flex:1;min-height:0;overflow:hidden}
|
|
74
|
+
.done-container.on{display:flex}
|
|
75
|
+
.done-preview{display:flex;width:100%;max-height:80%;align-items:center;justify-content:center;background:var(--bg2);border-radius:6px;overflow:hidden;position:relative}
|
|
76
|
+
.done-preview::before{content:'';position:absolute;inset:0;background:repeating-conic-gradient(#1a1a1d 0% 25%, #222 0% 50%) 50%/10px 10px;z-index:0}
|
|
77
|
+
.done-preview img{max-width:100%;height:auto;object-fit:contain;position:relative;z-index:1}
|
|
78
|
+
.done-logs{flex:1 1 auto;font:10px/1.4 Consolas,monospace;background:var(--bg2);border-radius:6px;padding:8px;overflow-y:auto;min-height:60px}
|
|
79
|
+
.done-logs p{margin:2px 0}
|
|
80
|
+
.done-logs .i{color:var(--txt3)}
|
|
81
|
+
.done-logs .s{color:var(--ok)}
|
|
82
|
+
.done-logs .e{color:var(--err)}
|
|
83
|
+
.done-result{display:flex;align-items:center;gap:12px;padding:8px 12px;background:var(--bg2);border-radius:6px;flex:0 0 auto}
|
|
84
|
+
.done-result h4{font-size:13px;margin:0;font-weight:600}
|
|
85
|
+
.done-container.ok .done-result h4{color:var(--ok)}
|
|
86
|
+
.done-container.err .done-result h4{color:var(--err)}
|
|
87
|
+
.done-result p{font-size:11px;color:var(--txt2);margin:0;flex:1}
|
|
88
|
+
.done-btns{display:flex;gap:8px;flex:0 0 auto}
|
|
89
|
+
.done-btns .btn{padding:8px 16px;font-size:11px}
|
|
76
90
|
|
|
77
91
|
/* Warning/info box */
|
|
78
92
|
.warn-box{display:none;font-size:10px;color:#ca8a04;background:rgba(202,138,4,0.1);padding:6px 8px;border-radius:4px;text-align:center}
|
package/dist/gui/piclet.html
CHANGED
|
@@ -162,6 +162,11 @@
|
|
|
162
162
|
.gif-export-btn{display:none;width:100%;padding:8px 4px;font-size:10px;background:var(--acc);border:none;border-radius:4px;color:#000;cursor:pointer;font-weight:600;margin-top:6px}
|
|
163
163
|
.gif-export-btn.show{display:block}
|
|
164
164
|
.gif-export-btn:hover{background:var(--acc2)}
|
|
165
|
+
.frame-actions{display:none;gap:4px;margin-top:6px}
|
|
166
|
+
.frame-actions.show{display:flex}
|
|
167
|
+
.frame-actions button{flex:1;padding:6px 4px;font-size:9px;background:var(--bg3);border:1px solid var(--brd);border-radius:4px;color:var(--txt2);cursor:pointer}
|
|
168
|
+
.frame-actions button:hover{border-color:var(--acc);color:var(--acc)}
|
|
169
|
+
.frame-actions button.del:hover{border-color:#c44;color:#c44}
|
|
165
170
|
|
|
166
171
|
/* Export modal */
|
|
167
172
|
.export-modal{background:var(--bg2);border:1px solid var(--brd);border-radius:8px;padding:16px 20px;min-width:280px;max-width:320px}
|
|
@@ -227,7 +232,12 @@
|
|
|
227
232
|
<span>Frames</span>
|
|
228
233
|
</div>
|
|
229
234
|
<div class="frame-strip-scroll" id="frameScroll"></div>
|
|
235
|
+
<div class="frame-actions" id="frameActions">
|
|
236
|
+
<button class="del" onclick="deleteSelectedFrame()" title="Delete frame">Del</button>
|
|
237
|
+
<button onclick="replaceSelectedFrame()" title="Replace frame">Replace</button>
|
|
238
|
+
</div>
|
|
230
239
|
<button class="gif-export-btn" id="gifExportBtn" onclick="showExportModal()">Export</button>
|
|
240
|
+
<input type="file" id="replaceInput" accept=".png,.jpg,.jpeg" style="display:none" onchange="handleReplaceFile(event)">
|
|
231
241
|
</div>
|
|
232
242
|
|
|
233
243
|
<!-- Left divider (for frame panel) -->
|
|
@@ -280,6 +290,12 @@
|
|
|
280
290
|
</div>
|
|
281
291
|
<label class="opt"><input type="checkbox" id="rb-trim" checked onchange="schedulePreview()"><span class="box"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,3"/></svg></span>Auto-trim edges</label>
|
|
282
292
|
<label class="opt"><input type="checkbox" id="rb-edges" onchange="schedulePreview()"><span class="box"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,3"/></svg></span>Border only (preserve inner)</label>
|
|
293
|
+
<label class="opt"><input type="checkbox" id="rb-edgedetect" onchange="toggleEdgeDetect()"><span class="box"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,3"/></svg></span>Feather edges (smoother cuts)</label>
|
|
294
|
+
<div class="tool-row" id="rb-edgeRow" style="display:none">
|
|
295
|
+
<label>Softness</label>
|
|
296
|
+
<input type="range" id="rb-edgestr" min="0" max="100" value="50" oninput="$('rb-edgestrV').textContent=this.value+'%'" onchange="schedulePreview()">
|
|
297
|
+
<span class="val" id="rb-edgestrV">50%</span>
|
|
298
|
+
</div>
|
|
283
299
|
</div>
|
|
284
300
|
</div>
|
|
285
301
|
</div>
|
|
@@ -379,15 +395,22 @@
|
|
|
379
395
|
|
|
380
396
|
<!-- Loading state -->
|
|
381
397
|
<div class="ld" id="L"><div class="sp"></div><span id="lT">Processing...</span></div>
|
|
382
|
-
<!-- Log -->
|
|
383
|
-
<div class="log" id="
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
398
|
+
<!-- Log with preview -->
|
|
399
|
+
<div class="log-container" id="LC">
|
|
400
|
+
<div class="log" id="G"></div>
|
|
401
|
+
<img class="log-preview" id="LP">
|
|
402
|
+
</div>
|
|
403
|
+
<!-- Done state (replaces log container) -->
|
|
404
|
+
<div class="done-container" id="DC">
|
|
405
|
+
<div class="done-preview" id="donePreview"></div>
|
|
406
|
+
<div class="done-logs" id="doneLogs"></div>
|
|
407
|
+
<div class="done-result">
|
|
408
|
+
<h4 id="dT"></h4>
|
|
409
|
+
<p id="dM"></p>
|
|
410
|
+
</div>
|
|
411
|
+
<div class="done-btns">
|
|
389
412
|
<button class="btn btn-g" onclick="reset()">Back</button>
|
|
390
|
-
<button class="btn" onclick="openOutputFolder()" id="openFolderBtn"
|
|
413
|
+
<button class="btn" onclick="openOutputFolder()" id="openFolderBtn">Open Folder</button>
|
|
391
414
|
<button class="btn btn-p" onclick="PicLet.close()">Done</button>
|
|
392
415
|
</div>
|
|
393
416
|
</div>
|
|
@@ -403,6 +426,55 @@
|
|
|
403
426
|
</div>
|
|
404
427
|
</div>
|
|
405
428
|
|
|
429
|
+
<!-- Simplify GIF modal -->
|
|
430
|
+
<div class="modal-overlay" id="simplifyModal" onclick="hideSimplifyModal(event)">
|
|
431
|
+
<div class="export-modal" onclick="event.stopPropagation()">
|
|
432
|
+
<h3>Large GIF Detected</h3>
|
|
433
|
+
<p style="font-size:11px;color:var(--txt2);margin-bottom:12px">
|
|
434
|
+
This GIF has <b id="simplifyFrameCount">0</b> frames which may be slow to process.
|
|
435
|
+
Would you like to simplify it by skipping frames?
|
|
436
|
+
</p>
|
|
437
|
+
<div class="export-options">
|
|
438
|
+
<label class="export-opt" onclick="selectSimplifyOption(1)">
|
|
439
|
+
<input type="radio" name="simplifyType" value="1">
|
|
440
|
+
<span class="radio"></span>
|
|
441
|
+
<div class="export-opt-content">
|
|
442
|
+
<div class="export-opt-title">Keep All Frames</div>
|
|
443
|
+
<div class="export-opt-desc">Use the original GIF as-is</div>
|
|
444
|
+
</div>
|
|
445
|
+
</label>
|
|
446
|
+
<label class="export-opt selected" onclick="selectSimplifyOption(2)">
|
|
447
|
+
<input type="radio" name="simplifyType" value="2" checked>
|
|
448
|
+
<span class="radio"></span>
|
|
449
|
+
<div class="export-opt-content">
|
|
450
|
+
<div class="export-opt-title">Skip Every 2nd Frame</div>
|
|
451
|
+
<div class="export-opt-desc" id="simplifyDesc2">Reduces to ~0 frames</div>
|
|
452
|
+
</div>
|
|
453
|
+
</label>
|
|
454
|
+
<label class="export-opt" onclick="selectSimplifyOption(3)">
|
|
455
|
+
<input type="radio" name="simplifyType" value="3">
|
|
456
|
+
<span class="radio"></span>
|
|
457
|
+
<div class="export-opt-content">
|
|
458
|
+
<div class="export-opt-title">Skip Every 3rd Frame</div>
|
|
459
|
+
<div class="export-opt-desc" id="simplifyDesc3">Reduces to ~0 frames</div>
|
|
460
|
+
</div>
|
|
461
|
+
</label>
|
|
462
|
+
<label class="export-opt" onclick="selectSimplifyOption(4)">
|
|
463
|
+
<input type="radio" name="simplifyType" value="4">
|
|
464
|
+
<span class="radio"></span>
|
|
465
|
+
<div class="export-opt-content">
|
|
466
|
+
<div class="export-opt-title">Skip Every 4th Frame</div>
|
|
467
|
+
<div class="export-opt-desc" id="simplifyDesc4">Reduces to ~0 frames</div>
|
|
468
|
+
</div>
|
|
469
|
+
</label>
|
|
470
|
+
</div>
|
|
471
|
+
<div class="export-modal-btns">
|
|
472
|
+
<button class="cancel" onclick="hideSimplifyModal()">Cancel</button>
|
|
473
|
+
<button class="confirm" onclick="confirmSimplify()">Continue</button>
|
|
474
|
+
</div>
|
|
475
|
+
</div>
|
|
476
|
+
</div>
|
|
477
|
+
|
|
406
478
|
<!-- Export modal -->
|
|
407
479
|
<div class="modal-overlay" id="exportModal" onclick="hideExportModal(event)">
|
|
408
480
|
<div class="export-modal" onclick="event.stopPropagation()">
|
|
@@ -645,6 +717,16 @@ function toggleRatioLock() {
|
|
|
645
717
|
schedulePreview();
|
|
646
718
|
}
|
|
647
719
|
|
|
720
|
+
function toggleEdgeDetect() {
|
|
721
|
+
const enabled = $('rb-edgedetect').checked;
|
|
722
|
+
$('rb-edgeRow').style.display = enabled ? 'flex' : 'none';
|
|
723
|
+
// Edge detect and preserve inner are mutually exclusive
|
|
724
|
+
if (enabled) {
|
|
725
|
+
$('rb-edges').checked = false;
|
|
726
|
+
}
|
|
727
|
+
schedulePreview();
|
|
728
|
+
}
|
|
729
|
+
|
|
648
730
|
// Get combined options for all active tools
|
|
649
731
|
function getOptions() {
|
|
650
732
|
const opts = { tools: Array.from(activeTools) };
|
|
@@ -653,7 +735,9 @@ function getOptions() {
|
|
|
653
735
|
opts.removebg = {
|
|
654
736
|
fuzz: +$('rb-fuzz').value,
|
|
655
737
|
trim: $('rb-trim').checked,
|
|
656
|
-
preserveInner: $('rb-edges').checked
|
|
738
|
+
preserveInner: $('rb-edges').checked,
|
|
739
|
+
edgeDetect: $('rb-edgedetect').checked,
|
|
740
|
+
edgeStrength: +$('rb-edgestr').value
|
|
657
741
|
};
|
|
658
742
|
}
|
|
659
743
|
|
|
@@ -697,18 +781,7 @@ function schedulePreview() {
|
|
|
697
781
|
|
|
698
782
|
// Show original image
|
|
699
783
|
async function showOriginal() {
|
|
700
|
-
//
|
|
701
|
-
if (isGifFile && gifFrames.length > 0 && gifFrames[selectedFrameIndex]) {
|
|
702
|
-
const frame = gifFrames[selectedFrameIndex];
|
|
703
|
-
pA.innerHTML = '';
|
|
704
|
-
const img = document.createElement('img');
|
|
705
|
-
img.src = frame.thumbnail;
|
|
706
|
-
pA.appendChild(img);
|
|
707
|
-
pI.textContent = `${imageInfo.width} × ${imageInfo.height} (Frame ${selectedFrameIndex + 1} of ${imageInfo.frameCount})`;
|
|
708
|
-
updateZoom();
|
|
709
|
-
return;
|
|
710
|
-
}
|
|
711
|
-
|
|
784
|
+
// Always fetch full-scale preview from API (not the 96px thumbnails)
|
|
712
785
|
pA.innerHTML = '<div class="mini-sp"></div>';
|
|
713
786
|
try {
|
|
714
787
|
const opts = { tools: [], original: true };
|
|
@@ -723,7 +796,7 @@ async function showOriginal() {
|
|
|
723
796
|
pA.appendChild(img);
|
|
724
797
|
const frameInfo = isGifFile && imageInfo.frameCount > 1 ? ` (Frame ${selectedFrameIndex + 1} of ${imageInfo.frameCount})` : '';
|
|
725
798
|
pI.textContent = `${imageInfo.width} × ${imageInfo.height}${frameInfo}`;
|
|
726
|
-
updateZoom();
|
|
799
|
+
updateZoom();
|
|
727
800
|
}
|
|
728
801
|
} catch (e) {
|
|
729
802
|
pA.innerHTML = '<span class="placeholder">Failed to load image</span>';
|
|
@@ -897,11 +970,24 @@ let isPlaying = false;
|
|
|
897
970
|
let playInterval = null;
|
|
898
971
|
let playSpeed = 100; // ms per frame
|
|
899
972
|
|
|
900
|
-
function showFrameStrip() {
|
|
973
|
+
function showFrameStrip(skipSimplifyCheck = false) {
|
|
974
|
+
// Check for large GIFs and offer to simplify
|
|
975
|
+
if (!skipSimplifyCheck && imageInfo.frameCount >= LARGE_GIF_THRESHOLD) {
|
|
976
|
+
checkForLargeGif(imageInfo.frameCount, () => {
|
|
977
|
+
// After simplification (or if user keeps all frames), show the strip
|
|
978
|
+
actuallyShowFrameStrip();
|
|
979
|
+
});
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
actuallyShowFrameStrip();
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function actuallyShowFrameStrip() {
|
|
901
986
|
$('framePanel').classList.add('show');
|
|
902
987
|
$('dividerLeft').classList.add('show');
|
|
903
988
|
$('playControls').classList.add('show');
|
|
904
989
|
$('gifExportBtn').classList.add('show');
|
|
990
|
+
$('frameActions').classList.add('show');
|
|
905
991
|
$('frameCount').textContent = imageInfo.frameCount;
|
|
906
992
|
selectedFrameIndex = 0;
|
|
907
993
|
gifFrames = [];
|
|
@@ -914,6 +1000,7 @@ function hideFrameStrip() {
|
|
|
914
1000
|
$('dividerLeft').classList.remove('show');
|
|
915
1001
|
$('playControls').classList.remove('show');
|
|
916
1002
|
$('gifExportBtn').classList.remove('show');
|
|
1003
|
+
$('frameActions').classList.remove('show');
|
|
917
1004
|
stopPlayback();
|
|
918
1005
|
gifFrames = [];
|
|
919
1006
|
framePreviewCache = {};
|
|
@@ -1025,16 +1112,18 @@ function updatePlaybackFrame() {
|
|
|
1025
1112
|
thumb.classList.toggle('selected', i === selectedFrameIndex);
|
|
1026
1113
|
});
|
|
1027
1114
|
|
|
1028
|
-
//
|
|
1029
|
-
const
|
|
1030
|
-
|
|
1115
|
+
// Get the frame image from the strip (includes applied effects if active)
|
|
1116
|
+
const frameThumb = document.querySelector(`.frame-thumb[data-index="${selectedFrameIndex}"] img`);
|
|
1117
|
+
const frameSrc = frameThumb?.src || (gifFrames[selectedFrameIndex]?.thumbnail);
|
|
1118
|
+
|
|
1119
|
+
if (frameSrc) {
|
|
1031
1120
|
const img = pA.querySelector('img');
|
|
1032
1121
|
if (img) {
|
|
1033
|
-
img.src =
|
|
1122
|
+
img.src = frameSrc;
|
|
1034
1123
|
} else {
|
|
1035
1124
|
pA.innerHTML = '';
|
|
1036
1125
|
const newImg = document.createElement('img');
|
|
1037
|
-
newImg.src =
|
|
1126
|
+
newImg.src = frameSrc;
|
|
1038
1127
|
pA.appendChild(newImg);
|
|
1039
1128
|
updateZoom();
|
|
1040
1129
|
}
|
|
@@ -1090,16 +1179,8 @@ function selectFrame(index) {
|
|
|
1090
1179
|
|
|
1091
1180
|
async function showSelectedFrame() {
|
|
1092
1181
|
if (activeTools.size === 0) {
|
|
1093
|
-
//
|
|
1094
|
-
|
|
1095
|
-
if (frame && frame.thumbnail) {
|
|
1096
|
-
pA.innerHTML = '';
|
|
1097
|
-
const img = document.createElement('img');
|
|
1098
|
-
img.src = frame.thumbnail;
|
|
1099
|
-
pA.appendChild(img);
|
|
1100
|
-
pI.textContent = `Frame ${selectedFrameIndex + 1} of ${imageInfo.frameCount}`;
|
|
1101
|
-
updateZoom();
|
|
1102
|
-
}
|
|
1182
|
+
// Fetch full-scale frame from API
|
|
1183
|
+
showOriginal();
|
|
1103
1184
|
} else {
|
|
1104
1185
|
// Regenerate preview for selected frame
|
|
1105
1186
|
generatePreview();
|
|
@@ -1178,8 +1259,9 @@ async function runExport(opts, message) {
|
|
|
1178
1259
|
$('M').classList.add('hide');
|
|
1179
1260
|
$('B').classList.add('hide');
|
|
1180
1261
|
$('L').classList.add('on');
|
|
1181
|
-
$('
|
|
1262
|
+
$('LC').classList.add('on');
|
|
1182
1263
|
$('G').innerHTML = '';
|
|
1264
|
+
$('LP').classList.remove('on');
|
|
1183
1265
|
$('lT').textContent = message;
|
|
1184
1266
|
|
|
1185
1267
|
try {
|
|
@@ -1187,10 +1269,198 @@ async function runExport(opts, message) {
|
|
|
1187
1269
|
if (result.logs) {
|
|
1188
1270
|
result.logs.forEach(l => log('G', l.type[0], l.message));
|
|
1189
1271
|
}
|
|
1190
|
-
|
|
1272
|
+
// Show done state with preview
|
|
1273
|
+
await showDone(result.success, result.success ? 'Done' : 'Failed', result.success ? result.output : result.error);
|
|
1191
1274
|
} catch (e) {
|
|
1192
1275
|
log('G', 'e', e.message);
|
|
1193
|
-
showDone(false, 'Error', e.message);
|
|
1276
|
+
await showDone(false, 'Error', e.message);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// ── Frame Delete/Replace ──
|
|
1281
|
+
|
|
1282
|
+
async function deleteSelectedFrame() {
|
|
1283
|
+
if (!isGifFile || gifFrames.length <= 1) {
|
|
1284
|
+
showAlert('Cannot delete the only frame');
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
stopPlayback();
|
|
1289
|
+
|
|
1290
|
+
// Show loading on the frame
|
|
1291
|
+
const thumb = document.querySelector(`.frame-thumb[data-index="${selectedFrameIndex}"]`);
|
|
1292
|
+
if (thumb) thumb.classList.add('processing');
|
|
1293
|
+
|
|
1294
|
+
try {
|
|
1295
|
+
const result = await postJson('/api/delete-frame', { frameIndex: selectedFrameIndex });
|
|
1296
|
+
|
|
1297
|
+
if (result.success) {
|
|
1298
|
+
imageInfo.frameCount = result.frameCount;
|
|
1299
|
+
$('frameCount').textContent = result.frameCount;
|
|
1300
|
+
|
|
1301
|
+
// Adjust selected index if needed
|
|
1302
|
+
if (selectedFrameIndex >= result.frameCount) {
|
|
1303
|
+
selectedFrameIndex = result.frameCount - 1;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
// Reload frame thumbnails
|
|
1307
|
+
gifFrames = [];
|
|
1308
|
+
framePreviewCache = {};
|
|
1309
|
+
loadFrameThumbnails();
|
|
1310
|
+
|
|
1311
|
+
// Show updated frame
|
|
1312
|
+
setTimeout(() => selectFrame(selectedFrameIndex), 100);
|
|
1313
|
+
} else {
|
|
1314
|
+
if (thumb) thumb.classList.remove('processing');
|
|
1315
|
+
showAlert('Failed to delete frame: ' + (result.error || 'Unknown error'));
|
|
1316
|
+
}
|
|
1317
|
+
} catch (err) {
|
|
1318
|
+
if (thumb) thumb.classList.remove('processing');
|
|
1319
|
+
showAlert('Failed to delete frame: ' + err.message);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
function replaceSelectedFrame() {
|
|
1324
|
+
if (!isGifFile) return;
|
|
1325
|
+
$('replaceInput').click();
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
async function handleReplaceFile(e) {
|
|
1329
|
+
const file = e.target.files[0];
|
|
1330
|
+
if (!file) return;
|
|
1331
|
+
e.target.value = ''; // Reset for future use
|
|
1332
|
+
|
|
1333
|
+
stopPlayback();
|
|
1334
|
+
|
|
1335
|
+
// Show loading on the frame
|
|
1336
|
+
const thumb = document.querySelector(`.frame-thumb[data-index="${selectedFrameIndex}"]`);
|
|
1337
|
+
if (thumb) thumb.classList.add('processing');
|
|
1338
|
+
|
|
1339
|
+
try {
|
|
1340
|
+
// Read file as base64
|
|
1341
|
+
const reader = new FileReader();
|
|
1342
|
+
reader.onload = async () => {
|
|
1343
|
+
const base64 = reader.result.split(',')[1]; // Remove data URL prefix
|
|
1344
|
+
|
|
1345
|
+
const result = await postJson('/api/replace-frame', {
|
|
1346
|
+
frameIndex: selectedFrameIndex,
|
|
1347
|
+
imageData: base64
|
|
1348
|
+
});
|
|
1349
|
+
|
|
1350
|
+
if (result.success) {
|
|
1351
|
+
// Reload frame thumbnails
|
|
1352
|
+
gifFrames = [];
|
|
1353
|
+
framePreviewCache = {};
|
|
1354
|
+
loadFrameThumbnails();
|
|
1355
|
+
|
|
1356
|
+
// Show updated frame
|
|
1357
|
+
setTimeout(() => selectFrame(selectedFrameIndex), 100);
|
|
1358
|
+
} else {
|
|
1359
|
+
if (thumb) thumb.classList.remove('processing');
|
|
1360
|
+
showAlert('Failed to replace frame: ' + (result.error || 'Unknown error'));
|
|
1361
|
+
}
|
|
1362
|
+
};
|
|
1363
|
+
reader.readAsDataURL(file);
|
|
1364
|
+
} catch (err) {
|
|
1365
|
+
if (thumb) thumb.classList.remove('processing');
|
|
1366
|
+
showAlert('Failed to replace frame: ' + err.message);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
// ── GIF Simplification ──
|
|
1371
|
+
|
|
1372
|
+
const LARGE_GIF_THRESHOLD = 50; // Frames threshold for showing simplify prompt
|
|
1373
|
+
let selectedSimplifyOption = 2; // Default to skip every 2nd frame
|
|
1374
|
+
let pendingSimplifyCallback = null;
|
|
1375
|
+
|
|
1376
|
+
function checkForLargeGif(frameCount, callback) {
|
|
1377
|
+
if (frameCount >= LARGE_GIF_THRESHOLD) {
|
|
1378
|
+
showSimplifyModal(frameCount, callback);
|
|
1379
|
+
return true;
|
|
1380
|
+
}
|
|
1381
|
+
if (callback) callback();
|
|
1382
|
+
return false;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
function showSimplifyModal(frameCount, callback) {
|
|
1386
|
+
pendingSimplifyCallback = callback;
|
|
1387
|
+
|
|
1388
|
+
// Update modal content
|
|
1389
|
+
$('simplifyFrameCount').textContent = frameCount;
|
|
1390
|
+
$('simplifyDesc2').textContent = `Reduces to ~${Math.ceil(frameCount / 2)} frames`;
|
|
1391
|
+
$('simplifyDesc3').textContent = `Reduces to ~${Math.ceil(frameCount / 3)} frames`;
|
|
1392
|
+
$('simplifyDesc4').textContent = `Reduces to ~${Math.ceil(frameCount / 4)} frames`;
|
|
1393
|
+
|
|
1394
|
+
// Reset selection to skip every 2nd (recommended for most cases)
|
|
1395
|
+
selectedSimplifyOption = 2;
|
|
1396
|
+
document.querySelectorAll('#simplifyModal .export-opt').forEach(opt => {
|
|
1397
|
+
const input = opt.querySelector('input');
|
|
1398
|
+
opt.classList.toggle('selected', input.value === '2');
|
|
1399
|
+
input.checked = input.value === '2';
|
|
1400
|
+
});
|
|
1401
|
+
|
|
1402
|
+
$('simplifyModal').classList.add('on');
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
function hideSimplifyModal(e) {
|
|
1406
|
+
if (!e || e.target === $('simplifyModal')) {
|
|
1407
|
+
$('simplifyModal').classList.remove('on');
|
|
1408
|
+
pendingSimplifyCallback = null;
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
function selectSimplifyOption(value) {
|
|
1413
|
+
selectedSimplifyOption = value;
|
|
1414
|
+
document.querySelectorAll('#simplifyModal .export-opt').forEach(opt => {
|
|
1415
|
+
const input = opt.querySelector('input');
|
|
1416
|
+
opt.classList.toggle('selected', +input.value === value);
|
|
1417
|
+
input.checked = +input.value === value;
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
async function confirmSimplify() {
|
|
1422
|
+
// Save callback before hiding modal (hideSimplifyModal clears it)
|
|
1423
|
+
const callback = pendingSimplifyCallback;
|
|
1424
|
+
hideSimplifyModal();
|
|
1425
|
+
|
|
1426
|
+
if (selectedSimplifyOption === 1) {
|
|
1427
|
+
// Keep all frames - just continue
|
|
1428
|
+
if (callback) callback();
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
// Show loading state
|
|
1433
|
+
pA.innerHTML = '<div class="mini-sp"></div>';
|
|
1434
|
+
pI.textContent = 'Simplifying GIF...';
|
|
1435
|
+
|
|
1436
|
+
try {
|
|
1437
|
+
const result = await postJson('/api/simplify-gif', { skipFactor: selectedSimplifyOption });
|
|
1438
|
+
|
|
1439
|
+
if (result.success) {
|
|
1440
|
+
// Update image info with simplified version
|
|
1441
|
+
imageInfo.width = result.width;
|
|
1442
|
+
imageInfo.height = result.height;
|
|
1443
|
+
imageInfo.frameCount = result.frameCount;
|
|
1444
|
+
|
|
1445
|
+
// Update display
|
|
1446
|
+
$('origSize').textContent = imageInfo.width + '×' + imageInfo.height;
|
|
1447
|
+
|
|
1448
|
+
// Refresh frame strip (skip simplify check since we just simplified)
|
|
1449
|
+
currentFrameCount = result.frameCount;
|
|
1450
|
+
if (result.frameCount > 1) {
|
|
1451
|
+
showFrameStrip(true); // Skip simplify check
|
|
1452
|
+
} else {
|
|
1453
|
+
hideFrameStrip();
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
if (callback) callback();
|
|
1457
|
+
} else {
|
|
1458
|
+
showAlert('Failed to simplify GIF: ' + (result.error || 'Unknown error'));
|
|
1459
|
+
showOriginal();
|
|
1460
|
+
}
|
|
1461
|
+
} catch (err) {
|
|
1462
|
+
showAlert('Failed to simplify GIF: ' + err.message);
|
|
1463
|
+
showOriginal();
|
|
1194
1464
|
}
|
|
1195
1465
|
}
|
|
1196
1466
|
|
|
@@ -1373,8 +1643,9 @@ async function apply() {
|
|
|
1373
1643
|
$('M').classList.add('hide');
|
|
1374
1644
|
$('B').classList.add('hide');
|
|
1375
1645
|
$('L').classList.add('on');
|
|
1376
|
-
$('
|
|
1646
|
+
$('LC').classList.add('on');
|
|
1377
1647
|
$('G').innerHTML = '';
|
|
1648
|
+
$('LP').classList.remove('on');
|
|
1378
1649
|
|
|
1379
1650
|
const toolNames = Array.from(activeTools).map(t => {
|
|
1380
1651
|
const names = { removebg: 'Remove BG', scale: 'Scale', icons: 'Icons', storepack: 'Store Assets' };
|
|
@@ -1389,20 +1660,22 @@ async function apply() {
|
|
|
1389
1660
|
result.logs.forEach(l => log('G', l.type[0], l.message));
|
|
1390
1661
|
}
|
|
1391
1662
|
|
|
1392
|
-
|
|
1663
|
+
// Show done state with preview
|
|
1664
|
+
await showDone(result.success, result.success ? 'Done' : 'Failed', result.success ? result.output : result.error);
|
|
1393
1665
|
} catch (e) {
|
|
1394
1666
|
log('G', 'e', e.message);
|
|
1395
|
-
showDone(false, 'Error', e.message);
|
|
1667
|
+
await showDone(false, 'Error', e.message);
|
|
1396
1668
|
}
|
|
1397
1669
|
}
|
|
1398
1670
|
|
|
1399
1671
|
// Reset to main view
|
|
1400
1672
|
function reset() {
|
|
1401
|
-
$('
|
|
1402
|
-
$('
|
|
1673
|
+
$('DC').classList.remove('on', 'ok', 'err');
|
|
1674
|
+
$('donePreview').classList.remove('on');
|
|
1675
|
+
$('LC').classList.remove('on');
|
|
1676
|
+
$('LP').classList.remove('on');
|
|
1403
1677
|
$('M').classList.remove('hide');
|
|
1404
1678
|
$('B').classList.remove('hide');
|
|
1405
|
-
$('openFolderBtn').style.display = 'none';
|
|
1406
1679
|
lastOutputSuccess = false;
|
|
1407
1680
|
}
|
|
1408
1681
|
|
|
@@ -1420,21 +1693,75 @@ function openOutputFolder() {
|
|
|
1420
1693
|
postJson('/api/open-folder', {});
|
|
1421
1694
|
}
|
|
1422
1695
|
|
|
1423
|
-
// Show
|
|
1424
|
-
function
|
|
1696
|
+
// Show output preview thumbnail in log area
|
|
1697
|
+
async function showLogPreview() {
|
|
1698
|
+
try {
|
|
1699
|
+
const result = await fetchJson('/api/output-preview');
|
|
1700
|
+
if (result.success && result.imageData) {
|
|
1701
|
+
$('LP').src = result.imageData;
|
|
1702
|
+
$('LP').classList.add('on');
|
|
1703
|
+
}
|
|
1704
|
+
} catch { /* ignore */ }
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
// Show done state (replaces log container)
|
|
1708
|
+
async function showDone(success, title, message) {
|
|
1425
1709
|
$('L').classList.remove('on');
|
|
1426
|
-
$('
|
|
1710
|
+
$('LC').classList.remove('on'); // Hide log container
|
|
1711
|
+
$('DC').classList.add('on', success ? 'ok' : 'err');
|
|
1712
|
+
$('DC').classList.remove(success ? 'err' : 'ok');
|
|
1427
1713
|
$('dT').textContent = title;
|
|
1428
1714
|
$('dM').textContent = message;
|
|
1429
1715
|
lastOutputSuccess = success;
|
|
1430
1716
|
$('openFolderBtn').style.display = success ? '' : 'none';
|
|
1431
1717
|
if (success) hasChanges = false;
|
|
1718
|
+
|
|
1719
|
+
// Copy logs to done container
|
|
1720
|
+
$('doneLogs').innerHTML = $('G').innerHTML;
|
|
1721
|
+
|
|
1722
|
+
// Fetch and show thumbnail
|
|
1723
|
+
const donePreview = $('donePreview');
|
|
1724
|
+
if (success) {
|
|
1725
|
+
try {
|
|
1726
|
+
const result = await fetchJson('/api/output-preview');
|
|
1727
|
+
if (result.success && result.imageData) {
|
|
1728
|
+
donePreview.innerHTML = '';
|
|
1729
|
+
const img = document.createElement('img');
|
|
1730
|
+
img.src = result.imageData;
|
|
1731
|
+
img.alt = 'Output';
|
|
1732
|
+
donePreview.appendChild(img);
|
|
1733
|
+
} else {
|
|
1734
|
+
// Show error in preview area
|
|
1735
|
+
donePreview.innerHTML = `<span style="color:var(--err);font-size:10px;z-index:1;position:relative">${result.error || 'No preview'}</span>`;
|
|
1736
|
+
}
|
|
1737
|
+
} catch (e) {
|
|
1738
|
+
donePreview.innerHTML = `<span style="color:var(--err);font-size:10px;z-index:1;position:relative">Error: ${e.message}</span>`;
|
|
1739
|
+
}
|
|
1740
|
+
} else {
|
|
1741
|
+
donePreview.innerHTML = '';
|
|
1742
|
+
}
|
|
1432
1743
|
}
|
|
1433
1744
|
|
|
1434
1745
|
// Init
|
|
1435
1746
|
setupDragDrop();
|
|
1436
1747
|
setupDivider();
|
|
1437
1748
|
|
|
1749
|
+
// Spacebar to toggle GIF playback
|
|
1750
|
+
document.addEventListener('keydown', (e) => {
|
|
1751
|
+
// Only handle space if not typing in an input
|
|
1752
|
+
if (e.code === 'Space' && !['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement?.tagName)) {
|
|
1753
|
+
if (isGifFile && gifFrames.length > 1) {
|
|
1754
|
+
e.preventDefault();
|
|
1755
|
+
const wasPlaying = isPlaying;
|
|
1756
|
+
togglePlayback();
|
|
1757
|
+
// When stopping, refresh to full-scale preview
|
|
1758
|
+
if (wasPlaying) {
|
|
1759
|
+
showSelectedFrame();
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
});
|
|
1764
|
+
|
|
1438
1765
|
// Resizable dividers
|
|
1439
1766
|
function setupDivider() {
|
|
1440
1767
|
const divider = $('divider');
|