@spark-apps/piclet 1.0.0
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/LICENSE +21 -0
- package/Readme.md +180 -0
- package/dist/cli.js +2989 -0
- package/dist/cli.js.map +1 -0
- package/dist/gui/css/theme.css +88 -0
- package/dist/gui/iconpack.html +113 -0
- package/dist/gui/img/logo.ico +0 -0
- package/dist/gui/js/piclet.js +75 -0
- package/dist/gui/loading.hta +96 -0
- package/dist/gui/makeicon.html +165 -0
- package/dist/gui/piclet.html +998 -0
- package/dist/gui/remove-bg.html +178 -0
- package/dist/gui/rescale.html +195 -0
- package/dist/gui/storepack.html +179 -0
- package/dist/icons/banana.ico +0 -0
- package/dist/icons/iconpack.ico +0 -0
- package/dist/icons/makeicon.ico +0 -0
- package/dist/icons/removebg.ico +0 -0
- package/dist/icons/rescale.ico +0 -0
- package/dist/icons/storepack.ico +0 -0
- package/dist/launcher.vbs +44 -0
- package/package.json +72 -0
|
@@ -0,0 +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>
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html 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>Scale Image</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
|
+
.dim-section{display:flex;flex-direction:column;gap:8px}
|
|
17
|
+
.dim-row{display:flex;align-items:center;gap:8px}
|
|
18
|
+
.dim-row label{font-size:11px;color:var(--txt3);width:44px;flex-shrink:0}
|
|
19
|
+
.dim-row input[type="range"]{flex:1;height:4px;background:var(--bg3);border-radius:3px;-webkit-appearance:none}
|
|
20
|
+
.dim-row input[type="range"]::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:14px;background:var(--acc);border-radius:50%;cursor:pointer}
|
|
21
|
+
.dim-row .val{width:58px;font-size:12px;color:var(--acc2);text-align:right;font-weight:500}
|
|
22
|
+
</style>
|
|
23
|
+
</head>
|
|
24
|
+
<body>
|
|
25
|
+
<div class="app">
|
|
26
|
+
<div class="hd"><b>PicLet</b><span>Scale Image</span></div>
|
|
27
|
+
<div class="meta">
|
|
28
|
+
<div>File<b id="fn">-</b></div>
|
|
29
|
+
<div>Size<b id="sz">-</b></div>
|
|
30
|
+
</div>
|
|
31
|
+
<!-- Form -->
|
|
32
|
+
<div id="F" class="form">
|
|
33
|
+
<div class="preview-area" id="pA">
|
|
34
|
+
<span class="placeholder">Adjust to preview</span>
|
|
35
|
+
</div>
|
|
36
|
+
<div class="preview-info" id="pI"></div>
|
|
37
|
+
<div class="dim-section">
|
|
38
|
+
<div class="dim-row">
|
|
39
|
+
<label>Width</label>
|
|
40
|
+
<input type="range" id="wR" min="16" max="1000" value="500">
|
|
41
|
+
<span class="val" id="wV">500px</span>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="dim-row">
|
|
44
|
+
<label>Height</label>
|
|
45
|
+
<input type="range" id="hR" min="16" max="1000" value="500">
|
|
46
|
+
<span class="val" id="hV">500px</span>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="opts">
|
|
50
|
+
<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>Square output</label>
|
|
51
|
+
</div>
|
|
52
|
+
<div class="btns">
|
|
53
|
+
<button class="btn btn-g" onclick="PicLet.close()">Cancel</button>
|
|
54
|
+
<button class="btn btn-p" onclick="apply()">Apply</button>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
<!-- Loading state -->
|
|
58
|
+
<div class="ld" id="L"><div class="sp"></div><span id="lT">Scaling...</span></div>
|
|
59
|
+
<!-- Log -->
|
|
60
|
+
<div class="log" id="G"></div>
|
|
61
|
+
<!-- Done state -->
|
|
62
|
+
<div class="dn" id="D">
|
|
63
|
+
<h4 id="dT"></h4>
|
|
64
|
+
<p id="dM"></p>
|
|
65
|
+
<div class="btns" style="width:100%;margin-top:8px">
|
|
66
|
+
<button class="btn btn-p" onclick="PicLet.close()">Done</button>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
<script>
|
|
71
|
+
const { $, log, fetchJson, postJson } = PicLet;
|
|
72
|
+
const wR = $('wR'), hR = $('hR'), wV = $('wV'), hV = $('hV');
|
|
73
|
+
const pA = $('pA'), pI = $('pI');
|
|
74
|
+
|
|
75
|
+
let origW = 1000, origH = 1000;
|
|
76
|
+
let previewTimeout = null;
|
|
77
|
+
let isSliding = false;
|
|
78
|
+
let lastPreviewOpts = null;
|
|
79
|
+
|
|
80
|
+
// Update value displays
|
|
81
|
+
function updateValues() {
|
|
82
|
+
wV.textContent = wR.value + 'px';
|
|
83
|
+
hV.textContent = hR.value + 'px';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Slider input handlers
|
|
87
|
+
wR.oninput = updateValues;
|
|
88
|
+
hR.oninput = updateValues;
|
|
89
|
+
|
|
90
|
+
// Track sliding state
|
|
91
|
+
[wR, hR].forEach(slider => {
|
|
92
|
+
slider.addEventListener('mousedown', () => { isSliding = true; });
|
|
93
|
+
slider.addEventListener('mouseup', () => { isSliding = false; schedulePreview(); });
|
|
94
|
+
slider.addEventListener('mouseleave', () => { if (isSliding) { isSliding = false; schedulePreview(); } });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Checkbox change
|
|
98
|
+
$('cS').onchange = schedulePreview;
|
|
99
|
+
|
|
100
|
+
// Get current options
|
|
101
|
+
function getOptions() {
|
|
102
|
+
return { width: +wR.value, height: +hR.value, makeSquare: $('cS').checked };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Check if options changed
|
|
106
|
+
function optionsChanged() {
|
|
107
|
+
const current = JSON.stringify(getOptions());
|
|
108
|
+
if (current === lastPreviewOpts) return false;
|
|
109
|
+
lastPreviewOpts = current;
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Schedule preview
|
|
114
|
+
function schedulePreview() {
|
|
115
|
+
if (isSliding) return;
|
|
116
|
+
if (previewTimeout) clearTimeout(previewTimeout);
|
|
117
|
+
previewTimeout = setTimeout(() => {
|
|
118
|
+
if (!isSliding && optionsChanged()) {
|
|
119
|
+
generatePreview();
|
|
120
|
+
}
|
|
121
|
+
}, 300);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Generate preview
|
|
125
|
+
async function generatePreview() {
|
|
126
|
+
pA.innerHTML = '<div class="mini-sp"></div>';
|
|
127
|
+
pI.textContent = '';
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const result = await postJson('/api/preview', getOptions());
|
|
131
|
+
|
|
132
|
+
if (result.success && result.imageData) {
|
|
133
|
+
const img = document.createElement('img');
|
|
134
|
+
img.src = result.imageData;
|
|
135
|
+
img.alt = 'Preview';
|
|
136
|
+
pA.innerHTML = '';
|
|
137
|
+
pA.appendChild(img);
|
|
138
|
+
pI.textContent = result.width && result.height ? `${result.width}×${result.height}` : '';
|
|
139
|
+
} else {
|
|
140
|
+
pA.innerHTML = '<span class="placeholder">' + (result.error || 'Preview failed') + '</span>';
|
|
141
|
+
pI.textContent = '';
|
|
142
|
+
}
|
|
143
|
+
} catch (e) {
|
|
144
|
+
pA.innerHTML = '<span class="placeholder">Preview error</span>';
|
|
145
|
+
pI.textContent = '';
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Load initial data
|
|
150
|
+
fetchJson('/api/info').then(d => {
|
|
151
|
+
$('fn').textContent = d.fileName;
|
|
152
|
+
$('sz').textContent = d.width + '×' + d.height;
|
|
153
|
+
origW = d.width;
|
|
154
|
+
origH = d.height;
|
|
155
|
+
|
|
156
|
+
// Max = original dimensions
|
|
157
|
+
wR.max = origW;
|
|
158
|
+
hR.max = origH;
|
|
159
|
+
|
|
160
|
+
// Default = original size
|
|
161
|
+
wR.value = origW;
|
|
162
|
+
hR.value = origH;
|
|
163
|
+
updateValues();
|
|
164
|
+
|
|
165
|
+
// Generate initial preview
|
|
166
|
+
setTimeout(generatePreview, 100);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Apply
|
|
170
|
+
async function apply() {
|
|
171
|
+
$('F').classList.add('hide');
|
|
172
|
+
$('L').classList.add('on');
|
|
173
|
+
$('G').classList.add('on');
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
const result = await postJson('/api/process', getOptions());
|
|
177
|
+
if (result.logs) {
|
|
178
|
+
result.logs.forEach(l => log('G', l.type[0], l.message));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
$('L').classList.remove('on');
|
|
182
|
+
$('D').classList.add('on', result.success ? 'ok' : 'err');
|
|
183
|
+
$('dT').textContent = result.success ? 'Done' : 'Failed';
|
|
184
|
+
$('dM').textContent = result.success ? result.output : result.error;
|
|
185
|
+
} catch (e) {
|
|
186
|
+
log('G', 'e', e.message);
|
|
187
|
+
$('L').classList.remove('on');
|
|
188
|
+
$('D').classList.add('on', 'err');
|
|
189
|
+
$('dT').textContent = 'Error';
|
|
190
|
+
$('dM').textContent = e.message;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
</script>
|
|
194
|
+
</body>
|
|
195
|
+
</html>
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html data-piclet data-width="380" data-height="560">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<title>Store Pack</title>
|
|
7
|
+
<link rel="stylesheet" href="/css/theme.css">
|
|
8
|
+
<script src="/js/piclet.js"></script>
|
|
9
|
+
<style>
|
|
10
|
+
.preset-select{width:100%;padding:10px 12px;background:var(--bg2);border:1px solid var(--brd);border-radius:6px;color:var(--txt);font:inherit;font-size:13px;cursor:pointer}
|
|
11
|
+
.preset-select:focus{outline:none;border-color:var(--acc)}
|
|
12
|
+
.preset-desc{font-size:11px;color:var(--txt3);margin-top:4px}
|
|
13
|
+
.icon-list{height:140px;background:var(--bg2);border-radius:6px;padding:8px;overflow-y:auto;font:11px/1.5 Consolas,monospace}
|
|
14
|
+
.icon-list div{display:flex;justify-content:space-between;padding:2px 4px;border-radius:3px}
|
|
15
|
+
.icon-list div:hover{background:var(--bg3)}
|
|
16
|
+
.icon-list .name{color:var(--txt2)}
|
|
17
|
+
.icon-list .size{color:var(--acc2);font-weight:500}
|
|
18
|
+
.icon-list .ratio{color:var(--txt3);font-size:10px;margin-left:4px}
|
|
19
|
+
.section-label{font-size:10px;color:var(--txt3);text-transform:uppercase;letter-spacing:0.5px;margin:8px 0 4px}
|
|
20
|
+
.scale-opts{display:flex;flex-direction:column;gap:4px}
|
|
21
|
+
.scale-opt{display:flex;align-items:center;gap:8px;padding:8px;background:var(--bg2);border:1px solid var(--brd);border-radius:5px;cursor:pointer;font-size:12px;color:var(--txt2)}
|
|
22
|
+
.scale-opt:hover{border-color:var(--acc);color:var(--txt)}
|
|
23
|
+
.scale-opt input{display:none}
|
|
24
|
+
.scale-opt input:checked+.radio{background:var(--acc);border-color:var(--acc)}
|
|
25
|
+
.scale-opt input:checked+.radio::after{opacity:1}
|
|
26
|
+
.radio{width:14px;height:14px;border:1px solid var(--brd);border-radius:50%;position:relative;flex-shrink:0}
|
|
27
|
+
.radio::after{content:'';position:absolute;top:3px;left:3px;width:6px;height:6px;background:#fff;border-radius:50%;opacity:0}
|
|
28
|
+
.scale-opt span{flex:1}
|
|
29
|
+
.scale-opt small{font-size:10px;color:var(--txt3)}
|
|
30
|
+
</style>
|
|
31
|
+
</head>
|
|
32
|
+
<body>
|
|
33
|
+
<div class="app">
|
|
34
|
+
<div class="hd"><b>PicLet</b><span>Store Pack Generator</span></div>
|
|
35
|
+
<div class="meta">
|
|
36
|
+
<div>File<b id="fn">-</b></div>
|
|
37
|
+
<div>Size<b id="sz">-</b></div>
|
|
38
|
+
</div>
|
|
39
|
+
<!-- Form state -->
|
|
40
|
+
<div id="F" class="form">
|
|
41
|
+
<div>
|
|
42
|
+
<select class="preset-select" id="pS" onchange="updatePreset()">
|
|
43
|
+
<option value="">Loading...</option>
|
|
44
|
+
</select>
|
|
45
|
+
<div class="preset-desc" id="pD"></div>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="section-label">Output Sizes</div>
|
|
48
|
+
<div class="icon-list" id="pI"></div>
|
|
49
|
+
<div class="section-label">Scaling Mode</div>
|
|
50
|
+
<div class="scale-opts">
|
|
51
|
+
<label class="scale-opt">
|
|
52
|
+
<input type="radio" name="scale" value="fit" checked>
|
|
53
|
+
<span class="radio"></span>
|
|
54
|
+
<span>Fit & Pad</span>
|
|
55
|
+
<small>Center, transparent padding</small>
|
|
56
|
+
</label>
|
|
57
|
+
<label class="scale-opt">
|
|
58
|
+
<input type="radio" name="scale" value="fill">
|
|
59
|
+
<span class="radio"></span>
|
|
60
|
+
<span>Fill & Crop</span>
|
|
61
|
+
<small>Cover area, crop edges</small>
|
|
62
|
+
</label>
|
|
63
|
+
<label class="scale-opt">
|
|
64
|
+
<input type="radio" name="scale" value="stretch">
|
|
65
|
+
<span class="radio"></span>
|
|
66
|
+
<span>Stretch</span>
|
|
67
|
+
<small>Distort to exact size</small>
|
|
68
|
+
</label>
|
|
69
|
+
</div>
|
|
70
|
+
<div class="btns">
|
|
71
|
+
<button class="btn btn-g" onclick="PicLet.close()">Cancel</button>
|
|
72
|
+
<button class="btn btn-p" onclick="generate()">Generate</button>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
<!-- Loading state -->
|
|
76
|
+
<div class="ld" id="L"><div class="sp"></div><span id="lT">Generating...</span></div>
|
|
77
|
+
<!-- Log -->
|
|
78
|
+
<div class="log" id="G"></div>
|
|
79
|
+
<!-- Done state -->
|
|
80
|
+
<div class="dn" id="D">
|
|
81
|
+
<h4 id="dT"></h4>
|
|
82
|
+
<p id="dM"></p>
|
|
83
|
+
<div class="btns" style="width:100%;margin-top:8px">
|
|
84
|
+
<button class="btn btn-p" onclick="PicLet.close()">Done</button>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
<script>
|
|
89
|
+
const { $, log, fetchJson, postJson } = PicLet;
|
|
90
|
+
|
|
91
|
+
let presets = [];
|
|
92
|
+
let currentPreset = null;
|
|
93
|
+
|
|
94
|
+
// Get aspect ratio label
|
|
95
|
+
function getRatio(w, h) {
|
|
96
|
+
if (w === h) return '1:1';
|
|
97
|
+
const gcd = (a, b) => b ? gcd(b, a % b) : a;
|
|
98
|
+
const d = gcd(w, h);
|
|
99
|
+
return `${w/d}:${h/d}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Update preset display
|
|
103
|
+
function updatePreset() {
|
|
104
|
+
const id = $('pS').value;
|
|
105
|
+
currentPreset = presets.find(p => p.id === id);
|
|
106
|
+
|
|
107
|
+
if (currentPreset && currentPreset.icons) {
|
|
108
|
+
$('pD').textContent = currentPreset.description;
|
|
109
|
+
$('pI').innerHTML = currentPreset.icons.map(i => {
|
|
110
|
+
const ratio = getRatio(i.width, i.height);
|
|
111
|
+
const ratioClass = ratio !== '1:1' ? `<span class="ratio">${ratio}</span>` : '';
|
|
112
|
+
return `<div><span class="name">${i.filename}</span><span class="size">${i.width}×${i.height}${ratioClass}</span></div>`;
|
|
113
|
+
}).join('');
|
|
114
|
+
} else {
|
|
115
|
+
$('pD').textContent = '';
|
|
116
|
+
$('pI').innerHTML = '<div class="name">No icons defined</div>';
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Load initial data
|
|
121
|
+
fetchJson('/api/info').then(d => {
|
|
122
|
+
$('fn').textContent = d.fileName;
|
|
123
|
+
$('sz').textContent = d.width + '×' + d.height;
|
|
124
|
+
|
|
125
|
+
if (d.defaults && d.defaults.presets) {
|
|
126
|
+
presets = d.defaults.presets;
|
|
127
|
+
const select = $('pS');
|
|
128
|
+
select.innerHTML = presets.map(p =>
|
|
129
|
+
`<option value="${p.id}">${p.name}</option>`
|
|
130
|
+
).join('');
|
|
131
|
+
|
|
132
|
+
if (presets.length > 0) {
|
|
133
|
+
select.value = presets[0].id;
|
|
134
|
+
updatePreset();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Get selected scale mode
|
|
140
|
+
function getScaleMode() {
|
|
141
|
+
return document.querySelector('input[name="scale"]:checked')?.value || 'fit';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Generate all images
|
|
145
|
+
async function generate() {
|
|
146
|
+
if (!currentPreset) {
|
|
147
|
+
alert('Please select a preset');
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
$('F').classList.add('hide');
|
|
152
|
+
$('L').classList.add('on');
|
|
153
|
+
$('G').classList.add('on');
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const result = await postJson('/api/process', {
|
|
157
|
+
preset: currentPreset.id,
|
|
158
|
+
scaleMode: getScaleMode()
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (result.logs) {
|
|
162
|
+
result.logs.forEach(l => log('G', l.type[0], l.message));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
$('L').classList.remove('on');
|
|
166
|
+
$('D').classList.add('on', result.success ? 'ok' : 'err');
|
|
167
|
+
$('dT').textContent = result.success ? 'Done' : 'Failed';
|
|
168
|
+
$('dM').textContent = result.success ? result.output : result.error;
|
|
169
|
+
} catch (e) {
|
|
170
|
+
log('G', 'e', e.message);
|
|
171
|
+
$('L').classList.remove('on');
|
|
172
|
+
$('D').classList.add('on', 'err');
|
|
173
|
+
$('dT').textContent = 'Error';
|
|
174
|
+
$('dM').textContent = e.message;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
</script>
|
|
178
|
+
</body>
|
|
179
|
+
</html>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
' PicLet Hidden Launcher
|
|
2
|
+
' Runs WSL commands without showing cmd.exe window
|
|
3
|
+
' Usage: wscript.exe launcher.vbs <tool> <filepath> <flags>
|
|
4
|
+
|
|
5
|
+
Set WshShell = CreateObject("WScript.Shell")
|
|
6
|
+
Set fso = CreateObject("Scripting.FileSystemObject")
|
|
7
|
+
Set args = WScript.Arguments
|
|
8
|
+
|
|
9
|
+
If args.Count < 2 Then
|
|
10
|
+
WScript.Quit 1
|
|
11
|
+
End If
|
|
12
|
+
|
|
13
|
+
tool = args(0)
|
|
14
|
+
filePath = args(1)
|
|
15
|
+
|
|
16
|
+
' Get the directory where this script is located
|
|
17
|
+
scriptDir = fso.GetParentFolderName(WScript.ScriptFullName)
|
|
18
|
+
loadingHta = scriptDir & "\gui\loading.hta"
|
|
19
|
+
|
|
20
|
+
' Show frameless loading window immediately using HTA
|
|
21
|
+
If fso.FileExists(loadingHta) Then
|
|
22
|
+
WshShell.Run "mshta """ & loadingHta & """", 1, False
|
|
23
|
+
End If
|
|
24
|
+
|
|
25
|
+
' Convert Windows path to WSL path format
|
|
26
|
+
' D:\path\to\file.png -> /mnt/d/path/to/file.png
|
|
27
|
+
If Mid(filePath, 2, 1) = ":" Then
|
|
28
|
+
driveLetter = LCase(Left(filePath, 1))
|
|
29
|
+
restOfPath = Mid(filePath, 3)
|
|
30
|
+
restOfPath = Replace(restOfPath, "\", "/")
|
|
31
|
+
wslPath = "/mnt/" & driveLetter & restOfPath
|
|
32
|
+
Else
|
|
33
|
+
wslPath = Replace(filePath, "\", "/")
|
|
34
|
+
End If
|
|
35
|
+
|
|
36
|
+
' Build flags from remaining arguments
|
|
37
|
+
flags = ""
|
|
38
|
+
For i = 2 To args.Count - 1
|
|
39
|
+
flags = flags & " " & args(i)
|
|
40
|
+
Next
|
|
41
|
+
|
|
42
|
+
' Run wsl command hidden (0 = hidden, False = don't wait)
|
|
43
|
+
cmd = "wsl piclet " & tool & " """ & wslPath & """" & flags
|
|
44
|
+
WshShell.Run cmd, 0, False
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@spark-apps/piclet",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Lightweight image tools for content creators",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"piclet": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsup",
|
|
11
|
+
"dev": "tsup --watch",
|
|
12
|
+
"postinstall": "node dist/cli.js install",
|
|
13
|
+
"preuninstall": "node dist/cli.js uninstall",
|
|
14
|
+
"lint": "biome check .",
|
|
15
|
+
"lint:fix": "biome check --write .",
|
|
16
|
+
"format": "biome format --write .",
|
|
17
|
+
"typecheck": "tsc --noEmit",
|
|
18
|
+
"prepublishOnly": "bun run build"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"image",
|
|
22
|
+
"icon",
|
|
23
|
+
"ico",
|
|
24
|
+
"imagemagick",
|
|
25
|
+
"wsl",
|
|
26
|
+
"windows",
|
|
27
|
+
"context-menu",
|
|
28
|
+
"png",
|
|
29
|
+
"resize",
|
|
30
|
+
"scale"
|
|
31
|
+
],
|
|
32
|
+
"author": "spark-apps",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/muammar-yacoob/PicLet.git"
|
|
40
|
+
},
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/muammar-yacoob/PicLet/issues"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/muammar-yacoob/PicLet#readme",
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=18"
|
|
47
|
+
},
|
|
48
|
+
"os": [
|
|
49
|
+
"linux"
|
|
50
|
+
],
|
|
51
|
+
"files": [
|
|
52
|
+
"dist"
|
|
53
|
+
],
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@biomejs/biome": "^1.9.4",
|
|
56
|
+
"@types/express": "^5.0.6",
|
|
57
|
+
"@types/figlet": "^1.7.0",
|
|
58
|
+
"@types/gradient-string": "^1.1.6",
|
|
59
|
+
"@types/node": "^22.0.0",
|
|
60
|
+
"@types/prompts": "^2.4.9",
|
|
61
|
+
"tsup": "^8.5.0",
|
|
62
|
+
"typescript": "^5.7.0"
|
|
63
|
+
},
|
|
64
|
+
"dependencies": {
|
|
65
|
+
"chalk": "^5.6.2",
|
|
66
|
+
"commander": "^14.0.2",
|
|
67
|
+
"express": "^5.2.1",
|
|
68
|
+
"figlet": "^1.8.0",
|
|
69
|
+
"gradient-string": "^3.0.0",
|
|
70
|
+
"prompts": "^2.4.2"
|
|
71
|
+
}
|
|
72
|
+
}
|