@spark-apps/piclet 1.0.0 → 1.0.3

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.
@@ -0,0 +1,229 @@
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,88 +1,88 @@
1
- /* PicLet Theme */
2
- :root{--bg:#09090b;--bg2:#111113;--bg3:#1a1a1d;--brd:rgba(255,255,255,.08);--txt:#e4e4e7;--txt2:#a1a1a6;--txt3:#636366;--acc:#eab308;--acc2:#fcd34d;--ok:#10b981;--err:#ef4444}
3
- *{margin:0;padding:0;box-sizing:border-box;user-select:none;scrollbar-width:thin;scrollbar-color:var(--acc) var(--bg2)}
4
- *::-webkit-scrollbar{width:6px;height:0}
5
- *::-webkit-scrollbar-track{background:var(--bg2)}
6
- *::-webkit-scrollbar-thumb{background:var(--acc);border-radius:3px}
7
- *::-webkit-scrollbar-thumb:hover{background:var(--acc2)}
8
- html{zoom:100%!important;overflow-x:hidden}
9
- html,body{height:100%;overflow:hidden;background:var(--bg)}
10
- body{font:500 13px/1.3 -apple-system,system-ui,sans-serif;color:var(--txt);padding:12px;display:flex;flex-direction:column}
11
-
12
- /* Main container - fills window */
13
- .app{flex:1;display:flex;flex-direction:column;min-height:0}
14
-
15
- /* Header */
16
- .hd{display:flex;align-items:center;gap:6px;padding-bottom:8px;border-bottom:1px solid var(--brd);flex-shrink:0}
17
- .hd b{font-size:14px;background:linear-gradient(135deg,#eab308,#fbbf24);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
18
- .hd span{font-size:11px;color:var(--txt3)}
19
-
20
- /* Meta info */
21
- .meta{display:flex;gap:12px;font-size:11px;color:var(--txt3);padding:8px 0;flex-shrink:0}
22
- .meta b{color:var(--txt2);font-weight:500;margin-left:3px}
23
-
24
- /* Form - flexes to fill */
25
- .form{flex:1;display:flex;flex-direction:column;gap:10px;min-height:0}
26
-
27
- /* Input row */
28
- .row{display:flex;align-items:center;gap:10px;flex-shrink:0}
29
- .row label{font-size:11px;color:var(--txt3);width:36px;flex-shrink:0}
30
- input[type="number"]{width:44px;padding:6px;background:var(--bg2);border:1px solid var(--brd);border-radius:5px;color:var(--txt);font:inherit;font-size:13px;text-align:center}
31
- input[type="number"]:focus{outline:none;border-color:var(--acc)}
32
- input[type="range"]{flex:1;height:4px;background:var(--bg3);border-radius:3px;-webkit-appearance:none}
33
- input[type="range"]::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:14px;background:var(--acc);border-radius:50%;cursor:pointer}
34
-
35
- /* Options - flexes */
36
- .opts{display:flex;gap:10px;flex-wrap:wrap;padding:4px 0;flex:1;align-content:flex-start}
37
- .opt{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--txt2);cursor:pointer;height:fit-content}
38
- .opt:hover{color:var(--txt)}
39
- .opt input{display:none}
40
- .box{width:16px;height:16px;background:var(--bg2);border:1px solid var(--brd);border-radius:4px;display:grid;place-items:center;flex-shrink:0;transition:all .15s}
41
- .opt input:checked+.box{background:var(--acc);border-color:var(--acc)}
42
- .box svg{width:10px;height:10px;fill:none;stroke:#000;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round;opacity:0;transition:opacity .15s}
43
- .opt input:checked+.box svg{opacity:1}
44
-
45
- /* Buttons - anchored bottom */
46
- .btns{display:flex;gap:8px;flex-shrink:0;margin-top:auto}
47
- .btn{flex:1;padding:10px 12px;border:none;border-radius:6px;font:inherit;font-size:13px;font-weight:500;cursor:pointer}
48
- .btn:active{transform:scale(.98)}
49
- .btn-g{background:transparent;color:var(--txt3);border:1px solid var(--brd)}
50
- .btn-g:hover{background:var(--bg2);color:var(--txt2)}
51
- .btn-p{background:var(--acc);color:#fff}
52
- .btn-p:hover{background:var(--acc2)}
53
-
54
- /* Loading state - centers in available space */
55
- .ld{display:none;flex:1;flex-direction:column;align-items:center;justify-content:center;gap:8px}
56
- .ld.on{display:flex}
57
- .sp{width:20px;height:20px;border:2px solid var(--bg3);border-top-color:var(--acc);border-radius:50%;animation:s .5s linear infinite}
58
- @keyframes s{to{transform:rotate(360deg)}}
59
- .ld span{font-size:12px;color:var(--txt3)}
60
-
61
- /* Log - fills available space */
62
- .log{display:none;font:11px/1.4 Consolas,monospace;background:var(--bg2);border-radius:5px;padding:8px;flex:1;min-height:60px;max-height:none;overflow-y:auto}
63
- .log.on{display:block}
64
- .log p{margin:2px 0}
65
- .log .i{color:var(--txt3)}
66
- .log .s{color:var(--ok)}
67
- .log .e{color:var(--err)}
68
-
69
- /* Done state */
70
- .dn{display:none;flex:1;flex-direction:column;align-items:center;justify-content:center;gap:4px}
71
- .dn.on{display:flex}
72
- .dn h4{font-size:14px}
73
- .dn.ok h4{color:var(--ok)}
74
- .dn.err h4{color:var(--err)}
75
- .dn p{font-size:12px;color:var(--txt2);text-align:center}
76
-
77
- /* Warning/info box */
78
- .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}
79
- .warn-box.on{display:block}
80
-
81
- /* Tooltips */
82
- .opt{position:relative}
83
- .opt[data-tip]:hover::after{content:attr(data-tip);position:absolute;bottom:100%;left:50%;transform:translateX(-50%);background:var(--bg3);color:var(--txt2);font-size:10px;padding:4px 8px;border-radius:4px;white-space:nowrap;margin-bottom:4px;z-index:10;pointer-events:none}
84
- .opt[data-tip]:hover::before{content:'';position:absolute;bottom:100%;left:50%;transform:translateX(-50%);border:4px solid transparent;border-top-color:var(--bg3);margin-bottom:-4px;z-index:10}
85
- .row[data-tip]{position:relative}
86
- .row[data-tip]:hover::after{content:attr(data-tip);position:absolute;bottom:100%;left:50%;transform:translateX(-50%);background:var(--bg3);color:var(--txt2);font-size:10px;padding:4px 8px;border-radius:4px;white-space:nowrap;margin-bottom:4px;z-index:10;pointer-events:none}
87
-
88
- .hide{display:none!important}
1
+ /* PicLet Theme */
2
+ :root{--bg:#09090b;--bg2:#111113;--bg3:#1a1a1d;--brd:rgba(255,255,255,.08);--txt:#e4e4e7;--txt2:#a1a1a6;--txt3:#636366;--acc:#eab308;--acc2:#fcd34d;--ok:#10b981;--err:#ef4444}
3
+ *{margin:0;padding:0;box-sizing:border-box;user-select:none;scrollbar-width:thin;scrollbar-color:var(--acc) var(--bg2)}
4
+ *::-webkit-scrollbar{width:6px;height:0}
5
+ *::-webkit-scrollbar-track{background:var(--bg2)}
6
+ *::-webkit-scrollbar-thumb{background:var(--acc);border-radius:3px}
7
+ *::-webkit-scrollbar-thumb:hover{background:var(--acc2)}
8
+ html{zoom:100%!important;overflow-x:hidden}
9
+ html,body{height:100%;overflow:hidden;background:var(--bg)}
10
+ body{font:500 13px/1.3 -apple-system,system-ui,sans-serif;color:var(--txt);padding:12px;display:flex;flex-direction:column}
11
+
12
+ /* Main container - fills window */
13
+ .app{flex:1;display:flex;flex-direction:column;min-height:0}
14
+
15
+ /* Header */
16
+ .hd{display:flex;align-items:center;gap:6px;padding-bottom:8px;border-bottom:1px solid var(--brd);flex-shrink:0}
17
+ .hd b{font-size:14px;background:linear-gradient(135deg,#eab308,#fbbf24);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
18
+ .hd span{font-size:11px;color:var(--txt3)}
19
+
20
+ /* Meta info (legacy - kept for other pages) */
21
+ .meta{display:flex;gap:12px;font-size:11px;color:var(--txt3);padding:8px 0;flex-shrink:0}
22
+ .meta b{color:var(--txt2);font-weight:500;margin-left:3px}
23
+
24
+ /* Form - flexes to fill */
25
+ .form{flex:1;display:flex;flex-direction:column;gap:10px;min-height:0}
26
+
27
+ /* Input row */
28
+ .row{display:flex;align-items:center;gap:10px;flex-shrink:0}
29
+ .row label{font-size:11px;color:var(--txt3);width:36px;flex-shrink:0}
30
+ input[type="number"]{width:44px;padding:6px;background:var(--bg2);border:1px solid var(--brd);border-radius:5px;color:var(--txt);font:inherit;font-size:13px;text-align:center}
31
+ input[type="number"]:focus{outline:none;border-color:var(--acc)}
32
+ input[type="range"]{flex:1;height:4px;background:var(--bg3);border-radius:3px;-webkit-appearance:none}
33
+ input[type="range"]::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:14px;background:var(--acc);border-radius:50%;cursor:pointer}
34
+
35
+ /* Options - flexes */
36
+ .opts{display:flex;gap:10px;flex-wrap:wrap;padding:4px 0;flex:1;align-content:flex-start}
37
+ .opt{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--txt2);cursor:pointer;height:fit-content}
38
+ .opt:hover{color:var(--txt)}
39
+ .opt input{display:none}
40
+ .box{width:16px;height:16px;background:var(--bg2);border:1px solid var(--brd);border-radius:4px;display:grid;place-items:center;flex-shrink:0;transition:all .15s}
41
+ .opt input:checked+.box{background:var(--acc);border-color:var(--acc)}
42
+ .box svg{width:10px;height:10px;fill:none;stroke:#000;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round;opacity:0;transition:opacity .15s}
43
+ .opt input:checked+.box svg{opacity:1}
44
+
45
+ /* Buttons - anchored bottom */
46
+ .btns{display:flex;gap:8px;flex-shrink:0;margin-top:auto}
47
+ .btn{flex:1;padding:10px 12px;border:none;border-radius:6px;font:inherit;font-size:13px;font-weight:500;cursor:pointer}
48
+ .btn:active{transform:scale(.98)}
49
+ .btn-g{background:transparent;color:var(--txt3);border:1px solid var(--brd)}
50
+ .btn-g:hover{background:var(--bg2);color:var(--txt2)}
51
+ .btn-p{background:var(--acc);color:#fff}
52
+ .btn-p:hover{background:var(--acc2)}
53
+
54
+ /* Loading state - centers in available space */
55
+ .ld{display:none;flex:1;flex-direction:column;align-items:center;justify-content:center;gap:8px}
56
+ .ld.on{display:flex}
57
+ .sp{width:20px;height:20px;border:2px solid var(--bg3);border-top-color:var(--acc);border-radius:50%;animation:s .5s linear infinite}
58
+ @keyframes s{to{transform:rotate(360deg)}}
59
+ .ld span{font-size:12px;color:var(--txt3)}
60
+
61
+ /* Log - fills available space */
62
+ .log{display:none;font:11px/1.4 Consolas,monospace;background:var(--bg2);border-radius:5px;padding:8px;flex:1;min-height:60px;max-height:none;overflow-y:auto}
63
+ .log.on{display:block}
64
+ .log p{margin:2px 0}
65
+ .log .i{color:var(--txt3)}
66
+ .log .s{color:var(--ok)}
67
+ .log .e{color:var(--err)}
68
+
69
+ /* Done state */
70
+ .dn{display:none;flex:1;flex-direction:column;align-items:center;justify-content:center;gap:4px}
71
+ .dn.on{display:flex}
72
+ .dn h4{font-size:14px}
73
+ .dn.ok h4{color:var(--ok)}
74
+ .dn.err h4{color:var(--err)}
75
+ .dn p{font-size:12px;color:var(--txt2);text-align:center}
76
+
77
+ /* Warning/info box */
78
+ .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}
79
+ .warn-box.on{display:block}
80
+
81
+ /* Tooltips */
82
+ .opt{position:relative}
83
+ .opt[data-tip]:hover::after{content:attr(data-tip);position:absolute;bottom:100%;left:50%;transform:translateX(-50%);background:var(--bg3);color:var(--txt2);font-size:10px;padding:4px 8px;border-radius:4px;white-space:nowrap;margin-bottom:4px;z-index:10;pointer-events:none}
84
+ .opt[data-tip]:hover::before{content:'';position:absolute;bottom:100%;left:50%;transform:translateX(-50%);border:4px solid transparent;border-top-color:var(--bg3);margin-bottom:-4px;z-index:10}
85
+ .row[data-tip]{position:relative}
86
+ .row[data-tip]:hover::after{content:attr(data-tip);position:absolute;bottom:100%;left:50%;transform:translateX(-50%);background:var(--bg3);color:var(--txt2);font-size:10px;padding:4px 8px;border-radius:4px;white-space:nowrap;margin-bottom:4px;z-index:10;pointer-events:none}
87
+
88
+ .hide{display:none!important}
@@ -0,0 +1,156 @@
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">&lt;</button>
41
+ <span id="fN">Frame 1 / 1</span>
42
+ <button id="bN" title="Next frame">&gt;</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>