react-native-icon-switcher 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/package.json +1 -1
- package/plugin/build/core/assets.d.ts +20 -0
- package/plugin/build/core/assets.js +216 -22
- package/plugin/build/index.js +1 -0
- package/plugin/build/sync.js +13 -0
package/README.md
CHANGED
|
@@ -107,6 +107,10 @@ Icons with no art for a platform are omitted from that platform's generated outp
|
|
|
107
107
|
|
|
108
108
|
**Android reserved names:** names may not be `round`, `foreground`, `background`, or `monochrome`, nor end in `_round`, `_foreground`, `_background`, or `_monochrome`.
|
|
109
109
|
|
|
110
|
+
**Legacy Android icons (API 24–25):** besides the adaptive layers and `mipmap-anydpi-v26` XML, `sync` and the plugin write `ic_launcher*.png` the way Android Studio's Image Asset tool does: the visible centre of an adaptive icon (72 dp of the 108 dp layers; a plain path uses the whole image) in a 38 dp rounded square or a 44 dp circle, with a soft drop shadow, on a transparent 48 dp canvas.
|
|
111
|
+
|
|
112
|
+
**Managed iOS icon sets:** when `sync` or the plugin rewrites an `.appiconset` it owns, it deletes files in that folder that the new `Contents.json` does not reference (`sync --check` reports them). Other icon sets are left alone.
|
|
113
|
+
|
|
110
114
|
## Usage
|
|
111
115
|
|
|
112
116
|
```ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-icon-switcher",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Switch the app icon at runtime on React Native and Expo from one config file, with the native wiring generated for you",
|
|
5
5
|
"main": "./lib/module/index.js",
|
|
6
6
|
"types": "./lib/typescript/src/index.d.ts",
|
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
import type { HexColor, ResolvedConfig } from '../types';
|
|
2
|
+
/** Drop shadow tuned to match Image Asset exports at xxxhdpi. */
|
|
3
|
+
export declare const LEGACY_SHADOW_OFFSET_Y_DP = 0.75;
|
|
4
|
+
export declare const LEGACY_SHADOW_BLUR_DP = 1;
|
|
5
|
+
export declare const LEGACY_SHADOW_OPACITY = 0.25;
|
|
6
|
+
/**
|
|
7
|
+
* Largest per-channel difference at which two PNGs still count as the same
|
|
8
|
+
* image. libvips resampling rounds slightly differently on arm64 and x86, so
|
|
9
|
+
* icons generated on a Mac and re-generated in Linux CI can differ by a level
|
|
10
|
+
* or two without any visible change.
|
|
11
|
+
*/
|
|
12
|
+
export declare const PNG_PIXEL_TOLERANCE = 3;
|
|
13
|
+
/** True when both buffers are PNGs of the same size whose pixels differ by at most `PNG_PIXEL_TOLERANCE`. */
|
|
14
|
+
export declare function pngsMatchWithinTolerance(a: Buffer, b: Buffer): Promise<boolean>;
|
|
2
15
|
export type IosVariant = 'light' | 'dark' | 'tinted';
|
|
3
16
|
export type AssetJob = {
|
|
4
17
|
kind: 'ios-png';
|
|
@@ -72,6 +85,13 @@ export declare function writeAssets(plan: AssetPlan, opts: {
|
|
|
72
85
|
iosAssetCatalog: string;
|
|
73
86
|
androidResDir: string;
|
|
74
87
|
}): Promise<WrittenFile[]>;
|
|
88
|
+
/**
|
|
89
|
+
* Remove unreferenced files from managed `.appiconset` folders in `plan`.
|
|
90
|
+
* Returns absolute paths of stale files (removed or that would be removed in check mode).
|
|
91
|
+
*/
|
|
92
|
+
export declare function pruneStaleIosAppIconSets(iosAssetCatalog: string, plan: AssetPlan, opts: {
|
|
93
|
+
check: boolean;
|
|
94
|
+
}): Promise<string[]>;
|
|
75
95
|
/** Test helper: corner pixel RGB after iOS flatten (alpha removed). */
|
|
76
96
|
export declare function _readCornerRgb(pngPath: string): Promise<{
|
|
77
97
|
r: number;
|
|
@@ -3,8 +3,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.PNG_PIXEL_TOLERANCE = exports.LEGACY_SHADOW_OPACITY = exports.LEGACY_SHADOW_BLUR_DP = exports.LEGACY_SHADOW_OFFSET_Y_DP = void 0;
|
|
7
|
+
exports.pngsMatchWithinTolerance = pngsMatchWithinTolerance;
|
|
6
8
|
exports.planAssets = planAssets;
|
|
7
9
|
exports.writeAssets = writeAssets;
|
|
10
|
+
exports.pruneStaleIosAppIconSets = pruneStaleIosAppIconSets;
|
|
8
11
|
exports._readCornerRgb = _readCornerRgb;
|
|
9
12
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
10
13
|
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
@@ -25,6 +28,47 @@ const ADAPTIVE_DENSITIES = [
|
|
|
25
28
|
{ folder: 'mipmap-xxxhdpi', size: 432 },
|
|
26
29
|
];
|
|
27
30
|
const PNG_OPTIONS = { compressionLevel: 9, adaptiveFiltering: false };
|
|
31
|
+
/** Legacy launcher canvas (API 24–25), matching Android Studio Image Asset at 48 dp. */
|
|
32
|
+
const LEGACY_CANVAS_DP = 48;
|
|
33
|
+
const LEGACY_SQUARE_INSET_DP = 5;
|
|
34
|
+
const LEGACY_SQUARE_BODY_DP = 38;
|
|
35
|
+
const LEGACY_SQUARE_RADIUS_DP = 3;
|
|
36
|
+
const LEGACY_ROUND_INSET_DP = 2;
|
|
37
|
+
const LEGACY_ROUND_DIAMETER_DP = 44;
|
|
38
|
+
const ADAPTIVE_LAYER_DP = 108;
|
|
39
|
+
const ADAPTIVE_VIEWPORT_DP = 72;
|
|
40
|
+
/** Drop shadow tuned to match Image Asset exports at xxxhdpi. */
|
|
41
|
+
exports.LEGACY_SHADOW_OFFSET_Y_DP = 0.75;
|
|
42
|
+
exports.LEGACY_SHADOW_BLUR_DP = 1;
|
|
43
|
+
exports.LEGACY_SHADOW_OPACITY = 0.25;
|
|
44
|
+
/**
|
|
45
|
+
* Largest per-channel difference at which two PNGs still count as the same
|
|
46
|
+
* image. libvips resampling rounds slightly differently on arm64 and x86, so
|
|
47
|
+
* icons generated on a Mac and re-generated in Linux CI can differ by a level
|
|
48
|
+
* or two without any visible change.
|
|
49
|
+
*/
|
|
50
|
+
exports.PNG_PIXEL_TOLERANCE = 3;
|
|
51
|
+
const PNG_SIGNATURE = Buffer.from([
|
|
52
|
+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
|
53
|
+
]);
|
|
54
|
+
/** True when both buffers are PNGs of the same size whose pixels differ by at most `PNG_PIXEL_TOLERANCE`. */
|
|
55
|
+
async function pngsMatchWithinTolerance(a, b) {
|
|
56
|
+
if (!a.subarray(0, 8).equals(PNG_SIGNATURE) ||
|
|
57
|
+
!b.subarray(0, 8).equals(PNG_SIGNATURE)) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
const [left, right] = await Promise.all([a, b].map((buf) => (0, sharp_1.default)(buf).ensureAlpha().raw().toBuffer({ resolveWithObject: true })));
|
|
61
|
+
if (left.info.width !== right.info.width ||
|
|
62
|
+
left.info.height !== right.info.height) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
for (let i = 0; i < left.data.length; i += 1) {
|
|
66
|
+
if (Math.abs(left.data[i] - right.data[i]) > exports.PNG_PIXEL_TOLERANCE) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
28
72
|
function _iosSetFolder(icon) {
|
|
29
73
|
return icon.isDefault
|
|
30
74
|
? 'AppIcon.appiconset'
|
|
@@ -274,31 +318,123 @@ async function _flattenPng(sourceAbs, flattenOn, width, height) {
|
|
|
274
318
|
.png(PNG_OPTIONS)
|
|
275
319
|
.toBuffer();
|
|
276
320
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
321
|
+
function _legacyDpToPx(dp, canvasPx) {
|
|
322
|
+
return Math.round((dp * canvasPx) / LEGACY_CANVAS_DP);
|
|
323
|
+
}
|
|
324
|
+
function _legacyShapeLayout(canvasPx, round) {
|
|
325
|
+
if (round) {
|
|
326
|
+
const inset = _legacyDpToPx(LEGACY_ROUND_INSET_DP, canvasPx);
|
|
327
|
+
const bodyPx = _legacyDpToPx(LEGACY_ROUND_DIAMETER_DP, canvasPx);
|
|
328
|
+
return { inset, bodyPx, radiusPx: bodyPx / 2 };
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
inset: _legacyDpToPx(LEGACY_SQUARE_INSET_DP, canvasPx),
|
|
332
|
+
bodyPx: _legacyDpToPx(LEGACY_SQUARE_BODY_DP, canvasPx),
|
|
333
|
+
radiusPx: _legacyDpToPx(LEGACY_SQUARE_RADIUS_DP, canvasPx),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
function _legacyShapeMaskSvg(bodyPx, round, radiusPx) {
|
|
337
|
+
if (round) {
|
|
338
|
+
const r = bodyPx / 2;
|
|
339
|
+
return Buffer.from(`<svg width="${bodyPx}" height="${bodyPx}"><circle cx="${r}" cy="${r}" r="${r}" fill="white"/></svg>`);
|
|
340
|
+
}
|
|
341
|
+
return Buffer.from(`<svg width="${bodyPx}" height="${bodyPx}"><rect width="${bodyPx}" height="${bodyPx}" rx="${radiusPx}" ry="${radiusPx}" fill="white"/></svg>`);
|
|
342
|
+
}
|
|
343
|
+
async function _legacyMaskedContent(content, bodyPx, round, radiusPx) {
|
|
344
|
+
const mask = _legacyShapeMaskSvg(bodyPx, round, radiusPx);
|
|
345
|
+
return (0, sharp_1.default)(content)
|
|
346
|
+
.resize(bodyPx, bodyPx, { fit: 'cover' })
|
|
347
|
+
.ensureAlpha()
|
|
280
348
|
.composite([{ input: mask, blend: 'dest-in' }])
|
|
281
|
-
.png(
|
|
349
|
+
.png()
|
|
282
350
|
.toBuffer();
|
|
283
351
|
}
|
|
284
|
-
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
352
|
+
function _boxBlurLine(src, size, radius, horizontal) {
|
|
353
|
+
const out = new Uint8Array(src.length);
|
|
354
|
+
const window = radius * 2 + 1;
|
|
355
|
+
for (let line = 0; line < size; line += 1) {
|
|
356
|
+
const at = (i) => (horizontal ? line * size + i : i * size + line);
|
|
357
|
+
let sum = 0;
|
|
358
|
+
for (let i = -radius; i <= radius; i += 1) {
|
|
359
|
+
if (i >= 0 && i < size) {
|
|
360
|
+
sum += src[at(i)];
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
for (let i = 0; i < size; i += 1) {
|
|
364
|
+
out[at(i)] = Math.floor(sum / window);
|
|
365
|
+
const leaving = i - radius;
|
|
366
|
+
const entering = i + radius + 1;
|
|
367
|
+
if (leaving >= 0) {
|
|
368
|
+
sum -= src[at(leaving)];
|
|
369
|
+
}
|
|
370
|
+
if (entering < size) {
|
|
371
|
+
sum += src[at(entering)];
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return out;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Black drop shadow of the shape on a transparent canvas. The blur is done in
|
|
379
|
+
* integer arithmetic here rather than with an SVG filter: librsvg's float blur
|
|
380
|
+
* produced different bytes on macOS and Linux, which broke `sync --check` in CI.
|
|
381
|
+
*/
|
|
382
|
+
async function _legacyShadowLayer(canvasPx, shapeMarkup, offsetY, blurPx) {
|
|
383
|
+
const shapeSvg = Buffer.from(`<svg width="${canvasPx}" height="${canvasPx}" xmlns="http://www.w3.org/2000/svg">${shapeMarkup}</svg>`);
|
|
384
|
+
const shape = await (0, sharp_1.default)(shapeSvg)
|
|
385
|
+
.ensureAlpha()
|
|
386
|
+
.extractChannel(3)
|
|
387
|
+
.raw()
|
|
288
388
|
.toBuffer();
|
|
289
|
-
|
|
290
|
-
|
|
389
|
+
// Three box-blur passes approximate a Gaussian.
|
|
390
|
+
const radius = Math.max(1, Math.round(blurPx / 2));
|
|
391
|
+
let alpha = Uint8Array.from(shape);
|
|
392
|
+
for (let pass = 0; pass < 3; pass += 1) {
|
|
393
|
+
alpha = _boxBlurLine(alpha, canvasPx, radius, true);
|
|
394
|
+
alpha = _boxBlurLine(alpha, canvasPx, radius, false);
|
|
395
|
+
}
|
|
396
|
+
const opacity256 = Math.round(exports.LEGACY_SHADOW_OPACITY * 256);
|
|
397
|
+
const rgba = Buffer.alloc(canvasPx * canvasPx * 4);
|
|
398
|
+
for (let y = 0; y < canvasPx; y += 1) {
|
|
399
|
+
const sourceY = y - offsetY;
|
|
400
|
+
if (sourceY < 0 || sourceY >= canvasPx) {
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
for (let x = 0; x < canvasPx; x += 1) {
|
|
404
|
+
const a = alpha[sourceY * canvasPx + x];
|
|
405
|
+
rgba[(y * canvasPx + x) * 4 + 3] = Math.floor((a * opacity256) / 256);
|
|
406
|
+
}
|
|
291
407
|
}
|
|
292
|
-
return
|
|
408
|
+
return (0, sharp_1.default)(rgba, {
|
|
409
|
+
raw: { width: canvasPx, height: canvasPx, channels: 4 },
|
|
410
|
+
})
|
|
411
|
+
.png()
|
|
412
|
+
.toBuffer();
|
|
293
413
|
}
|
|
294
|
-
async function
|
|
414
|
+
async function _legacyStudioPng(content, canvasPx, round) {
|
|
415
|
+
const { inset, bodyPx, radiusPx } = _legacyShapeLayout(canvasPx, round);
|
|
416
|
+
const masked = await _legacyMaskedContent(content, bodyPx, round, radiusPx);
|
|
417
|
+
const shadowOffsetY = _legacyDpToPx(exports.LEGACY_SHADOW_OFFSET_Y_DP, canvasPx);
|
|
418
|
+
const blurPx = Math.max(1, _legacyDpToPx(exports.LEGACY_SHADOW_BLUR_DP, canvasPx));
|
|
419
|
+
const cx = inset + bodyPx / 2;
|
|
420
|
+
const cy = inset + bodyPx / 2;
|
|
421
|
+
const shapeMarkup = round
|
|
422
|
+
? `<circle cx="${cx}" cy="${cy}" r="${bodyPx / 2}" fill="#000"/>`
|
|
423
|
+
: `<rect x="${inset}" y="${inset}" width="${bodyPx}" height="${bodyPx}" rx="${radiusPx}" ry="${radiusPx}" fill="#000"/>`;
|
|
424
|
+
const shadowLayer = await _legacyShadowLayer(canvasPx, shapeMarkup, shadowOffsetY, blurPx);
|
|
425
|
+
return (0, sharp_1.default)(shadowLayer)
|
|
426
|
+
.composite([{ input: masked, left: inset, top: inset, blend: 'over' }])
|
|
427
|
+
.png(PNG_OPTIONS)
|
|
428
|
+
.toBuffer();
|
|
429
|
+
}
|
|
430
|
+
async function _adaptiveLayerComposite(foregroundAbs, layerPx, background) {
|
|
295
431
|
let base;
|
|
296
432
|
if ('color' in background) {
|
|
297
433
|
const { r, g, b } = _hexToRgb(background.color);
|
|
298
434
|
base = await (0, sharp_1.default)({
|
|
299
435
|
create: {
|
|
300
|
-
width:
|
|
301
|
-
height:
|
|
436
|
+
width: layerPx,
|
|
437
|
+
height: layerPx,
|
|
302
438
|
channels: 4,
|
|
303
439
|
background: { r, g, b, alpha: 255 },
|
|
304
440
|
},
|
|
@@ -308,25 +444,41 @@ async function _legacyCompositePng(foregroundAbs, size, round, background) {
|
|
|
308
444
|
}
|
|
309
445
|
else {
|
|
310
446
|
base = await (0, sharp_1.default)(background.imageAbs)
|
|
311
|
-
.resize(
|
|
447
|
+
.resize(layerPx, layerPx, { fit: 'cover' })
|
|
312
448
|
.png()
|
|
313
449
|
.toBuffer();
|
|
314
450
|
}
|
|
315
451
|
const foreground = await (0, sharp_1.default)(foregroundAbs)
|
|
316
|
-
.resize(
|
|
452
|
+
.resize(layerPx, layerPx, {
|
|
317
453
|
fit: 'contain',
|
|
318
454
|
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
|
319
455
|
})
|
|
320
456
|
.png()
|
|
321
457
|
.toBuffer();
|
|
322
|
-
const
|
|
458
|
+
const cropPx = Math.round((layerPx * ADAPTIVE_VIEWPORT_DP) / ADAPTIVE_LAYER_DP);
|
|
459
|
+
const cropInset = Math.round((layerPx - cropPx) / 2);
|
|
460
|
+
const composed = await (0, sharp_1.default)(base)
|
|
323
461
|
.composite([{ input: foreground, blend: 'over' }])
|
|
324
|
-
.png(
|
|
462
|
+
.png()
|
|
463
|
+
.toBuffer();
|
|
464
|
+
return (0, sharp_1.default)(composed)
|
|
465
|
+
.extract({
|
|
466
|
+
left: cropInset,
|
|
467
|
+
top: cropInset,
|
|
468
|
+
width: cropPx,
|
|
469
|
+
height: cropPx,
|
|
470
|
+
})
|
|
471
|
+
.png()
|
|
325
472
|
.toBuffer();
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
return
|
|
473
|
+
}
|
|
474
|
+
async function _legacyPng(sourceAbs, size, round) {
|
|
475
|
+
const content = await (0, sharp_1.default)(sourceAbs).ensureAlpha().png().toBuffer();
|
|
476
|
+
return _legacyStudioPng(content, size, round);
|
|
477
|
+
}
|
|
478
|
+
async function _legacyCompositePng(foregroundAbs, size, round, background) {
|
|
479
|
+
const layerPx = Math.round((size * ADAPTIVE_LAYER_DP) / LEGACY_CANVAS_DP);
|
|
480
|
+
const viewport = await _adaptiveLayerComposite(foregroundAbs, layerPx, background);
|
|
481
|
+
return _legacyStudioPng(viewport, size, round);
|
|
330
482
|
}
|
|
331
483
|
async function _resizePng(sourceAbs, size) {
|
|
332
484
|
return (0, sharp_1.default)(sourceAbs)
|
|
@@ -452,6 +604,48 @@ async function writeAssets(plan, opts) {
|
|
|
452
604
|
}
|
|
453
605
|
return written;
|
|
454
606
|
}
|
|
607
|
+
function _iosFilenamesForContentsJob(job) {
|
|
608
|
+
const keep = new Set(['Contents.json']);
|
|
609
|
+
for (const variant of job.variants) {
|
|
610
|
+
keep.add(variant === 'light' ? 'AppIcon-1024.png' : `AppIcon-1024-${variant}.png`);
|
|
611
|
+
}
|
|
612
|
+
return keep;
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Remove unreferenced files from managed `.appiconset` folders in `plan`.
|
|
616
|
+
* Returns absolute paths of stale files (removed or that would be removed in check mode).
|
|
617
|
+
*/
|
|
618
|
+
async function pruneStaleIosAppIconSets(iosAssetCatalog, plan, opts) {
|
|
619
|
+
const staleAbs = [];
|
|
620
|
+
for (const asset of plan.assets) {
|
|
621
|
+
if (asset.job.kind !== 'ios-contents') {
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
const setRel = node_path_1.default.dirname(asset.relativePath);
|
|
625
|
+
const setDir = node_path_1.default.join(iosAssetCatalog, setRel);
|
|
626
|
+
const keep = _iosFilenamesForContentsJob(asset.job);
|
|
627
|
+
let entries;
|
|
628
|
+
try {
|
|
629
|
+
entries = await promises_1.default.readdir(setDir, { withFileTypes: true });
|
|
630
|
+
}
|
|
631
|
+
catch {
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
for (const entry of entries) {
|
|
635
|
+
const name = entry.name;
|
|
636
|
+
// Only plain files are ours to remove; leave folders and links alone.
|
|
637
|
+
if (!entry.isFile() || keep.has(name)) {
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
const abs = node_path_1.default.join(setDir, name);
|
|
641
|
+
staleAbs.push(abs);
|
|
642
|
+
if (!opts.check) {
|
|
643
|
+
await promises_1.default.unlink(abs);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return staleAbs;
|
|
648
|
+
}
|
|
455
649
|
/** Test helper: corner pixel RGB after iOS flatten (alpha removed). */
|
|
456
650
|
async function _readCornerRgb(pngPath) {
|
|
457
651
|
var _a, _b, _c;
|
package/plugin/build/index.js
CHANGED
|
@@ -125,6 +125,7 @@ const withIconSwitcher = (config, props) => {
|
|
|
125
125
|
iosAssetCatalog,
|
|
126
126
|
androidResDir: path_1.default.join(projectRoot, '.noop-android'),
|
|
127
127
|
});
|
|
128
|
+
await (0, assets_1.pruneStaleIosAppIconSets)(iosAssetCatalog, plan, { check: false });
|
|
128
129
|
await _writeTypesIfChanged(projectRoot, resolved);
|
|
129
130
|
return config;
|
|
130
131
|
},
|
package/plugin/build/sync.js
CHANGED
|
@@ -36,6 +36,12 @@ async function _applyFileChange(_projectRoot, abs, next, check) {
|
|
|
36
36
|
if (existing !== null && existing.equals(nextBuf)) {
|
|
37
37
|
return 'unchanged';
|
|
38
38
|
}
|
|
39
|
+
// Keep a generated image that only differs by resampling rounding (for
|
|
40
|
+
// example made on another CPU), so --check passes and nothing is rewritten.
|
|
41
|
+
if (existing !== null &&
|
|
42
|
+
(await (0, assets_1.pngsMatchWithinTolerance)(existing, nextBuf))) {
|
|
43
|
+
return 'unchanged';
|
|
44
|
+
}
|
|
39
45
|
if (check) {
|
|
40
46
|
return 'updated';
|
|
41
47
|
}
|
|
@@ -76,6 +82,13 @@ async function _syncAssets(resolved, projectRoot, iosAssetCatalog, androidResDir
|
|
|
76
82
|
unchanged += 1;
|
|
77
83
|
}
|
|
78
84
|
}
|
|
85
|
+
const staleIos = await (0, assets_1.pruneStaleIosAppIconSets)(iosAssetCatalog, plan, {
|
|
86
|
+
check,
|
|
87
|
+
});
|
|
88
|
+
for (const abs of staleIos) {
|
|
89
|
+
driftedPaths.push(_rel(projectRoot, abs));
|
|
90
|
+
updated += 1;
|
|
91
|
+
}
|
|
79
92
|
}
|
|
80
93
|
finally {
|
|
81
94
|
await promises_1.default.rm(tmpRoot, { recursive: true, force: true });
|