@nice2dev/icons 1.0.5 → 1.0.8
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/cjs/NiceDesktopIconExporter.js +648 -0
- package/dist/cjs/NiceDesktopIconExporter.js.map +1 -0
- package/dist/cjs/NiceFaviconGenerator.js +429 -0
- package/dist/cjs/NiceFaviconGenerator.js.map +1 -0
- package/dist/cjs/NiceIconEditor.js +443 -0
- package/dist/cjs/NiceIconEditor.js.map +1 -0
- package/dist/cjs/NiceIconPreview.js +198 -0
- package/dist/cjs/NiceIconPreview.js.map +1 -0
- package/dist/cjs/NiceIconSetCreator.js +565 -0
- package/dist/cjs/NiceIconSetCreator.js.map +1 -0
- package/dist/cjs/_virtual/_commonjsHelpers.js +8 -0
- package/dist/cjs/_virtual/_commonjsHelpers.js.map +1 -0
- package/dist/cjs/_virtual/client.js +33 -0
- package/dist/cjs/_virtual/client.js.map +1 -0
- package/dist/cjs/_virtual/client2.js +6 -0
- package/dist/cjs/_virtual/client2.js.map +1 -0
- package/dist/cjs/advancedIconSearch.js +548 -0
- package/dist/cjs/advancedIconSearch.js.map +1 -0
- package/dist/cjs/iconAnalytics.js +395 -152
- package/dist/cjs/iconAnalytics.js.map +1 -1
- package/dist/cjs/index.js +42 -4
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/node_modules/react-dom/client.js +39 -0
- package/dist/cjs/node_modules/react-dom/client.js.map +1 -0
- package/dist/esm/NiceDesktopIconExporter.js +642 -0
- package/dist/esm/NiceDesktopIconExporter.js.map +1 -0
- package/dist/esm/NiceFaviconGenerator.js +426 -0
- package/dist/esm/NiceFaviconGenerator.js.map +1 -0
- package/dist/esm/NiceIconEditor.js +440 -0
- package/dist/esm/NiceIconEditor.js.map +1 -0
- package/dist/esm/NiceIconPreview.js +196 -0
- package/dist/esm/NiceIconPreview.js.map +1 -0
- package/dist/esm/NiceIconSetCreator.js +562 -0
- package/dist/esm/NiceIconSetCreator.js.map +1 -0
- package/dist/esm/_virtual/_commonjsHelpers.js +6 -0
- package/dist/esm/_virtual/_commonjsHelpers.js.map +1 -0
- package/dist/esm/_virtual/client.js +28 -0
- package/dist/esm/_virtual/client.js.map +1 -0
- package/dist/esm/_virtual/client2.js +4 -0
- package/dist/esm/_virtual/client2.js.map +1 -0
- package/dist/esm/advancedIconSearch.js +529 -0
- package/dist/esm/advancedIconSearch.js.map +1 -0
- package/dist/esm/iconAnalytics.js +393 -152
- package/dist/esm/iconAnalytics.js.map +1 -1
- package/dist/esm/index.js +7 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/node_modules/react-dom/client.js +37 -0
- package/dist/esm/node_modules/react-dom/client.js.map +1 -0
- package/dist/types/NiceDesktopIconExporter.d.ts +119 -0
- package/dist/types/NiceFaviconGenerator.d.ts +64 -0
- package/dist/types/NiceIconEditor.d.ts +97 -0
- package/dist/types/NiceIconPreview.d.ts +47 -0
- package/dist/types/NiceIconSetCreator.d.ts +97 -0
- package/dist/types/advancedIconSearch.d.ts +218 -0
- package/dist/types/iconAnalytics.d.ts +219 -112
- package/dist/types/index.d.ts +18 -6
- package/package.json +2 -2
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
|
+
import { useState, useMemo, useCallback } from 'react';
|
|
3
|
+
|
|
4
|
+
/* ══════════════════════════════════════════════════════════════════════════════
|
|
5
|
+
Constants
|
|
6
|
+
══════════════════════════════════════════════════════════════════════════════ */
|
|
7
|
+
/** Standard desktop icon sizes used across platforms */
|
|
8
|
+
const DESKTOP_ICON_SIZES = [
|
|
9
|
+
{ size: 16, label: '16×16', platforms: ['windows', 'linux'] },
|
|
10
|
+
{ size: 24, label: '24×24', platforms: ['linux'] },
|
|
11
|
+
{ size: 32, label: '32×32', platforms: ['windows', 'linux'] },
|
|
12
|
+
{ size: 48, label: '48×48', platforms: ['windows', 'linux'] },
|
|
13
|
+
{ size: 64, label: '64×64', platforms: ['windows', 'linux'] },
|
|
14
|
+
{ size: 128, label: '128×128', platforms: ['windows', 'macos', 'linux'] },
|
|
15
|
+
{ size: 256, label: '256×256', platforms: ['windows', 'macos', 'linux'] },
|
|
16
|
+
{ size: 512, label: '512×512', platforms: ['macos', 'linux'] },
|
|
17
|
+
{ size: 1024, label: '1024×1024', platforms: ['macos'] },
|
|
18
|
+
];
|
|
19
|
+
/** Retina/HiDPI scale factors */
|
|
20
|
+
const RETINA_VARIANTS = [
|
|
21
|
+
{ scale: 1, suffix: '' },
|
|
22
|
+
{ scale: 2, suffix: '@2x' },
|
|
23
|
+
{ scale: 3, suffix: '@3x' },
|
|
24
|
+
];
|
|
25
|
+
/** Default export configuration */
|
|
26
|
+
const DEFAULT_CONFIG = {
|
|
27
|
+
name: 'icon',
|
|
28
|
+
includeWindows: true,
|
|
29
|
+
includeMacOS: true,
|
|
30
|
+
includeLinux: true,
|
|
31
|
+
includeRetina: true,
|
|
32
|
+
backgroundColor: undefined, // transparent
|
|
33
|
+
padding: 0.1,
|
|
34
|
+
fillColor: '#1e1e1e',
|
|
35
|
+
sizes: [16, 32, 48, 64, 128, 256, 512],
|
|
36
|
+
};
|
|
37
|
+
/** Linux hicolor theme icon directories */
|
|
38
|
+
const LINUX_HICOLOR_DIRS = [
|
|
39
|
+
{ size: 16, dir: 'hicolor/16x16/apps' },
|
|
40
|
+
{ size: 24, dir: 'hicolor/24x24/apps' },
|
|
41
|
+
{ size: 32, dir: 'hicolor/32x32/apps' },
|
|
42
|
+
{ size: 48, dir: 'hicolor/48x48/apps' },
|
|
43
|
+
{ size: 64, dir: 'hicolor/64x64/apps' },
|
|
44
|
+
{ size: 128, dir: 'hicolor/128x128/apps' },
|
|
45
|
+
{ size: 256, dir: 'hicolor/256x256/apps' },
|
|
46
|
+
{ size: 512, dir: 'hicolor/512x512/apps' },
|
|
47
|
+
];
|
|
48
|
+
/* ══════════════════════════════════════════════════════════════════════════════
|
|
49
|
+
Utility Functions
|
|
50
|
+
══════════════════════════════════════════════════════════════════════════════ */
|
|
51
|
+
/**
|
|
52
|
+
* Render an icon component to a canvas at the specified size
|
|
53
|
+
*/
|
|
54
|
+
async function renderIconToCanvas(IconComponent, size, fillColor, backgroundColor, padding = 0.1) {
|
|
55
|
+
const canvas = document.createElement('canvas');
|
|
56
|
+
canvas.width = size;
|
|
57
|
+
canvas.height = size;
|
|
58
|
+
const ctx = canvas.getContext('2d');
|
|
59
|
+
// Fill background (or make transparent)
|
|
60
|
+
if (backgroundColor) {
|
|
61
|
+
ctx.fillStyle = backgroundColor;
|
|
62
|
+
ctx.fillRect(0, 0, size, size);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
ctx.clearRect(0, 0, size, size);
|
|
66
|
+
}
|
|
67
|
+
// Render icon to temporary container
|
|
68
|
+
const container = document.createElement('div');
|
|
69
|
+
const iconSize = Math.round(size * (1 - padding * 2));
|
|
70
|
+
const offset = Math.round(size * padding);
|
|
71
|
+
const { createRoot } = await import('./_virtual/client.js').then(function (n) { return n.c; });
|
|
72
|
+
const root = createRoot(container);
|
|
73
|
+
return new Promise((resolve) => {
|
|
74
|
+
root.render(jsx(IconComponent, { size: iconSize, fill: fillColor }));
|
|
75
|
+
// Wait for render
|
|
76
|
+
setTimeout(() => {
|
|
77
|
+
const svg = container.querySelector('svg');
|
|
78
|
+
if (svg) {
|
|
79
|
+
const svgData = new XMLSerializer().serializeToString(svg);
|
|
80
|
+
const img = new Image();
|
|
81
|
+
img.onload = () => {
|
|
82
|
+
ctx.drawImage(img, offset, offset, iconSize, iconSize);
|
|
83
|
+
root.unmount();
|
|
84
|
+
resolve(canvas);
|
|
85
|
+
};
|
|
86
|
+
img.onerror = () => {
|
|
87
|
+
root.unmount();
|
|
88
|
+
resolve(canvas);
|
|
89
|
+
};
|
|
90
|
+
img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgData)));
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
root.unmount();
|
|
94
|
+
resolve(canvas);
|
|
95
|
+
}
|
|
96
|
+
}, 50);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Convert canvas to PNG blob
|
|
101
|
+
*/
|
|
102
|
+
function canvasToBlob(canvas) {
|
|
103
|
+
return new Promise((resolve, reject) => {
|
|
104
|
+
canvas.toBlob((blob) => {
|
|
105
|
+
if (blob)
|
|
106
|
+
resolve(blob);
|
|
107
|
+
else
|
|
108
|
+
reject(new Error('Failed to convert canvas to blob'));
|
|
109
|
+
}, 'image/png');
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Create a Windows ICO file from multiple PNG images.
|
|
114
|
+
* ICO format: https://en.wikipedia.org/wiki/ICO_(file_format)
|
|
115
|
+
*/
|
|
116
|
+
async function createIcoFile(pngBuffers) {
|
|
117
|
+
// Sort by size descending
|
|
118
|
+
const sorted = [...pngBuffers].sort((a, b) => b.size - a.size);
|
|
119
|
+
// ICO header: 6 bytes
|
|
120
|
+
// Image entry: 16 bytes each
|
|
121
|
+
const headerSize = 6;
|
|
122
|
+
const entrySize = 16;
|
|
123
|
+
const directorySize = headerSize + entrySize * sorted.length;
|
|
124
|
+
// Calculate total size and offsets
|
|
125
|
+
let currentOffset = directorySize;
|
|
126
|
+
const entries = [];
|
|
127
|
+
for (const png of sorted) {
|
|
128
|
+
entries.push({ size: png.size, offset: currentOffset, data: png.data });
|
|
129
|
+
currentOffset += png.data.byteLength;
|
|
130
|
+
}
|
|
131
|
+
// Create the ICO buffer
|
|
132
|
+
const totalSize = currentOffset;
|
|
133
|
+
const buffer = new ArrayBuffer(totalSize);
|
|
134
|
+
const view = new DataView(buffer);
|
|
135
|
+
const uint8 = new Uint8Array(buffer);
|
|
136
|
+
// Write ICO header
|
|
137
|
+
view.setUint16(0, 0, true); // Reserved (must be 0)
|
|
138
|
+
view.setUint16(2, 1, true); // Type (1 = ICO)
|
|
139
|
+
view.setUint16(4, sorted.length, true); // Number of images
|
|
140
|
+
// Write image directory entries
|
|
141
|
+
let entryOffset = headerSize;
|
|
142
|
+
for (const entry of entries) {
|
|
143
|
+
const size = entry.size > 255 ? 0 : entry.size; // 0 means 256
|
|
144
|
+
view.setUint8(entryOffset + 0, size); // Width
|
|
145
|
+
view.setUint8(entryOffset + 1, size); // Height
|
|
146
|
+
view.setUint8(entryOffset + 2, 0); // Color palette (0 = no palette)
|
|
147
|
+
view.setUint8(entryOffset + 3, 0); // Reserved
|
|
148
|
+
view.setUint16(entryOffset + 4, 1, true); // Color planes
|
|
149
|
+
view.setUint16(entryOffset + 6, 32, true); // Bits per pixel
|
|
150
|
+
view.setUint32(entryOffset + 8, entry.data.byteLength, true); // Image data size
|
|
151
|
+
view.setUint32(entryOffset + 12, entry.offset, true); // Image data offset
|
|
152
|
+
entryOffset += entrySize;
|
|
153
|
+
}
|
|
154
|
+
// Write image data
|
|
155
|
+
for (const entry of entries) {
|
|
156
|
+
uint8.set(new Uint8Array(entry.data), entry.offset);
|
|
157
|
+
}
|
|
158
|
+
return new Blob([buffer], { type: 'image/x-icon' });
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Create a ZIP file containing multiple files
|
|
162
|
+
*/
|
|
163
|
+
async function createZipFile(files) {
|
|
164
|
+
// Simple ZIP implementation without external libraries
|
|
165
|
+
// Uses store (no compression) method for simplicity
|
|
166
|
+
const encoder = new TextEncoder();
|
|
167
|
+
const fileEntries = [];
|
|
168
|
+
let offset = 0;
|
|
169
|
+
// Prepare file entries
|
|
170
|
+
for (const file of files) {
|
|
171
|
+
const nameBytes = encoder.encode(file.name);
|
|
172
|
+
let dataBytes;
|
|
173
|
+
if (typeof file.data === 'string') {
|
|
174
|
+
dataBytes = encoder.encode(file.data);
|
|
175
|
+
}
|
|
176
|
+
else if (file.data instanceof Blob) {
|
|
177
|
+
dataBytes = new Uint8Array(await file.data.arrayBuffer());
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
dataBytes = new Uint8Array(file.data);
|
|
181
|
+
}
|
|
182
|
+
const crc32 = calculateCRC32(dataBytes);
|
|
183
|
+
fileEntries.push({ name: nameBytes, data: dataBytes, crc32, offset });
|
|
184
|
+
// Local file header size: 30 + name length + data length
|
|
185
|
+
offset += 30 + nameBytes.length + dataBytes.length;
|
|
186
|
+
}
|
|
187
|
+
const centralDirectoryOffset = offset;
|
|
188
|
+
// Calculate central directory size
|
|
189
|
+
let centralDirSize = 0;
|
|
190
|
+
for (const entry of fileEntries) {
|
|
191
|
+
centralDirSize += 46 + entry.name.length;
|
|
192
|
+
}
|
|
193
|
+
// Total size: local headers + data + central directory + end of central directory
|
|
194
|
+
const totalSize = offset + centralDirSize + 22;
|
|
195
|
+
const buffer = new ArrayBuffer(totalSize);
|
|
196
|
+
const view = new DataView(buffer);
|
|
197
|
+
const bytes = new Uint8Array(buffer);
|
|
198
|
+
let pos = 0;
|
|
199
|
+
// Write local file headers and data
|
|
200
|
+
for (const entry of fileEntries) {
|
|
201
|
+
// Local file header signature
|
|
202
|
+
view.setUint32(pos, 0x04034b50, true);
|
|
203
|
+
pos += 4;
|
|
204
|
+
// Version needed
|
|
205
|
+
view.setUint16(pos, 20, true);
|
|
206
|
+
pos += 2;
|
|
207
|
+
// General purpose bit flag
|
|
208
|
+
view.setUint16(pos, 0, true);
|
|
209
|
+
pos += 2;
|
|
210
|
+
// Compression method (0 = store)
|
|
211
|
+
view.setUint16(pos, 0, true);
|
|
212
|
+
pos += 2;
|
|
213
|
+
// File time & date (use current)
|
|
214
|
+
const now = new Date();
|
|
215
|
+
const dosTime = ((now.getHours() << 11) | (now.getMinutes() << 5) | (now.getSeconds() >> 1)) & 0xffff;
|
|
216
|
+
const dosDate = (((now.getFullYear() - 1980) << 9) | ((now.getMonth() + 1) << 5) | now.getDate()) & 0xffff;
|
|
217
|
+
view.setUint16(pos, dosTime, true);
|
|
218
|
+
pos += 2;
|
|
219
|
+
view.setUint16(pos, dosDate, true);
|
|
220
|
+
pos += 2;
|
|
221
|
+
// CRC-32
|
|
222
|
+
view.setUint32(pos, entry.crc32, true);
|
|
223
|
+
pos += 4;
|
|
224
|
+
// Compressed size
|
|
225
|
+
view.setUint32(pos, entry.data.length, true);
|
|
226
|
+
pos += 4;
|
|
227
|
+
// Uncompressed size
|
|
228
|
+
view.setUint32(pos, entry.data.length, true);
|
|
229
|
+
pos += 4;
|
|
230
|
+
// File name length
|
|
231
|
+
view.setUint16(pos, entry.name.length, true);
|
|
232
|
+
pos += 2;
|
|
233
|
+
// Extra field length
|
|
234
|
+
view.setUint16(pos, 0, true);
|
|
235
|
+
pos += 2;
|
|
236
|
+
// File name
|
|
237
|
+
bytes.set(entry.name, pos);
|
|
238
|
+
pos += entry.name.length;
|
|
239
|
+
// File data
|
|
240
|
+
bytes.set(entry.data, pos);
|
|
241
|
+
pos += entry.data.length;
|
|
242
|
+
}
|
|
243
|
+
// Write central directory
|
|
244
|
+
for (const entry of fileEntries) {
|
|
245
|
+
// Central directory file header signature
|
|
246
|
+
view.setUint32(pos, 0x02014b50, true);
|
|
247
|
+
pos += 4;
|
|
248
|
+
// Version made by
|
|
249
|
+
view.setUint16(pos, 20, true);
|
|
250
|
+
pos += 2;
|
|
251
|
+
// Version needed
|
|
252
|
+
view.setUint16(pos, 20, true);
|
|
253
|
+
pos += 2;
|
|
254
|
+
// General purpose bit flag
|
|
255
|
+
view.setUint16(pos, 0, true);
|
|
256
|
+
pos += 2;
|
|
257
|
+
// Compression method
|
|
258
|
+
view.setUint16(pos, 0, true);
|
|
259
|
+
pos += 2;
|
|
260
|
+
// File time & date
|
|
261
|
+
const now = new Date();
|
|
262
|
+
const dosTime = ((now.getHours() << 11) | (now.getMinutes() << 5) | (now.getSeconds() >> 1)) & 0xffff;
|
|
263
|
+
const dosDate = (((now.getFullYear() - 1980) << 9) | ((now.getMonth() + 1) << 5) | now.getDate()) & 0xffff;
|
|
264
|
+
view.setUint16(pos, dosTime, true);
|
|
265
|
+
pos += 2;
|
|
266
|
+
view.setUint16(pos, dosDate, true);
|
|
267
|
+
pos += 2;
|
|
268
|
+
// CRC-32
|
|
269
|
+
view.setUint32(pos, entry.crc32, true);
|
|
270
|
+
pos += 4;
|
|
271
|
+
// Compressed size
|
|
272
|
+
view.setUint32(pos, entry.data.length, true);
|
|
273
|
+
pos += 4;
|
|
274
|
+
// Uncompressed size
|
|
275
|
+
view.setUint32(pos, entry.data.length, true);
|
|
276
|
+
pos += 4;
|
|
277
|
+
// File name length
|
|
278
|
+
view.setUint16(pos, entry.name.length, true);
|
|
279
|
+
pos += 2;
|
|
280
|
+
// Extra field length
|
|
281
|
+
view.setUint16(pos, 0, true);
|
|
282
|
+
pos += 2;
|
|
283
|
+
// File comment length
|
|
284
|
+
view.setUint16(pos, 0, true);
|
|
285
|
+
pos += 2;
|
|
286
|
+
// Disk number start
|
|
287
|
+
view.setUint16(pos, 0, true);
|
|
288
|
+
pos += 2;
|
|
289
|
+
// Internal file attributes
|
|
290
|
+
view.setUint16(pos, 0, true);
|
|
291
|
+
pos += 2;
|
|
292
|
+
// External file attributes
|
|
293
|
+
view.setUint32(pos, 0, true);
|
|
294
|
+
pos += 4;
|
|
295
|
+
// Relative offset of local header
|
|
296
|
+
view.setUint32(pos, entry.offset, true);
|
|
297
|
+
pos += 4;
|
|
298
|
+
// File name
|
|
299
|
+
bytes.set(entry.name, pos);
|
|
300
|
+
pos += entry.name.length;
|
|
301
|
+
}
|
|
302
|
+
// End of central directory record
|
|
303
|
+
view.setUint32(pos, 0x06054b50, true);
|
|
304
|
+
pos += 4;
|
|
305
|
+
// Disk number
|
|
306
|
+
view.setUint16(pos, 0, true);
|
|
307
|
+
pos += 2;
|
|
308
|
+
// Disk number with CD
|
|
309
|
+
view.setUint16(pos, 0, true);
|
|
310
|
+
pos += 2;
|
|
311
|
+
// Number of entries on this disk
|
|
312
|
+
view.setUint16(pos, fileEntries.length, true);
|
|
313
|
+
pos += 2;
|
|
314
|
+
// Total number of entries
|
|
315
|
+
view.setUint16(pos, fileEntries.length, true);
|
|
316
|
+
pos += 2;
|
|
317
|
+
// Size of central directory
|
|
318
|
+
view.setUint32(pos, centralDirSize, true);
|
|
319
|
+
pos += 4;
|
|
320
|
+
// Offset of central directory
|
|
321
|
+
view.setUint32(pos, centralDirectoryOffset, true);
|
|
322
|
+
pos += 4;
|
|
323
|
+
// Comment length
|
|
324
|
+
view.setUint16(pos, 0, true);
|
|
325
|
+
return new Blob([buffer], { type: 'application/zip' });
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Calculate CRC-32 for ZIP files
|
|
329
|
+
*/
|
|
330
|
+
function calculateCRC32(data) {
|
|
331
|
+
let crc = 0xffffffff;
|
|
332
|
+
const table = getCRC32Table();
|
|
333
|
+
for (let i = 0; i < data.length; i++) {
|
|
334
|
+
crc = (crc >>> 8) ^ table[(crc ^ data[i]) & 0xff];
|
|
335
|
+
}
|
|
336
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
337
|
+
}
|
|
338
|
+
let crc32Table = null;
|
|
339
|
+
function getCRC32Table() {
|
|
340
|
+
if (crc32Table)
|
|
341
|
+
return crc32Table;
|
|
342
|
+
crc32Table = new Uint32Array(256);
|
|
343
|
+
for (let i = 0; i < 256; i++) {
|
|
344
|
+
let c = i;
|
|
345
|
+
for (let j = 0; j < 8; j++) {
|
|
346
|
+
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
347
|
+
}
|
|
348
|
+
crc32Table[i] = c;
|
|
349
|
+
}
|
|
350
|
+
return crc32Table;
|
|
351
|
+
}
|
|
352
|
+
/* ══════════════════════════════════════════════════════════════════════════════
|
|
353
|
+
Hook
|
|
354
|
+
══════════════════════════════════════════════════════════════════════════════ */
|
|
355
|
+
/**
|
|
356
|
+
* Hook for exporting desktop icons in multiple formats and sizes.
|
|
357
|
+
*
|
|
358
|
+
* @example
|
|
359
|
+
* ```tsx
|
|
360
|
+
* const { exportAll, downloadAll, icons, isExporting } = useDesktopIconExporter(MyIcon);
|
|
361
|
+
*
|
|
362
|
+
* // Export all icons
|
|
363
|
+
* await exportAll();
|
|
364
|
+
*
|
|
365
|
+
* // Download as ZIP
|
|
366
|
+
* await downloadAll();
|
|
367
|
+
* ```
|
|
368
|
+
*/
|
|
369
|
+
function useDesktopIconExporter(icon, initialConfig) {
|
|
370
|
+
const [config, setConfig] = useState({
|
|
371
|
+
...DEFAULT_CONFIG,
|
|
372
|
+
...initialConfig,
|
|
373
|
+
});
|
|
374
|
+
const [icons, setIcons] = useState([]);
|
|
375
|
+
const [isExporting, setIsExporting] = useState(false);
|
|
376
|
+
const [progress, setProgress] = useState(0);
|
|
377
|
+
// Calculate total number of icons to export
|
|
378
|
+
const totalIcons = useMemo(() => {
|
|
379
|
+
let count = 0;
|
|
380
|
+
const scales = config.includeRetina ? RETINA_VARIANTS.length : 1;
|
|
381
|
+
for (const size of config.sizes) {
|
|
382
|
+
if (config.includeWindows || config.includeLinux)
|
|
383
|
+
count += scales;
|
|
384
|
+
if (config.includeMacOS && size >= 128)
|
|
385
|
+
count += scales;
|
|
386
|
+
}
|
|
387
|
+
return count;
|
|
388
|
+
}, [config]);
|
|
389
|
+
// Export all icons as PNGs
|
|
390
|
+
const exportAll = useCallback(async () => {
|
|
391
|
+
setIsExporting(true);
|
|
392
|
+
setProgress(0);
|
|
393
|
+
const exported = [];
|
|
394
|
+
let completed = 0;
|
|
395
|
+
try {
|
|
396
|
+
const scales = config.includeRetina ? RETINA_VARIANTS : [RETINA_VARIANTS[0]];
|
|
397
|
+
for (const size of config.sizes) {
|
|
398
|
+
for (const variant of scales) {
|
|
399
|
+
const actualSize = size * variant.scale;
|
|
400
|
+
const canvas = await renderIconToCanvas(icon, actualSize, config.fillColor, config.backgroundColor, config.padding);
|
|
401
|
+
const dataUrl = canvas.toDataURL('image/png');
|
|
402
|
+
const blob = await canvasToBlob(canvas);
|
|
403
|
+
// Determine platforms
|
|
404
|
+
const platforms = [];
|
|
405
|
+
if (config.includeWindows && size <= 256)
|
|
406
|
+
platforms.push('windows');
|
|
407
|
+
if (config.includeMacOS && size >= 128)
|
|
408
|
+
platforms.push('macos');
|
|
409
|
+
if (config.includeLinux)
|
|
410
|
+
platforms.push('linux');
|
|
411
|
+
exported.push({
|
|
412
|
+
filename: `${config.name}-${size}x${size}${variant.suffix}.png`,
|
|
413
|
+
size,
|
|
414
|
+
scale: variant.scale,
|
|
415
|
+
platform: platforms.join(','),
|
|
416
|
+
dataUrl,
|
|
417
|
+
blob,
|
|
418
|
+
});
|
|
419
|
+
completed++;
|
|
420
|
+
setProgress((completed / totalIcons) * 100);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
setIcons(exported);
|
|
424
|
+
}
|
|
425
|
+
catch (error) {
|
|
426
|
+
console.error('Failed to export icons:', error);
|
|
427
|
+
}
|
|
428
|
+
finally {
|
|
429
|
+
setIsExporting(false);
|
|
430
|
+
}
|
|
431
|
+
}, [icon, config, totalIcons]);
|
|
432
|
+
// Export Windows ICO file
|
|
433
|
+
const exportWindows = useCallback(async () => {
|
|
434
|
+
const windowsSizes = config.sizes.filter((s) => s <= 256);
|
|
435
|
+
const pngBuffers = [];
|
|
436
|
+
for (const size of windowsSizes) {
|
|
437
|
+
const canvas = await renderIconToCanvas(icon, size, config.fillColor, config.backgroundColor, config.padding);
|
|
438
|
+
const blob = await canvasToBlob(canvas);
|
|
439
|
+
const buffer = await blob.arrayBuffer();
|
|
440
|
+
pngBuffers.push({ size, data: buffer });
|
|
441
|
+
}
|
|
442
|
+
const icoBlob = await createIcoFile(pngBuffers);
|
|
443
|
+
return URL.createObjectURL(icoBlob);
|
|
444
|
+
}, [icon, config]);
|
|
445
|
+
// Export macOS icons (as ZIP with PNG set - ICNS requires native tools)
|
|
446
|
+
const exportMacOS = useCallback(async () => {
|
|
447
|
+
const macSizes = config.sizes.filter((s) => s >= 128);
|
|
448
|
+
const files = [];
|
|
449
|
+
for (const size of macSizes) {
|
|
450
|
+
const scales = config.includeRetina ? RETINA_VARIANTS : [RETINA_VARIANTS[0]];
|
|
451
|
+
for (const variant of scales) {
|
|
452
|
+
const actualSize = size * variant.scale;
|
|
453
|
+
const canvas = await renderIconToCanvas(icon, actualSize, config.fillColor, config.backgroundColor, config.padding);
|
|
454
|
+
const blob = await canvasToBlob(canvas);
|
|
455
|
+
// macOS naming convention: icon_128x128@2x.png
|
|
456
|
+
files.push({
|
|
457
|
+
name: `${config.name}.iconset/icon_${size}x${size}${variant.suffix}.png`,
|
|
458
|
+
data: blob,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
// Add README with iconutil instructions
|
|
463
|
+
files.push({
|
|
464
|
+
name: 'README.txt',
|
|
465
|
+
data: new Blob([
|
|
466
|
+
`macOS Icon Set
|
|
467
|
+
=============
|
|
468
|
+
|
|
469
|
+
To convert to .icns format, use the following command in Terminal:
|
|
470
|
+
|
|
471
|
+
iconutil -c icns ${config.name}.iconset
|
|
472
|
+
|
|
473
|
+
This will create ${config.name}.icns
|
|
474
|
+
|
|
475
|
+
Note: iconutil is only available on macOS.
|
|
476
|
+
`,
|
|
477
|
+
], { type: 'text/plain' }),
|
|
478
|
+
});
|
|
479
|
+
const zipBlob = await createZipFile(files);
|
|
480
|
+
return URL.createObjectURL(zipBlob);
|
|
481
|
+
}, [icon, config]);
|
|
482
|
+
// Export Linux PNG set
|
|
483
|
+
const exportLinux = useCallback(async () => {
|
|
484
|
+
const linuxIcons = [];
|
|
485
|
+
for (const { size, dir } of LINUX_HICOLOR_DIRS) {
|
|
486
|
+
if (!config.sizes.includes(size))
|
|
487
|
+
continue;
|
|
488
|
+
const canvas = await renderIconToCanvas(icon, size, config.fillColor, config.backgroundColor, config.padding);
|
|
489
|
+
const dataUrl = canvas.toDataURL('image/png');
|
|
490
|
+
const blob = await canvasToBlob(canvas);
|
|
491
|
+
linuxIcons.push({
|
|
492
|
+
filename: `${dir}/${config.name}.png`,
|
|
493
|
+
size,
|
|
494
|
+
scale: 1,
|
|
495
|
+
platform: 'linux',
|
|
496
|
+
dataUrl,
|
|
497
|
+
blob,
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
return linuxIcons;
|
|
501
|
+
}, [icon, config]);
|
|
502
|
+
// Download single icon
|
|
503
|
+
const downloadSingle = useCallback((exportedIcon) => {
|
|
504
|
+
const link = document.createElement('a');
|
|
505
|
+
link.href = exportedIcon.dataUrl;
|
|
506
|
+
link.download = exportedIcon.filename;
|
|
507
|
+
link.click();
|
|
508
|
+
}, []);
|
|
509
|
+
// Download all as ZIP
|
|
510
|
+
const downloadAll = useCallback(async () => {
|
|
511
|
+
setIsExporting(true);
|
|
512
|
+
setProgress(0);
|
|
513
|
+
try {
|
|
514
|
+
const files = [];
|
|
515
|
+
// Export all PNG icons first
|
|
516
|
+
await exportAll();
|
|
517
|
+
// Add PNG icons
|
|
518
|
+
for (const exportedIcon of icons) {
|
|
519
|
+
if (exportedIcon.blob) {
|
|
520
|
+
files.push({ name: `png/${exportedIcon.filename}`, data: exportedIcon.blob });
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
// Add Windows ICO
|
|
524
|
+
if (config.includeWindows) {
|
|
525
|
+
const windowsSizes = config.sizes.filter((s) => s <= 256);
|
|
526
|
+
const pngBuffers = [];
|
|
527
|
+
for (const size of windowsSizes) {
|
|
528
|
+
const canvas = await renderIconToCanvas(icon, size, config.fillColor, config.backgroundColor, config.padding);
|
|
529
|
+
const blob = await canvasToBlob(canvas);
|
|
530
|
+
pngBuffers.push({ size, data: await blob.arrayBuffer() });
|
|
531
|
+
}
|
|
532
|
+
const icoBlob = await createIcoFile(pngBuffers);
|
|
533
|
+
files.push({ name: `windows/${config.name}.ico`, data: icoBlob });
|
|
534
|
+
}
|
|
535
|
+
// Add macOS iconset
|
|
536
|
+
if (config.includeMacOS) {
|
|
537
|
+
const macSizes = config.sizes.filter((s) => s >= 128);
|
|
538
|
+
const scales = config.includeRetina ? RETINA_VARIANTS : [RETINA_VARIANTS[0]];
|
|
539
|
+
for (const size of macSizes) {
|
|
540
|
+
for (const variant of scales) {
|
|
541
|
+
const actualSize = size * variant.scale;
|
|
542
|
+
const canvas = await renderIconToCanvas(icon, actualSize, config.fillColor, config.backgroundColor, config.padding);
|
|
543
|
+
const blob = await canvasToBlob(canvas);
|
|
544
|
+
files.push({
|
|
545
|
+
name: `macos/${config.name}.iconset/icon_${size}x${size}${variant.suffix}.png`,
|
|
546
|
+
data: blob,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
files.push({
|
|
551
|
+
name: 'macos/README.txt',
|
|
552
|
+
data: `To convert to .icns: iconutil -c icns ${config.name}.iconset`,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
// Add Linux icons
|
|
556
|
+
if (config.includeLinux) {
|
|
557
|
+
for (const { size, dir } of LINUX_HICOLOR_DIRS) {
|
|
558
|
+
if (!config.sizes.includes(size))
|
|
559
|
+
continue;
|
|
560
|
+
const canvas = await renderIconToCanvas(icon, size, config.fillColor, config.backgroundColor, config.padding);
|
|
561
|
+
const blob = await canvasToBlob(canvas);
|
|
562
|
+
files.push({ name: `linux/${dir}/${config.name}.png`, data: blob });
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
// Create and download ZIP
|
|
566
|
+
const zipBlob = await createZipFile(files);
|
|
567
|
+
const url = URL.createObjectURL(zipBlob);
|
|
568
|
+
const link = document.createElement('a');
|
|
569
|
+
link.href = url;
|
|
570
|
+
link.download = `${config.name}-icons.zip`;
|
|
571
|
+
link.click();
|
|
572
|
+
URL.revokeObjectURL(url);
|
|
573
|
+
}
|
|
574
|
+
catch (error) {
|
|
575
|
+
console.error('Failed to download icons:', error);
|
|
576
|
+
}
|
|
577
|
+
finally {
|
|
578
|
+
setIsExporting(false);
|
|
579
|
+
}
|
|
580
|
+
}, [icon, icons, config, exportAll]);
|
|
581
|
+
return {
|
|
582
|
+
config,
|
|
583
|
+
setConfig,
|
|
584
|
+
icons,
|
|
585
|
+
isExporting,
|
|
586
|
+
progress,
|
|
587
|
+
exportAll,
|
|
588
|
+
exportWindows,
|
|
589
|
+
exportMacOS,
|
|
590
|
+
exportLinux,
|
|
591
|
+
downloadAll,
|
|
592
|
+
downloadSingle,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
/* ══════════════════════════════════════════════════════════════════════════════
|
|
596
|
+
Component
|
|
597
|
+
══════════════════════════════════════════════════════════════════════════════ */
|
|
598
|
+
/**
|
|
599
|
+
* Desktop icon exporter component with UI for configuring and downloading icons.
|
|
600
|
+
*
|
|
601
|
+
* @example
|
|
602
|
+
* ```tsx
|
|
603
|
+
* import { NiceDesktopIconExporter } from '@nice2dev/icons';
|
|
604
|
+
* import { NtdRocket } from '@nice2dev/icons/paths';
|
|
605
|
+
*
|
|
606
|
+
* function App() {
|
|
607
|
+
* return (
|
|
608
|
+
* <NiceDesktopIconExporter
|
|
609
|
+
* icon={NtdRocket}
|
|
610
|
+
* initialConfig={{ name: 'my-app', fillColor: '#ff6b35' }}
|
|
611
|
+
* />
|
|
612
|
+
* );
|
|
613
|
+
* }
|
|
614
|
+
* ```
|
|
615
|
+
*/
|
|
616
|
+
const NiceDesktopIconExporter = ({ icon, initialConfig, onExport, className = '', }) => {
|
|
617
|
+
const { config, setConfig, icons, isExporting, progress, exportAll, downloadAll, downloadSingle, } = useDesktopIconExporter(icon, initialConfig);
|
|
618
|
+
const handleExport = async () => {
|
|
619
|
+
await exportAll();
|
|
620
|
+
onExport === null || onExport === void 0 ? void 0 : onExport(icons);
|
|
621
|
+
};
|
|
622
|
+
const IconComponent = icon;
|
|
623
|
+
return (jsxs("div", { className: `nice-desktop-icon-exporter ${className}`, children: [jsxs("div", { className: "nice-desktop-icon-exporter__preview", children: [jsx("h3", { children: "Preview" }), jsx("div", { className: "nice-desktop-icon-exporter__preview-grid", children: [16, 32, 48, 64, 128, 256].map((size) => (jsxs("div", { className: "nice-desktop-icon-exporter__preview-item", children: [jsx("div", { style: {
|
|
624
|
+
width: Math.min(size, 128),
|
|
625
|
+
height: Math.min(size, 128),
|
|
626
|
+
backgroundColor: config.backgroundColor || '#f0f0f0',
|
|
627
|
+
display: 'flex',
|
|
628
|
+
alignItems: 'center',
|
|
629
|
+
justifyContent: 'center',
|
|
630
|
+
borderRadius: 4,
|
|
631
|
+
}, children: jsx(IconComponent, { size: Math.min(size, 128) * (1 - config.padding * 2), fill: config.fillColor }) }), jsxs("span", { children: [size, "\u00D7", size] })] }, size))) })] }), jsxs("div", { className: "nice-desktop-icon-exporter__config", children: [jsx("h3", { children: "Configuration" }), jsxs("label", { children: ["Icon Name:", jsx("input", { type: "text", value: config.name, onChange: (e) => setConfig((c) => ({ ...c, name: e.target.value })) })] }), jsxs("label", { children: ["Fill Color:", jsx("input", { type: "color", value: config.fillColor, onChange: (e) => setConfig((c) => ({ ...c, fillColor: e.target.value })) })] }), jsxs("label", { children: ["Background:", jsx("input", { type: "color", value: config.backgroundColor || '#ffffff', onChange: (e) => setConfig((c) => ({ ...c, backgroundColor: e.target.value })) }), jsx("button", { onClick: () => setConfig((c) => ({ ...c, backgroundColor: undefined })), children: "Transparent" })] }), jsxs("label", { children: ["Padding: ", Math.round(config.padding * 100), "%", jsx("input", { type: "range", min: "0", max: "0.3", step: "0.01", value: config.padding, onChange: (e) => setConfig((c) => ({ ...c, padding: parseFloat(e.target.value) })) })] }), jsxs("div", { className: "nice-desktop-icon-exporter__platforms", children: [jsxs("label", { children: [jsx("input", { type: "checkbox", checked: config.includeWindows, onChange: (e) => setConfig((c) => ({ ...c, includeWindows: e.target.checked })) }), "Windows (.ico)"] }), jsxs("label", { children: [jsx("input", { type: "checkbox", checked: config.includeMacOS, onChange: (e) => setConfig((c) => ({ ...c, includeMacOS: e.target.checked })) }), "macOS (.iconset)"] }), jsxs("label", { children: [jsx("input", { type: "checkbox", checked: config.includeLinux, onChange: (e) => setConfig((c) => ({ ...c, includeLinux: e.target.checked })) }), "Linux (hicolor)"] }), jsxs("label", { children: [jsx("input", { type: "checkbox", checked: config.includeRetina, onChange: (e) => setConfig((c) => ({ ...c, includeRetina: e.target.checked })) }), "HiDPI/Retina (@2x, @3x)"] })] }), jsxs("div", { className: "nice-desktop-icon-exporter__sizes", children: [jsx("h4", { children: "Sizes:" }), DESKTOP_ICON_SIZES.map(({ size, label }) => (jsxs("label", { children: [jsx("input", { type: "checkbox", checked: config.sizes.includes(size), onChange: (e) => {
|
|
632
|
+
setConfig((c) => ({
|
|
633
|
+
...c,
|
|
634
|
+
sizes: e.target.checked
|
|
635
|
+
? [...c.sizes, size].sort((a, b) => a - b)
|
|
636
|
+
: c.sizes.filter((s) => s !== size),
|
|
637
|
+
}));
|
|
638
|
+
} }), label] }, size)))] })] }), jsxs("div", { className: "nice-desktop-icon-exporter__actions", children: [jsx("button", { onClick: handleExport, disabled: isExporting, children: isExporting ? `Exporting... ${Math.round(progress)}%` : 'Generate Icons' }), jsx("button", { onClick: downloadAll, disabled: isExporting || icons.length === 0, children: "Download All (ZIP)" })] }), icons.length > 0 && (jsxs("div", { className: "nice-desktop-icon-exporter__results", children: [jsxs("h3", { children: ["Generated Icons (", icons.length, ")"] }), jsx("div", { className: "nice-desktop-icon-exporter__results-grid", children: icons.map((exportedIcon) => (jsxs("div", { className: "nice-desktop-icon-exporter__result-item", children: [jsx("img", { src: exportedIcon.dataUrl, alt: exportedIcon.filename, style: { maxWidth: 64, maxHeight: 64 } }), jsx("span", { children: exportedIcon.filename }), jsx("button", { onClick: () => downloadSingle(exportedIcon), children: "Download" })] }, exportedIcon.filename))) })] }))] }));
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
export { DESKTOP_ICON_SIZES, LINUX_HICOLOR_DIRS, NiceDesktopIconExporter, RETINA_VARIANTS, useDesktopIconExporter };
|
|
642
|
+
//# sourceMappingURL=NiceDesktopIconExporter.js.map
|