@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,243 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-piclet data-width="400" data-height="480">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <title>Recolor</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
+ .color-section{display:flex;align-items:center;gap:10px}
18
+ .color-section label{font-size:11px;color:var(--txt3);width:50px;flex-shrink:0}
19
+ .color-input-wrap{flex:1;display:flex;align-items:center;gap:8px}
20
+ .color-preview{width:28px;height:28px;border-radius:4px;border:1px solid var(--brd);cursor:pointer;flex-shrink:0;position:relative}
21
+ .color-preview input[type="color"]{position:absolute;opacity:0;inset:0;width:100%;height:100%;cursor:pointer}
22
+ .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}
23
+ .color-input-wrap input[type="text"]:focus{outline:none;border-color:var(--acc)}
24
+ .arrow{font-size:16px;color:var(--txt3)}
25
+ .fuzz-row{display:flex;align-items:center;gap:10px}
26
+ .fuzz-row label{font-size:11px;color:var(--txt3);width:50px;flex-shrink:0}
27
+ .fuzz-row input[type="number"]{width:50px;padding:6px;background:var(--bg2);border:1px solid var(--brd);border-radius:5px;color:var(--txt);font:inherit;font-size:12px;text-align:center}
28
+ .fuzz-row input[type="number"]:focus{outline:none;border-color:var(--acc)}
29
+ .fuzz-row input[type="range"]{flex:1;height:4px;background:var(--bg3);border-radius:3px;-webkit-appearance:none}
30
+ .fuzz-row input[type="range"]::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:14px;background:var(--acc);border-radius:50%;cursor:pointer}
31
+ .detected-hint{font-size:10px;color:var(--txt3);margin-top:-6px;padding-left:60px}
32
+ </style>
33
+ </head>
34
+ <body>
35
+ <div class="app">
36
+ <div class="hd"><b>PicLet</b><span>Recolor</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="color-section">
49
+ <label>From</label>
50
+ <div class="color-input-wrap">
51
+ <div class="color-preview" id="fP" style="background:#ffffff">
52
+ <input type="color" id="fC" value="#ffffff">
53
+ </div>
54
+ <input type="text" id="fT" value="#ffffff" placeholder="#ffffff">
55
+ </div>
56
+ <span class="arrow">→</span>
57
+ <div class="color-input-wrap">
58
+ <div class="color-preview" id="tP" style="background:#000000">
59
+ <input type="color" id="tC" value="#000000">
60
+ </div>
61
+ <input type="text" id="tT" value="#000000" placeholder="#000000">
62
+ </div>
63
+ <label style="text-align:right">To</label>
64
+ </div>
65
+ <div class="detected-hint" id="dH"></div>
66
+ <div class="fuzz-row" data-tip="Color matching tolerance - higher matches more similar colors">
67
+ <label>Tolerance</label>
68
+ <input type="number" id="fN" min="0" max="100" value="10">
69
+ <input type="range" id="fR" min="0" max="100" value="10">
70
+ </div>
71
+ </div>
72
+ <div class="btns">
73
+ <button class="btn btn-g" onclick="PicLet.close()">Cancel</button>
74
+ <button class="btn btn-p" onclick="apply()">Apply</button>
75
+ </div>
76
+ </div>
77
+ <!-- Loading state -->
78
+ <div class="ld" id="L"><div class="sp"></div><span id="lT">Recoloring...</span></div>
79
+ <!-- Log -->
80
+ <div class="log" id="G"></div>
81
+ <!-- Done state -->
82
+ <div class="dn" id="D">
83
+ <h4 id="dT"></h4>
84
+ <p id="dM"></p>
85
+ <div class="btns" style="width:100%;margin-top:8px">
86
+ <button class="btn btn-p" onclick="PicLet.close()">Done</button>
87
+ </div>
88
+ </div>
89
+ </div>
90
+ <script>
91
+ const { $, log, fetchJson, postJson } = PicLet;
92
+ const pA = $('pA'), pI = $('pI');
93
+ const fC = $('fC'), fT = $('fT'), fP = $('fP');
94
+ const tC = $('tC'), tT = $('tT'), tP = $('tP');
95
+ const fN = $('fN'), fR = $('fR');
96
+
97
+ let previewTimeout = null;
98
+ let isSliding = false;
99
+ let lastPreviewOpts = null;
100
+
101
+ // From color
102
+ fC.oninput = () => {
103
+ fT.value = fC.value;
104
+ fP.style.background = fC.value;
105
+ schedulePreview();
106
+ };
107
+ fT.oninput = () => {
108
+ if (/^#[0-9a-fA-F]{6}$/.test(fT.value)) {
109
+ fC.value = fT.value;
110
+ fP.style.background = fT.value;
111
+ schedulePreview();
112
+ }
113
+ };
114
+ fT.onblur = schedulePreview;
115
+
116
+ // To color
117
+ tC.oninput = () => {
118
+ tT.value = tC.value;
119
+ tP.style.background = tC.value;
120
+ schedulePreview();
121
+ };
122
+ tT.oninput = () => {
123
+ if (/^#[0-9a-fA-F]{6}$/.test(tT.value)) {
124
+ tC.value = tT.value;
125
+ tP.style.background = tT.value;
126
+ schedulePreview();
127
+ }
128
+ };
129
+ tT.onblur = schedulePreview;
130
+
131
+ // Fuzz
132
+ fN.oninput = () => { fR.value = fN.value; schedulePreview(); };
133
+ fR.oninput = () => { fN.value = fR.value; };
134
+ fR.addEventListener('mousedown', () => { isSliding = true; });
135
+ fR.addEventListener('mouseup', () => { isSliding = false; schedulePreview(); });
136
+ fR.addEventListener('mouseleave', () => { if (isSliding) { isSliding = false; schedulePreview(); } });
137
+
138
+ // Get current options
139
+ function getOptions() {
140
+ return {
141
+ fromColor: fT.value || '#ffffff',
142
+ toColor: tT.value || '#000000',
143
+ fuzz: +fN.value || 10
144
+ };
145
+ }
146
+
147
+ // Check if options changed
148
+ function optionsChanged() {
149
+ const current = JSON.stringify(getOptions());
150
+ if (current === lastPreviewOpts) return false;
151
+ lastPreviewOpts = current;
152
+ return true;
153
+ }
154
+
155
+ // Schedule preview
156
+ function schedulePreview() {
157
+ if (isSliding) return;
158
+ if (previewTimeout) clearTimeout(previewTimeout);
159
+ previewTimeout = setTimeout(() => {
160
+ if (!isSliding && optionsChanged()) {
161
+ generatePreview();
162
+ }
163
+ }, 300);
164
+ }
165
+
166
+ // Generate preview
167
+ async function generatePreview() {
168
+ pA.innerHTML = '<div class="mini-sp"></div>';
169
+ pI.textContent = '';
170
+
171
+ try {
172
+ const result = await postJson('/api/preview', getOptions());
173
+
174
+ if (result.success && result.imageData) {
175
+ const img = document.createElement('img');
176
+ img.src = result.imageData;
177
+ img.alt = 'Preview';
178
+ pA.innerHTML = '';
179
+ pA.appendChild(img);
180
+ pI.textContent = result.width && result.height ? `${result.width}×${result.height}` : '';
181
+ } else {
182
+ pA.innerHTML = '<span class="placeholder">' + (result.error || 'Preview failed') + '</span>';
183
+ pI.textContent = '';
184
+ }
185
+ } catch (e) {
186
+ pA.innerHTML = '<span class="placeholder">Preview error</span>';
187
+ pI.textContent = '';
188
+ }
189
+ }
190
+
191
+ // Load initial data
192
+ fetchJson('/api/info').then(d => {
193
+ $('fn').textContent = d.fileName;
194
+ $('sz').textContent = d.width + '×' + d.height;
195
+
196
+ if (d.borderColor) {
197
+ $('dH').textContent = 'Detected corner: ' + d.borderColor;
198
+ }
199
+
200
+ if (d.defaults) {
201
+ const from = d.defaults.fromColor || '#ffffff';
202
+ fC.value = from.startsWith('#') ? from : '#ffffff';
203
+ fT.value = from;
204
+ fP.style.background = from;
205
+
206
+ const to = d.defaults.toColor || '#000000';
207
+ tC.value = to.startsWith('#') ? to : '#000000';
208
+ tT.value = to;
209
+ tP.style.background = to;
210
+
211
+ fN.value = fR.value = d.defaults.fuzz || 10;
212
+ }
213
+
214
+ setTimeout(generatePreview, 100);
215
+ });
216
+
217
+ // Apply
218
+ async function apply() {
219
+ $('F').classList.add('hide');
220
+ $('L').classList.add('on');
221
+ $('G').classList.add('on');
222
+
223
+ try {
224
+ const result = await postJson('/api/process', getOptions());
225
+ if (result.logs) {
226
+ result.logs.forEach(l => log('G', l.type[0], l.message));
227
+ }
228
+
229
+ $('L').classList.remove('on');
230
+ $('D').classList.add('on', result.success ? 'ok' : 'err');
231
+ $('dT').textContent = result.success ? 'Done' : 'Failed';
232
+ $('dM').textContent = result.success ? result.output : result.error;
233
+ } catch (e) {
234
+ log('G', 'e', e.message);
235
+ $('L').classList.remove('on');
236
+ $('D').classList.add('on', 'err');
237
+ $('dT').textContent = 'Error';
238
+ $('dM').textContent = e.message;
239
+ }
240
+ }
241
+ </script>
242
+ </body>
243
+ </html>
@@ -1,178 +1,178 @@
1
- <!DOCTYPE html>
2
- <html lang="en" 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>Remove BG</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
- </style>
17
- </head>
18
- <body>
19
- <div class="app">
20
- <div class="hd"><b>PicLet</b><span>Remove Background</span></div>
21
- <div class="meta">
22
- <div>File<b id="fn">-</b></div>
23
- <div>Size<b id="sz">-</b></div>
24
- </div>
25
- <!-- Form with inline preview -->
26
- <div id="F" class="form">
27
- <div class="preview-area" id="pA">
28
- <span class="placeholder">Adjust fuzz to preview</span>
29
- </div>
30
- <div class="preview-info" id="pI"></div>
31
- <div class="row" data-tip="Color matching sensitivity (higher = more colors removed)">
32
- <label>Tolerance</label>
33
- <input type="number" id="fN" min="0" max="100" value="10">
34
- <input type="range" id="fR" min="0" max="100" value="10">
35
- </div>
36
- <div class="opts">
37
- <label class="opt" data-tip="Remove empty edges after background removal"><input type="checkbox" id="cT" checked><span class="box"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,3"/></svg></span>Auto-trim</label>
38
- <label class="opt" data-tip="Only remove background from edges, keep inner areas"><input type="checkbox" id="cP"><span class="box"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,3"/></svg></span>Edges only</label>
39
- <label class="opt" data-tip="Add transparent padding to make output square"><input type="checkbox" id="cS"><span class="box"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,3"/></svg></span>Make square</label>
40
- </div>
41
- <div class="btns">
42
- <button class="btn btn-g" onclick="PicLet.close()">Cancel</button>
43
- <button class="btn btn-p" id="applyBtn" onclick="apply()">Apply</button>
44
- </div>
45
- </div>
46
- <!-- Loading state -->
47
- <div class="ld" id="L"><div class="sp"></div><span id="lT">Processing...</span></div>
48
- <!-- Log -->
49
- <div class="log" id="G"></div>
50
- <!-- Done state -->
51
- <div class="dn" id="D">
52
- <h4 id="dT"></h4>
53
- <p id="dM"></p>
54
- <div class="btns" style="width:100%;margin-top:8px">
55
- <button class="btn btn-p" onclick="PicLet.close()">Done</button>
56
- </div>
57
- </div>
58
- </div>
59
- <script>
60
- const { $, log, fetchJson, postJson } = PicLet;
61
- const fN = $('fN'), fR = $('fR'), pA = $('pA'), pI = $('pI');
62
-
63
- let previewTimeout = null;
64
- let isSliding = false;
65
- let lastPreviewOpts = null;
66
-
67
- // Sync slider and input
68
- fN.oninput = () => { fR.value = fN.value; schedulePreview(); };
69
- fR.oninput = () => { fN.value = fR.value; };
70
-
71
- // Track mouse state on slider
72
- fR.addEventListener('mousedown', () => { isSliding = true; });
73
- fR.addEventListener('mouseup', () => { isSliding = false; schedulePreview(); });
74
- fR.addEventListener('mouseleave', () => { if (isSliding) { isSliding = false; schedulePreview(); } });
75
-
76
- // Checkbox changes trigger preview
77
- $('cT').onchange = schedulePreview;
78
- $('cP').onchange = schedulePreview;
79
- $('cS').onchange = schedulePreview;
80
-
81
- // Get current options
82
- function getOptions() {
83
- return {
84
- fuzz: +fN.value || 10,
85
- trim: $('cT').checked,
86
- preserveInner: $('cP').checked,
87
- makeSquare: $('cS').checked
88
- };
89
- }
90
-
91
- // Check if options changed
92
- function optionsChanged() {
93
- const current = JSON.stringify(getOptions());
94
- if (current === lastPreviewOpts) return false;
95
- lastPreviewOpts = current;
96
- return true;
97
- }
98
-
99
- // Schedule preview (debounced, only when not sliding)
100
- function schedulePreview() {
101
- if (isSliding) return;
102
- if (previewTimeout) clearTimeout(previewTimeout);
103
- previewTimeout = setTimeout(() => {
104
- if (!isSliding && optionsChanged()) {
105
- generatePreview();
106
- }
107
- }, 300);
108
- }
109
-
110
- // Generate preview
111
- async function generatePreview() {
112
- // Show loading spinner
113
- pA.innerHTML = '<div class="mini-sp"></div>';
114
- pI.textContent = '';
115
-
116
- try {
117
- const result = await postJson('/api/preview', getOptions());
118
-
119
- if (result.success && result.imageData) {
120
- const img = document.createElement('img');
121
- img.src = result.imageData;
122
- img.alt = 'Preview';
123
- pA.innerHTML = '';
124
- pA.appendChild(img);
125
- pI.textContent = result.width && result.height ? `${result.width}×${result.height}` : '';
126
- } else {
127
- pA.innerHTML = '<span class="placeholder">' + (result.error || 'Preview failed') + '</span>';
128
- pI.textContent = '';
129
- }
130
- } catch (e) {
131
- pA.innerHTML = '<span class="placeholder">Preview error</span>';
132
- pI.textContent = '';
133
- }
134
- }
135
-
136
- // Load initial data
137
- fetchJson('/api/info').then(d => {
138
- $('fn').textContent = d.fileName;
139
- $('sz').textContent = d.width + '×' + d.height;
140
- if (d.defaults) {
141
- fN.value = fR.value = d.defaults.fuzz;
142
- $('cT').checked = d.defaults.trim;
143
- $('cP').checked = d.defaults.preserveInner;
144
- $('cS').checked = d.defaults.makeSquare;
145
- }
146
- // Generate initial preview
147
- setTimeout(generatePreview, 100);
148
- });
149
-
150
- // Apply (process for real)
151
- async function apply() {
152
- $('F').classList.add('hide');
153
- $('L').classList.add('on');
154
- $('lT').textContent = 'Processing...';
155
- $('G').classList.add('on');
156
-
157
- try {
158
- const result = await postJson('/api/process', getOptions());
159
-
160
- if (result.logs) {
161
- result.logs.forEach(l => log('G', l.type[0], l.message));
162
- }
163
-
164
- $('L').classList.remove('on');
165
- $('D').classList.add('on', result.success ? 'ok' : 'err');
166
- $('dT').textContent = result.success ? 'Done' : 'Failed';
167
- $('dM').textContent = result.success ? result.output : result.error;
168
- } catch (e) {
169
- log('G', 'e', e.message);
170
- $('L').classList.remove('on');
171
- $('D').classList.add('on', 'err');
172
- $('dT').textContent = 'Error';
173
- $('dM').textContent = e.message;
174
- }
175
- }
176
- </script>
177
- </body>
178
- </html>
1
+ <!DOCTYPE html>
2
+ <html lang="en" 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>Remove BG</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
+ </style>
17
+ </head>
18
+ <body>
19
+ <div class="app">
20
+ <div class="hd"><b>PicLet</b><span>Remove Background</span></div>
21
+ <div class="meta">
22
+ <div>File<b id="fn">-</b></div>
23
+ <div>Size<b id="sz">-</b></div>
24
+ </div>
25
+ <!-- Form with inline preview -->
26
+ <div id="F" class="form">
27
+ <div class="preview-area" id="pA">
28
+ <span class="placeholder">Adjust fuzz to preview</span>
29
+ </div>
30
+ <div class="preview-info" id="pI"></div>
31
+ <div class="row" data-tip="Color matching sensitivity (higher = more colors removed)">
32
+ <label>Tolerance</label>
33
+ <input type="number" id="fN" min="0" max="100" value="10">
34
+ <input type="range" id="fR" min="0" max="100" value="10">
35
+ </div>
36
+ <div class="opts">
37
+ <label class="opt" data-tip="Remove empty edges after background removal"><input type="checkbox" id="cT" checked><span class="box"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,3"/></svg></span>Auto-trim</label>
38
+ <label class="opt" data-tip="Only remove background from edges, keep inner areas"><input type="checkbox" id="cP"><span class="box"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,3"/></svg></span>Edges only</label>
39
+ <label class="opt" data-tip="Add transparent padding to make output square"><input type="checkbox" id="cS"><span class="box"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,3"/></svg></span>Make square</label>
40
+ </div>
41
+ <div class="btns">
42
+ <button class="btn btn-g" onclick="PicLet.close()">Cancel</button>
43
+ <button class="btn btn-p" id="applyBtn" onclick="apply()">Apply</button>
44
+ </div>
45
+ </div>
46
+ <!-- Loading state -->
47
+ <div class="ld" id="L"><div class="sp"></div><span id="lT">Processing...</span></div>
48
+ <!-- Log -->
49
+ <div class="log" id="G"></div>
50
+ <!-- Done state -->
51
+ <div class="dn" id="D">
52
+ <h4 id="dT"></h4>
53
+ <p id="dM"></p>
54
+ <div class="btns" style="width:100%;margin-top:8px">
55
+ <button class="btn btn-p" onclick="PicLet.close()">Done</button>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ <script>
60
+ const { $, log, fetchJson, postJson } = PicLet;
61
+ const fN = $('fN'), fR = $('fR'), pA = $('pA'), pI = $('pI');
62
+
63
+ let previewTimeout = null;
64
+ let isSliding = false;
65
+ let lastPreviewOpts = null;
66
+
67
+ // Sync slider and input
68
+ fN.oninput = () => { fR.value = fN.value; schedulePreview(); };
69
+ fR.oninput = () => { fN.value = fR.value; };
70
+
71
+ // Track mouse state on slider
72
+ fR.addEventListener('mousedown', () => { isSliding = true; });
73
+ fR.addEventListener('mouseup', () => { isSliding = false; schedulePreview(); });
74
+ fR.addEventListener('mouseleave', () => { if (isSliding) { isSliding = false; schedulePreview(); } });
75
+
76
+ // Checkbox changes trigger preview
77
+ $('cT').onchange = schedulePreview;
78
+ $('cP').onchange = schedulePreview;
79
+ $('cS').onchange = schedulePreview;
80
+
81
+ // Get current options
82
+ function getOptions() {
83
+ return {
84
+ fuzz: +fN.value || 10,
85
+ trim: $('cT').checked,
86
+ preserveInner: $('cP').checked,
87
+ makeSquare: $('cS').checked
88
+ };
89
+ }
90
+
91
+ // Check if options changed
92
+ function optionsChanged() {
93
+ const current = JSON.stringify(getOptions());
94
+ if (current === lastPreviewOpts) return false;
95
+ lastPreviewOpts = current;
96
+ return true;
97
+ }
98
+
99
+ // Schedule preview (debounced, only when not sliding)
100
+ function schedulePreview() {
101
+ if (isSliding) return;
102
+ if (previewTimeout) clearTimeout(previewTimeout);
103
+ previewTimeout = setTimeout(() => {
104
+ if (!isSliding && optionsChanged()) {
105
+ generatePreview();
106
+ }
107
+ }, 300);
108
+ }
109
+
110
+ // Generate preview
111
+ async function generatePreview() {
112
+ // Show loading spinner
113
+ pA.innerHTML = '<div class="mini-sp"></div>';
114
+ pI.textContent = '';
115
+
116
+ try {
117
+ const result = await postJson('/api/preview', getOptions());
118
+
119
+ if (result.success && result.imageData) {
120
+ const img = document.createElement('img');
121
+ img.src = result.imageData;
122
+ img.alt = 'Preview';
123
+ pA.innerHTML = '';
124
+ pA.appendChild(img);
125
+ pI.textContent = result.width && result.height ? `${result.width}×${result.height}` : '';
126
+ } else {
127
+ pA.innerHTML = '<span class="placeholder">' + (result.error || 'Preview failed') + '</span>';
128
+ pI.textContent = '';
129
+ }
130
+ } catch (e) {
131
+ pA.innerHTML = '<span class="placeholder">Preview error</span>';
132
+ pI.textContent = '';
133
+ }
134
+ }
135
+
136
+ // Load initial data
137
+ fetchJson('/api/info').then(d => {
138
+ $('fn').textContent = d.fileName;
139
+ $('sz').textContent = d.width + '×' + d.height;
140
+ if (d.defaults) {
141
+ fN.value = fR.value = d.defaults.fuzz;
142
+ $('cT').checked = d.defaults.trim;
143
+ $('cP').checked = d.defaults.preserveInner;
144
+ $('cS').checked = d.defaults.makeSquare;
145
+ }
146
+ // Generate initial preview
147
+ setTimeout(generatePreview, 100);
148
+ });
149
+
150
+ // Apply (process for real)
151
+ async function apply() {
152
+ $('F').classList.add('hide');
153
+ $('L').classList.add('on');
154
+ $('lT').textContent = 'Processing...';
155
+ $('G').classList.add('on');
156
+
157
+ try {
158
+ const result = await postJson('/api/process', getOptions());
159
+
160
+ if (result.logs) {
161
+ result.logs.forEach(l => log('G', l.type[0], l.message));
162
+ }
163
+
164
+ $('L').classList.remove('on');
165
+ $('D').classList.add('on', result.success ? 'ok' : 'err');
166
+ $('dT').textContent = result.success ? 'Done' : 'Failed';
167
+ $('dM').textContent = result.success ? result.output : result.error;
168
+ } catch (e) {
169
+ log('G', 'e', e.message);
170
+ $('L').classList.remove('on');
171
+ $('D').classList.add('on', 'err');
172
+ $('dT').textContent = 'Error';
173
+ $('dM').textContent = e.message;
174
+ }
175
+ }
176
+ </script>
177
+ </body>
178
+ </html>