@spark-apps/piclet 1.0.4 → 1.0.6
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 +327 -314
- package/dist/cli.js.map +1 -1
- package/dist/gui/css/theme.css +18 -10
- package/dist/gui/piclet.html +73 -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
|
@@ -69,16 +69,24 @@ input[type="range"]::-webkit-slider-thumb{-webkit-appearance:none;width:14px;hei
|
|
|
69
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
70
|
.log-preview.on{display:block}
|
|
71
71
|
|
|
72
|
-
/* Done
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
.done-
|
|
76
|
-
.done-
|
|
77
|
-
.done-
|
|
78
|
-
.done-
|
|
79
|
-
.done-
|
|
80
|
-
.done-
|
|
81
|
-
.done-
|
|
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}
|
|
82
90
|
|
|
83
91
|
/* Warning/info box */
|
|
84
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
|
@@ -400,13 +400,14 @@
|
|
|
400
400
|
<div class="log" id="G"></div>
|
|
401
401
|
<img class="log-preview" id="LP">
|
|
402
402
|
</div>
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
<div class="
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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>
|
|
410
411
|
<div class="done-btns">
|
|
411
412
|
<button class="btn btn-g" onclick="reset()">Back</button>
|
|
412
413
|
<button class="btn" onclick="openOutputFolder()" id="openFolderBtn">Open Folder</button>
|
|
@@ -780,18 +781,7 @@ function schedulePreview() {
|
|
|
780
781
|
|
|
781
782
|
// Show original image
|
|
782
783
|
async function showOriginal() {
|
|
783
|
-
//
|
|
784
|
-
if (isGifFile && gifFrames.length > 0 && gifFrames[selectedFrameIndex]) {
|
|
785
|
-
const frame = gifFrames[selectedFrameIndex];
|
|
786
|
-
pA.innerHTML = '';
|
|
787
|
-
const img = document.createElement('img');
|
|
788
|
-
img.src = frame.thumbnail;
|
|
789
|
-
pA.appendChild(img);
|
|
790
|
-
pI.textContent = `${imageInfo.width} × ${imageInfo.height} (Frame ${selectedFrameIndex + 1} of ${imageInfo.frameCount})`;
|
|
791
|
-
updateZoom();
|
|
792
|
-
return;
|
|
793
|
-
}
|
|
794
|
-
|
|
784
|
+
// Always fetch full-scale preview from API (not the 96px thumbnails)
|
|
795
785
|
pA.innerHTML = '<div class="mini-sp"></div>';
|
|
796
786
|
try {
|
|
797
787
|
const opts = { tools: [], original: true };
|
|
@@ -806,7 +796,7 @@ async function showOriginal() {
|
|
|
806
796
|
pA.appendChild(img);
|
|
807
797
|
const frameInfo = isGifFile && imageInfo.frameCount > 1 ? ` (Frame ${selectedFrameIndex + 1} of ${imageInfo.frameCount})` : '';
|
|
808
798
|
pI.textContent = `${imageInfo.width} × ${imageInfo.height}${frameInfo}`;
|
|
809
|
-
updateZoom();
|
|
799
|
+
updateZoom();
|
|
810
800
|
}
|
|
811
801
|
} catch (e) {
|
|
812
802
|
pA.innerHTML = '<span class="placeholder">Failed to load image</span>';
|
|
@@ -1122,16 +1112,18 @@ function updatePlaybackFrame() {
|
|
|
1122
1112
|
thumb.classList.toggle('selected', i === selectedFrameIndex);
|
|
1123
1113
|
});
|
|
1124
1114
|
|
|
1125
|
-
//
|
|
1126
|
-
const
|
|
1127
|
-
|
|
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) {
|
|
1128
1120
|
const img = pA.querySelector('img');
|
|
1129
1121
|
if (img) {
|
|
1130
|
-
img.src =
|
|
1122
|
+
img.src = frameSrc;
|
|
1131
1123
|
} else {
|
|
1132
1124
|
pA.innerHTML = '';
|
|
1133
1125
|
const newImg = document.createElement('img');
|
|
1134
|
-
newImg.src =
|
|
1126
|
+
newImg.src = frameSrc;
|
|
1135
1127
|
pA.appendChild(newImg);
|
|
1136
1128
|
updateZoom();
|
|
1137
1129
|
}
|
|
@@ -1187,16 +1179,8 @@ function selectFrame(index) {
|
|
|
1187
1179
|
|
|
1188
1180
|
async function showSelectedFrame() {
|
|
1189
1181
|
if (activeTools.size === 0) {
|
|
1190
|
-
//
|
|
1191
|
-
|
|
1192
|
-
if (frame && frame.thumbnail) {
|
|
1193
|
-
pA.innerHTML = '';
|
|
1194
|
-
const img = document.createElement('img');
|
|
1195
|
-
img.src = frame.thumbnail;
|
|
1196
|
-
pA.appendChild(img);
|
|
1197
|
-
pI.textContent = `Frame ${selectedFrameIndex + 1} of ${imageInfo.frameCount}`;
|
|
1198
|
-
updateZoom();
|
|
1199
|
-
}
|
|
1182
|
+
// Fetch full-scale frame from API
|
|
1183
|
+
showOriginal();
|
|
1200
1184
|
} else {
|
|
1201
1185
|
// Regenerate preview for selected frame
|
|
1202
1186
|
generatePreview();
|
|
@@ -1285,12 +1269,11 @@ async function runExport(opts, message) {
|
|
|
1285
1269
|
if (result.logs) {
|
|
1286
1270
|
result.logs.forEach(l => log('G', l.type[0], l.message));
|
|
1287
1271
|
}
|
|
1288
|
-
//
|
|
1289
|
-
|
|
1290
|
-
showDone(result.success, result.success ? 'Done' : 'Failed', result.success ? result.output : result.error);
|
|
1272
|
+
// Show done state with preview
|
|
1273
|
+
await showDone(result.success, result.success ? 'Done' : 'Failed', result.success ? result.output : result.error);
|
|
1291
1274
|
} catch (e) {
|
|
1292
1275
|
log('G', 'e', e.message);
|
|
1293
|
-
showDone(false, 'Error', e.message);
|
|
1276
|
+
await showDone(false, 'Error', e.message);
|
|
1294
1277
|
}
|
|
1295
1278
|
}
|
|
1296
1279
|
|
|
@@ -1677,19 +1660,18 @@ async function apply() {
|
|
|
1677
1660
|
result.logs.forEach(l => log('G', l.type[0], l.message));
|
|
1678
1661
|
}
|
|
1679
1662
|
|
|
1680
|
-
//
|
|
1681
|
-
|
|
1682
|
-
showDone(result.success, result.success ? 'Done' : 'Failed', result.success ? result.output : result.error);
|
|
1663
|
+
// Show done state with preview
|
|
1664
|
+
await showDone(result.success, result.success ? 'Done' : 'Failed', result.success ? result.output : result.error);
|
|
1683
1665
|
} catch (e) {
|
|
1684
1666
|
log('G', 'e', e.message);
|
|
1685
|
-
showDone(false, 'Error', e.message);
|
|
1667
|
+
await showDone(false, 'Error', e.message);
|
|
1686
1668
|
}
|
|
1687
1669
|
}
|
|
1688
1670
|
|
|
1689
1671
|
// Reset to main view
|
|
1690
1672
|
function reset() {
|
|
1691
|
-
$('
|
|
1692
|
-
$('
|
|
1673
|
+
$('DC').classList.remove('on', 'ok', 'err');
|
|
1674
|
+
$('donePreview').classList.remove('on');
|
|
1693
1675
|
$('LC').classList.remove('on');
|
|
1694
1676
|
$('LP').classList.remove('on');
|
|
1695
1677
|
$('M').classList.remove('hide');
|
|
@@ -1722,23 +1704,64 @@ async function showLogPreview() {
|
|
|
1722
1704
|
} catch { /* ignore */ }
|
|
1723
1705
|
}
|
|
1724
1706
|
|
|
1725
|
-
// Show done
|
|
1726
|
-
function showDone(success, title, message) {
|
|
1707
|
+
// Show done state (replaces log container)
|
|
1708
|
+
async function showDone(success, title, message) {
|
|
1727
1709
|
$('L').classList.remove('on');
|
|
1728
|
-
$('
|
|
1729
|
-
$('
|
|
1730
|
-
$('
|
|
1710
|
+
$('LC').classList.remove('on'); // Hide log container
|
|
1711
|
+
$('DC').classList.add('on', success ? 'ok' : 'err');
|
|
1712
|
+
$('DC').classList.remove(success ? 'err' : 'ok');
|
|
1731
1713
|
$('dT').textContent = title;
|
|
1732
1714
|
$('dM').textContent = message;
|
|
1733
1715
|
lastOutputSuccess = success;
|
|
1734
1716
|
$('openFolderBtn').style.display = success ? '' : 'none';
|
|
1735
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
|
+
}
|
|
1736
1743
|
}
|
|
1737
1744
|
|
|
1738
1745
|
// Init
|
|
1739
1746
|
setupDragDrop();
|
|
1740
1747
|
setupDivider();
|
|
1741
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
|
+
|
|
1742
1765
|
// Resizable dividers
|
|
1743
1766
|
function setupDivider() {
|
|
1744
1767
|
const divider = $('divider');
|
package/package.json
CHANGED
package/dist/gui/border.html
DELETED
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en" data-piclet data-width="400" data-height="440">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
-
<title>Add Border</title>
|
|
7
|
-
<link rel="stylesheet" href="/css/theme.css">
|
|
8
|
-
<script src="/js/piclet.js"></script>
|
|
9
|
-
<style>
|
|
10
|
-
.preview-area{flex:1;display:flex;align-items:center;justify-content:center;background:var(--bg2);border-radius:6px;min-height:140px;overflow:hidden;position:relative}
|
|
11
|
-
.preview-area::before{content:'';position:absolute;inset:0;background:repeating-conic-gradient(#1a1a1d 0% 25%, #222 0% 50%) 50%/16px 16px;z-index:0}
|
|
12
|
-
.preview-area img{max-width:100%;max-height:100%;object-fit:contain;position:relative;z-index:1}
|
|
13
|
-
.preview-area .placeholder{position:relative;z-index:1;font-size:11px;color:var(--txt3)}
|
|
14
|
-
.preview-area .mini-sp{width:16px;height:16px;border:2px solid var(--bg3);border-top-color:var(--acc);border-radius:50%;animation:s .5s linear infinite;position:relative;z-index:1}
|
|
15
|
-
.preview-info{font-size:10px;color:var(--txt3);text-align:center;margin-top:4px;height:14px}
|
|
16
|
-
.controls{display:flex;flex-direction:column;gap:10px}
|
|
17
|
-
.control-row{display:flex;align-items:center;gap:10px}
|
|
18
|
-
.control-row label{font-size:11px;color:var(--txt3);width:50px;flex-shrink:0}
|
|
19
|
-
.control-row input[type="range"]{flex:1;height:4px;background:var(--bg3);border-radius:3px;-webkit-appearance:none}
|
|
20
|
-
.control-row input[type="range"]::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:14px;background:var(--acc);border-radius:50%;cursor:pointer}
|
|
21
|
-
.control-row .val{width:40px;font-size:12px;color:var(--acc2);text-align:right;font-weight:500}
|
|
22
|
-
.color-row{display:flex;align-items:center;gap:10px}
|
|
23
|
-
.color-row label{font-size:11px;color:var(--txt3);width:50px;flex-shrink:0}
|
|
24
|
-
.color-input-wrap{flex:1;display:flex;align-items:center;gap:8px}
|
|
25
|
-
.color-preview{width:28px;height:28px;border-radius:4px;border:1px solid var(--brd);cursor:pointer;flex-shrink:0}
|
|
26
|
-
.color-input-wrap input[type="color"]{position:absolute;opacity:0;width:28px;height:28px;cursor:pointer}
|
|
27
|
-
.color-input-wrap input[type="text"]{flex:1;padding:6px 10px;background:var(--bg2);border:1px solid var(--brd);border-radius:5px;color:var(--txt);font:inherit;font-size:12px}
|
|
28
|
-
.color-input-wrap input[type="text"]:focus{outline:none;border-color:var(--acc)}
|
|
29
|
-
.preset-colors{display:flex;gap:4px;margin-left:auto}
|
|
30
|
-
.preset-color{width:20px;height:20px;border-radius:3px;border:1px solid var(--brd);cursor:pointer;transition:transform .1s}
|
|
31
|
-
.preset-color:hover{transform:scale(1.1)}
|
|
32
|
-
</style>
|
|
33
|
-
</head>
|
|
34
|
-
<body>
|
|
35
|
-
<div class="app">
|
|
36
|
-
<div class="hd"><b>PicLet</b><span>Add Border</span></div>
|
|
37
|
-
<div class="meta">
|
|
38
|
-
<div>File<b id="fn">-</b></div>
|
|
39
|
-
<div>Size<b id="sz">-</b></div>
|
|
40
|
-
</div>
|
|
41
|
-
<!-- Form -->
|
|
42
|
-
<div id="F" class="form">
|
|
43
|
-
<div class="preview-area" id="pA">
|
|
44
|
-
<span class="placeholder">Adjust to preview</span>
|
|
45
|
-
</div>
|
|
46
|
-
<div class="preview-info" id="pI"></div>
|
|
47
|
-
<div class="controls">
|
|
48
|
-
<div class="control-row">
|
|
49
|
-
<label>Width</label>
|
|
50
|
-
<input type="range" id="wR" min="1" max="100" value="10">
|
|
51
|
-
<span class="val" id="wV">10px</span>
|
|
52
|
-
</div>
|
|
53
|
-
<div class="color-row">
|
|
54
|
-
<label>Color</label>
|
|
55
|
-
<div class="color-input-wrap">
|
|
56
|
-
<div class="color-preview" id="cP" style="background:#ffffff"></div>
|
|
57
|
-
<input type="color" id="cC" value="#ffffff">
|
|
58
|
-
<input type="text" id="cT" value="#ffffff" placeholder="#ffffff">
|
|
59
|
-
<div class="preset-colors">
|
|
60
|
-
<div class="preset-color" style="background:#ffffff" data-color="#ffffff"></div>
|
|
61
|
-
<div class="preset-color" style="background:#000000" data-color="#000000"></div>
|
|
62
|
-
<div class="preset-color" style="background:#eab308" data-color="#eab308"></div>
|
|
63
|
-
<div class="preset-color" style="background:#ef4444" data-color="#ef4444"></div>
|
|
64
|
-
<div class="preset-color" style="background:#3b82f6" data-color="#3b82f6"></div>
|
|
65
|
-
</div>
|
|
66
|
-
</div>
|
|
67
|
-
</div>
|
|
68
|
-
</div>
|
|
69
|
-
<div class="btns">
|
|
70
|
-
<button class="btn btn-g" onclick="PicLet.close()">Cancel</button>
|
|
71
|
-
<button class="btn btn-p" onclick="apply()">Apply</button>
|
|
72
|
-
</div>
|
|
73
|
-
</div>
|
|
74
|
-
<!-- Loading state -->
|
|
75
|
-
<div class="ld" id="L"><div class="sp"></div><span id="lT">Adding border...</span></div>
|
|
76
|
-
<!-- Log -->
|
|
77
|
-
<div class="log" id="G"></div>
|
|
78
|
-
<!-- Done state -->
|
|
79
|
-
<div class="dn" id="D">
|
|
80
|
-
<h4 id="dT"></h4>
|
|
81
|
-
<p id="dM"></p>
|
|
82
|
-
<div class="btns" style="width:100%;margin-top:8px">
|
|
83
|
-
<button class="btn btn-p" onclick="PicLet.close()">Done</button>
|
|
84
|
-
</div>
|
|
85
|
-
</div>
|
|
86
|
-
</div>
|
|
87
|
-
<script>
|
|
88
|
-
const { $, log, fetchJson, postJson } = PicLet;
|
|
89
|
-
const pA = $('pA'), pI = $('pI');
|
|
90
|
-
const wR = $('wR'), wV = $('wV');
|
|
91
|
-
const cC = $('cC'), cT = $('cT'), cP = $('cP');
|
|
92
|
-
|
|
93
|
-
let previewTimeout = null;
|
|
94
|
-
let isSliding = false;
|
|
95
|
-
let lastPreviewOpts = null;
|
|
96
|
-
|
|
97
|
-
// Width slider
|
|
98
|
-
wR.oninput = () => { wV.textContent = wR.value + 'px'; };
|
|
99
|
-
wR.addEventListener('mousedown', () => { isSliding = true; });
|
|
100
|
-
wR.addEventListener('mouseup', () => { isSliding = false; schedulePreview(); });
|
|
101
|
-
wR.addEventListener('mouseleave', () => { if (isSliding) { isSliding = false; schedulePreview(); } });
|
|
102
|
-
|
|
103
|
-
// Color picker
|
|
104
|
-
cC.oninput = () => {
|
|
105
|
-
cT.value = cC.value;
|
|
106
|
-
cP.style.background = cC.value;
|
|
107
|
-
schedulePreview();
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
cT.oninput = () => {
|
|
111
|
-
const val = cT.value;
|
|
112
|
-
if (/^#[0-9a-fA-F]{6}$/.test(val)) {
|
|
113
|
-
cC.value = val;
|
|
114
|
-
cP.style.background = val;
|
|
115
|
-
schedulePreview();
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
cT.onblur = () => {
|
|
120
|
-
schedulePreview();
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
cP.onclick = () => cC.click();
|
|
124
|
-
|
|
125
|
-
// Preset colors
|
|
126
|
-
document.querySelectorAll('.preset-color').forEach(el => {
|
|
127
|
-
el.onclick = () => {
|
|
128
|
-
const color = el.dataset.color;
|
|
129
|
-
cC.value = color;
|
|
130
|
-
cT.value = color;
|
|
131
|
-
cP.style.background = color;
|
|
132
|
-
schedulePreview();
|
|
133
|
-
};
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
// Get current options
|
|
137
|
-
function getOptions() {
|
|
138
|
-
return {
|
|
139
|
-
width: +wR.value,
|
|
140
|
-
color: cT.value || '#ffffff'
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Check if options changed
|
|
145
|
-
function optionsChanged() {
|
|
146
|
-
const current = JSON.stringify(getOptions());
|
|
147
|
-
if (current === lastPreviewOpts) return false;
|
|
148
|
-
lastPreviewOpts = current;
|
|
149
|
-
return true;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Schedule preview
|
|
153
|
-
function schedulePreview() {
|
|
154
|
-
if (isSliding) return;
|
|
155
|
-
if (previewTimeout) clearTimeout(previewTimeout);
|
|
156
|
-
previewTimeout = setTimeout(() => {
|
|
157
|
-
if (!isSliding && optionsChanged()) {
|
|
158
|
-
generatePreview();
|
|
159
|
-
}
|
|
160
|
-
}, 300);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Generate preview
|
|
164
|
-
async function generatePreview() {
|
|
165
|
-
pA.innerHTML = '<div class="mini-sp"></div>';
|
|
166
|
-
pI.textContent = '';
|
|
167
|
-
|
|
168
|
-
try {
|
|
169
|
-
const result = await postJson('/api/preview', getOptions());
|
|
170
|
-
|
|
171
|
-
if (result.success && result.imageData) {
|
|
172
|
-
const img = document.createElement('img');
|
|
173
|
-
img.src = result.imageData;
|
|
174
|
-
img.alt = 'Preview';
|
|
175
|
-
pA.innerHTML = '';
|
|
176
|
-
pA.appendChild(img);
|
|
177
|
-
pI.textContent = result.width && result.height ? `${result.width}×${result.height}` : '';
|
|
178
|
-
} else {
|
|
179
|
-
pA.innerHTML = '<span class="placeholder">' + (result.error || 'Preview failed') + '</span>';
|
|
180
|
-
pI.textContent = '';
|
|
181
|
-
}
|
|
182
|
-
} catch (e) {
|
|
183
|
-
pA.innerHTML = '<span class="placeholder">Preview error</span>';
|
|
184
|
-
pI.textContent = '';
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Load initial data
|
|
189
|
-
fetchJson('/api/info').then(d => {
|
|
190
|
-
$('fn').textContent = d.fileName;
|
|
191
|
-
$('sz').textContent = d.width + '×' + d.height;
|
|
192
|
-
if (d.defaults) {
|
|
193
|
-
wR.value = d.defaults.width || 10;
|
|
194
|
-
wV.textContent = wR.value + 'px';
|
|
195
|
-
const color = d.defaults.color || '#ffffff';
|
|
196
|
-
cC.value = color;
|
|
197
|
-
cT.value = color;
|
|
198
|
-
cP.style.background = color;
|
|
199
|
-
}
|
|
200
|
-
setTimeout(generatePreview, 100);
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
// Apply
|
|
204
|
-
async function apply() {
|
|
205
|
-
$('F').classList.add('hide');
|
|
206
|
-
$('L').classList.add('on');
|
|
207
|
-
$('G').classList.add('on');
|
|
208
|
-
|
|
209
|
-
try {
|
|
210
|
-
const result = await postJson('/api/process', getOptions());
|
|
211
|
-
if (result.logs) {
|
|
212
|
-
result.logs.forEach(l => log('G', l.type[0], l.message));
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
$('L').classList.remove('on');
|
|
216
|
-
$('D').classList.add('on', result.success ? 'ok' : 'err');
|
|
217
|
-
$('dT').textContent = result.success ? 'Done' : 'Failed';
|
|
218
|
-
$('dM').textContent = result.success ? result.output : result.error;
|
|
219
|
-
} catch (e) {
|
|
220
|
-
log('G', 'e', e.message);
|
|
221
|
-
$('L').classList.remove('on');
|
|
222
|
-
$('D').classList.add('on', 'err');
|
|
223
|
-
$('dT').textContent = 'Error';
|
|
224
|
-
$('dM').textContent = e.message;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
</script>
|
|
228
|
-
</body>
|
|
229
|
-
</html>
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html data-piclet data-width="400" data-height="420">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
-
<title>Extract Frames</title>
|
|
7
|
-
<link rel="stylesheet" href="/css/theme.css">
|
|
8
|
-
<script src="/js/piclet.js"></script>
|
|
9
|
-
<style>
|
|
10
|
-
.preview-area{flex:1;display:flex;align-items:center;justify-content:center;background:var(--bg2);border-radius:6px;min-height:160px;overflow:hidden;position:relative}
|
|
11
|
-
.preview-area::before{content:'';position:absolute;inset:0;background:repeating-conic-gradient(#1a1a1d 0% 25%, #222 0% 50%) 50%/16px 16px;z-index:0}
|
|
12
|
-
.preview-area img{max-width:100%;max-height:100%;object-fit:contain;position:relative;z-index:1}
|
|
13
|
-
.preview-area .placeholder{position:relative;z-index:1;font-size:11px;color:var(--txt3)}
|
|
14
|
-
.preview-area .mini-sp{width:16px;height:16px;border:2px solid var(--bg3);border-top-color:var(--acc);border-radius:50%;animation:s .5s linear infinite;position:relative;z-index:1}
|
|
15
|
-
.preview-info{font-size:10px;color:var(--txt3);text-align:center;margin-top:4px;height:14px}
|
|
16
|
-
.frame-nav{display:flex;align-items:center;justify-content:center;gap:12px;margin-top:8px}
|
|
17
|
-
.frame-nav button{width:32px;height:32px;border-radius:6px;border:none;background:var(--bg3);color:var(--txt2);font-size:14px;cursor:pointer;display:flex;align-items:center;justify-content:center}
|
|
18
|
-
.frame-nav button:hover{background:var(--acc);color:#fff}
|
|
19
|
-
.frame-nav button:disabled{opacity:0.3;cursor:not-allowed}
|
|
20
|
-
.frame-nav span{font-size:12px;color:var(--txt2);min-width:80px;text-align:center}
|
|
21
|
-
.info-box{background:var(--bg2);border-radius:6px;padding:12px;margin-top:8px;text-align:center}
|
|
22
|
-
.info-box .count{font-size:24px;font-weight:600;color:var(--acc2)}
|
|
23
|
-
.info-box .label{font-size:11px;color:var(--txt3);margin-top:2px}
|
|
24
|
-
</style>
|
|
25
|
-
</head>
|
|
26
|
-
<body>
|
|
27
|
-
<div class="app">
|
|
28
|
-
<div class="hd"><b>PicLet</b><span>Extract Frames</span></div>
|
|
29
|
-
<div class="meta">
|
|
30
|
-
<div>File<b id="fn">-</b></div>
|
|
31
|
-
<div>Size<b id="sz">-</b></div>
|
|
32
|
-
</div>
|
|
33
|
-
<!-- Form -->
|
|
34
|
-
<div id="F" class="form">
|
|
35
|
-
<div class="preview-area" id="pA">
|
|
36
|
-
<span class="placeholder">Loading...</span>
|
|
37
|
-
</div>
|
|
38
|
-
<div class="preview-info" id="pI"></div>
|
|
39
|
-
<div class="frame-nav">
|
|
40
|
-
<button id="bP" title="Previous frame"><</button>
|
|
41
|
-
<span id="fN">Frame 1 / 1</span>
|
|
42
|
-
<button id="bN" title="Next frame">></button>
|
|
43
|
-
</div>
|
|
44
|
-
<div class="info-box">
|
|
45
|
-
<div class="count" id="fC">0</div>
|
|
46
|
-
<div class="label">frames will be extracted as PNG files</div>
|
|
47
|
-
</div>
|
|
48
|
-
<div class="btns">
|
|
49
|
-
<button class="btn btn-g" onclick="PicLet.close()">Cancel</button>
|
|
50
|
-
<button class="btn btn-p" onclick="apply()">Extract All</button>
|
|
51
|
-
</div>
|
|
52
|
-
</div>
|
|
53
|
-
<!-- Loading state -->
|
|
54
|
-
<div class="ld" id="L"><div class="sp"></div><span id="lT">Extracting...</span></div>
|
|
55
|
-
<!-- Log -->
|
|
56
|
-
<div class="log" id="G"></div>
|
|
57
|
-
<!-- Done state -->
|
|
58
|
-
<div class="dn" id="D">
|
|
59
|
-
<h4 id="dT"></h4>
|
|
60
|
-
<p id="dM"></p>
|
|
61
|
-
<div class="btns" style="width:100%;margin-top:8px">
|
|
62
|
-
<button class="btn btn-p" onclick="PicLet.close()">Done</button>
|
|
63
|
-
</div>
|
|
64
|
-
</div>
|
|
65
|
-
</div>
|
|
66
|
-
<script>
|
|
67
|
-
const { $, log, fetchJson, postJson } = PicLet;
|
|
68
|
-
const pA = $('pA'), pI = $('pI'), fN = $('fN'), fC = $('fC');
|
|
69
|
-
const bP = $('bP'), bN = $('bN');
|
|
70
|
-
|
|
71
|
-
let frameCount = 1;
|
|
72
|
-
let currentFrame = 0;
|
|
73
|
-
|
|
74
|
-
// Update frame display
|
|
75
|
-
function updateFrameDisplay() {
|
|
76
|
-
fN.textContent = `Frame ${currentFrame + 1} / ${frameCount}`;
|
|
77
|
-
bP.disabled = currentFrame === 0;
|
|
78
|
-
bN.disabled = currentFrame >= frameCount - 1;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Load frame preview
|
|
82
|
-
async function loadFrame(index) {
|
|
83
|
-
pA.innerHTML = '<div class="mini-sp"></div>';
|
|
84
|
-
pI.textContent = '';
|
|
85
|
-
|
|
86
|
-
try {
|
|
87
|
-
const result = await postJson('/api/preview', { frameIndex: index });
|
|
88
|
-
if (result.success && result.imageData) {
|
|
89
|
-
const img = document.createElement('img');
|
|
90
|
-
img.src = result.imageData;
|
|
91
|
-
img.alt = `Frame ${index + 1}`;
|
|
92
|
-
pA.innerHTML = '';
|
|
93
|
-
pA.appendChild(img);
|
|
94
|
-
pI.textContent = result.width && result.height ? `${result.width}x${result.height}` : '';
|
|
95
|
-
} else {
|
|
96
|
-
pA.innerHTML = '<span class="placeholder">' + (result.error || 'Preview failed') + '</span>';
|
|
97
|
-
}
|
|
98
|
-
} catch (e) {
|
|
99
|
-
pA.innerHTML = '<span class="placeholder">Preview error</span>';
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Navigation
|
|
104
|
-
bP.onclick = () => {
|
|
105
|
-
if (currentFrame > 0) {
|
|
106
|
-
currentFrame--;
|
|
107
|
-
updateFrameDisplay();
|
|
108
|
-
loadFrame(currentFrame);
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
bN.onclick = () => {
|
|
113
|
-
if (currentFrame < frameCount - 1) {
|
|
114
|
-
currentFrame++;
|
|
115
|
-
updateFrameDisplay();
|
|
116
|
-
loadFrame(currentFrame);
|
|
117
|
-
}
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
// Load initial data
|
|
121
|
-
fetchJson('/api/info').then(d => {
|
|
122
|
-
$('fn').textContent = d.fileName;
|
|
123
|
-
$('sz').textContent = d.width + 'x' + d.height;
|
|
124
|
-
frameCount = d.defaults?.frameCount || 1;
|
|
125
|
-
fC.textContent = frameCount;
|
|
126
|
-
updateFrameDisplay();
|
|
127
|
-
loadFrame(0);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
// Apply
|
|
131
|
-
async function apply() {
|
|
132
|
-
$('F').classList.add('hide');
|
|
133
|
-
$('L').classList.add('on');
|
|
134
|
-
$('G').classList.add('on');
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
const result = await postJson('/api/process', {});
|
|
138
|
-
if (result.logs) {
|
|
139
|
-
result.logs.forEach(l => log('G', l.type[0], l.message));
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
$('L').classList.remove('on');
|
|
143
|
-
$('D').classList.add('on', result.success ? 'ok' : 'err');
|
|
144
|
-
$('dT').textContent = result.success ? 'Done' : 'Failed';
|
|
145
|
-
$('dM').textContent = result.success ? result.output : result.error;
|
|
146
|
-
} catch (e) {
|
|
147
|
-
log('G', 'e', e.message);
|
|
148
|
-
$('L').classList.remove('on');
|
|
149
|
-
$('D').classList.add('on', 'err');
|
|
150
|
-
$('dT').textContent = 'Error';
|
|
151
|
-
$('dM').textContent = e.message;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
</script>
|
|
155
|
-
</body>
|
|
156
|
-
</html>
|