purgetss 7.5.2 → 7.6.1
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 +93 -11
- package/bin/purgetss +140 -1
- package/dist/purgetss.ui.js +65 -26
- package/dist/utilities.tss +21 -4
- package/experimental/completions2.js +1 -1
- package/lib/completions/titanium/completions-v3.json +62 -1
- package/lib/templates/purgetss.config.js.cjs +15 -1
- package/lib/templates/purgetss.ui.js.cjs +64 -25
- package/package.json +3 -1
- package/src/cli/commands/brand.js +69 -0
- package/src/cli/commands/create.js +11 -7
- package/src/cli/commands/fonts.js +9 -9
- package/src/cli/commands/icon-library.js +18 -16
- package/src/cli/commands/images.js +116 -0
- package/src/cli/commands/init.js +4 -0
- package/src/cli/commands/module.js +4 -2
- package/src/cli/commands/purge.js +77 -101
- package/src/cli/commands/semantic.js +180 -0
- package/src/cli/commands/shades.js +332 -13
- package/src/cli/utils/project-detection.js +4 -2
- package/src/core/analyzers/class-extractor.js +110 -3
- package/src/core/branding/brand-config.js +111 -0
- package/src/core/branding/branding-logger.js +40 -0
- package/src/core/branding/cleanup-legacy.js +220 -0
- package/src/core/branding/ensure-brand-section.js +80 -0
- package/src/core/branding/gen-android-adaptive.js +116 -0
- package/src/core/branding/gen-android-legacy.js +63 -0
- package/src/core/branding/gen-ic-launcher-xml.js +29 -0
- package/src/core/branding/gen-ios-dark.js +70 -0
- package/src/core/branding/gen-ios-tinted.js +55 -0
- package/src/core/branding/gen-ios.js +69 -0
- package/src/core/branding/gen-marketplace.js +71 -0
- package/src/core/branding/gen-notification.js +76 -0
- package/src/core/branding/gen-splash.js +64 -0
- package/src/core/branding/index.js +336 -0
- package/src/core/branding/post-gen-notes.js +145 -0
- package/src/core/branding/prepare-master.js +108 -0
- package/src/core/branding/tiapp-reader.js +110 -0
- package/src/core/builders/tailwind-helpers.js +1 -1
- package/src/core/images/ensure-images-section.js +57 -0
- package/src/core/images/gen-scales.js +181 -0
- package/src/core/images/index.js +171 -0
- package/src/shared/config-manager.js +46 -0
- package/src/shared/config-writer.js +84 -0
- package/src/shared/constants.js +3 -0
- package/src/shared/helpers/typography.js +38 -3
- package/src/shared/logger.js +69 -4
- package/src/shared/prompt.js +64 -0
- package/src/shared/svg-utils.js +80 -0
- package/src/shared/utils.js +8 -4
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PurgeTSS - gen-android-adaptive
|
|
3
|
+
*
|
|
4
|
+
* Adaptive icon triplet (foreground + background + monochrome) at 5 densities.
|
|
5
|
+
*
|
|
6
|
+
* Android adaptive icon: 108×108dp canvas, 66×66dp safe-zone.
|
|
7
|
+
* Densities: mdpi=108, hdpi=162, xhdpi=216, xxhdpi=324, xxxhdpi=432.
|
|
8
|
+
*
|
|
9
|
+
* foreground: logo centered inside safe-zone, transparent outside
|
|
10
|
+
* background: solid color filling the full canvas
|
|
11
|
+
* monochrome: foreground silhouette in white, alpha preserved
|
|
12
|
+
* (Android applies themed tint at runtime on API 31+ — this
|
|
13
|
+
* layer is Android's dark/light mode mechanism for app icons)
|
|
14
|
+
*
|
|
15
|
+
* @fileoverview Android adaptive icons × 5 densities
|
|
16
|
+
* @author César Estrada
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import fs from 'fs'
|
|
20
|
+
import path from 'path'
|
|
21
|
+
import sharp from 'sharp'
|
|
22
|
+
|
|
23
|
+
export const DENSITIES = [
|
|
24
|
+
{ name: 'mdpi', size: 108 },
|
|
25
|
+
{ name: 'hdpi', size: 162 },
|
|
26
|
+
{ name: 'xhdpi', size: 216 },
|
|
27
|
+
{ name: 'xxhdpi', size: 324 },
|
|
28
|
+
{ name: 'xxxhdpi', size: 432 }
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
export async function genAndroidAdaptive(tightMaster, bgColor, paddingPct, resRoot, opts = {}) {
|
|
32
|
+
const { monoTight = null } = opts
|
|
33
|
+
const generated = []
|
|
34
|
+
|
|
35
|
+
for (const { name, size } of DENSITIES) {
|
|
36
|
+
const inner = Math.floor((size * (100 - 2 * paddingPct)) / 100)
|
|
37
|
+
const dir = path.join(resRoot, `mipmap-${name}`)
|
|
38
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
39
|
+
|
|
40
|
+
const foregroundPath = path.join(dir, 'ic_launcher_foreground.png')
|
|
41
|
+
const backgroundPath = path.join(dir, 'ic_launcher_background.png')
|
|
42
|
+
const monochromePath = path.join(dir, 'ic_launcher_monochrome.png')
|
|
43
|
+
|
|
44
|
+
// Foreground: logo sized to inner, centered on transparent canvas
|
|
45
|
+
const innerLogo = await sharp(tightMaster)
|
|
46
|
+
.resize({
|
|
47
|
+
width: inner,
|
|
48
|
+
height: inner,
|
|
49
|
+
fit: 'inside',
|
|
50
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
51
|
+
})
|
|
52
|
+
.toBuffer()
|
|
53
|
+
|
|
54
|
+
await sharp({
|
|
55
|
+
create: {
|
|
56
|
+
width: size,
|
|
57
|
+
height: size,
|
|
58
|
+
channels: 4,
|
|
59
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
.composite([{ input: innerLogo, gravity: 'center' }])
|
|
63
|
+
.png({ compressionLevel: 9 })
|
|
64
|
+
.toFile(foregroundPath)
|
|
65
|
+
|
|
66
|
+
// Background: solid color
|
|
67
|
+
await sharp({
|
|
68
|
+
create: {
|
|
69
|
+
width: size,
|
|
70
|
+
height: size,
|
|
71
|
+
channels: 4,
|
|
72
|
+
background: bgColor
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
.png({ compressionLevel: 9 })
|
|
76
|
+
.toFile(backgroundPath)
|
|
77
|
+
|
|
78
|
+
// Monochrome: white silhouette with preserved alpha.
|
|
79
|
+
if (monoTight) {
|
|
80
|
+
const innerMono = await sharp(monoTight)
|
|
81
|
+
.resize({
|
|
82
|
+
width: inner,
|
|
83
|
+
height: inner,
|
|
84
|
+
fit: 'inside',
|
|
85
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
86
|
+
})
|
|
87
|
+
.toBuffer()
|
|
88
|
+
|
|
89
|
+
await sharp({
|
|
90
|
+
create: {
|
|
91
|
+
width: size,
|
|
92
|
+
height: size,
|
|
93
|
+
channels: 4,
|
|
94
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
.composite([{ input: innerMono, gravity: 'center' }])
|
|
98
|
+
.ensureAlpha()
|
|
99
|
+
.linear([0, 0, 0, 1], [255, 255, 255, 0])
|
|
100
|
+
.png({ compressionLevel: 9 })
|
|
101
|
+
.toFile(monochromePath)
|
|
102
|
+
} else {
|
|
103
|
+
await sharp(foregroundPath)
|
|
104
|
+
.ensureAlpha()
|
|
105
|
+
.linear([0, 0, 0, 1], [255, 255, 255, 0])
|
|
106
|
+
.png({ compressionLevel: 9 })
|
|
107
|
+
.toFile(monochromePath)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
generated.push(foregroundPath, backgroundPath, monochromePath)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
fs.mkdirSync(path.join(resRoot, 'mipmap-anydpi-v26'), { recursive: true })
|
|
114
|
+
|
|
115
|
+
return generated
|
|
116
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PurgeTSS - gen-android-legacy
|
|
3
|
+
*
|
|
4
|
+
* Flat ic_launcher.png for pre-adaptive Android (API 21–25, ~3% of users in 2026).
|
|
5
|
+
* Composites foreground over background, scaled to legacy launcher sizes.
|
|
6
|
+
*
|
|
7
|
+
* Legacy densities: mdpi=48, hdpi=72, xhdpi=96, xxhdpi=144, xxxhdpi=192.
|
|
8
|
+
*
|
|
9
|
+
* @fileoverview Legacy ic_launcher.png × 5 densities
|
|
10
|
+
* @author César Estrada
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from 'fs'
|
|
14
|
+
import path from 'path'
|
|
15
|
+
import sharp from 'sharp'
|
|
16
|
+
|
|
17
|
+
export const LEGACY_DENSITIES = [
|
|
18
|
+
{ name: 'mdpi', size: 48 },
|
|
19
|
+
{ name: 'hdpi', size: 72 },
|
|
20
|
+
{ name: 'xhdpi', size: 96 },
|
|
21
|
+
{ name: 'xxhdpi', size: 144 },
|
|
22
|
+
{ name: 'xxxhdpi', size: 192 }
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
export async function genAndroidLegacy(tightMaster, bgColor, paddingPct, resRoot) {
|
|
26
|
+
const generated = []
|
|
27
|
+
// Legacy icons have no adaptive mask — they render as drawn. Use ~60% of
|
|
28
|
+
// the adaptive padding so the logo fills more of the canvas.
|
|
29
|
+
const legacyPadding = Math.floor((paddingPct * 60) / 100)
|
|
30
|
+
|
|
31
|
+
for (const { name, size } of LEGACY_DENSITIES) {
|
|
32
|
+
const inner = Math.floor((size * (100 - 2 * legacyPadding)) / 100)
|
|
33
|
+
const dir = path.join(resRoot, `mipmap-${name}`)
|
|
34
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
35
|
+
|
|
36
|
+
const outPath = path.join(dir, 'ic_launcher.png')
|
|
37
|
+
|
|
38
|
+
const innerLogo = await sharp(tightMaster)
|
|
39
|
+
.resize({
|
|
40
|
+
width: inner,
|
|
41
|
+
height: inner,
|
|
42
|
+
fit: 'inside',
|
|
43
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
44
|
+
})
|
|
45
|
+
.toBuffer()
|
|
46
|
+
|
|
47
|
+
await sharp({
|
|
48
|
+
create: {
|
|
49
|
+
width: size,
|
|
50
|
+
height: size,
|
|
51
|
+
channels: 4,
|
|
52
|
+
background: bgColor
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
.composite([{ input: innerLogo, gravity: 'center' }])
|
|
56
|
+
.png({ compressionLevel: 9 })
|
|
57
|
+
.toFile(outPath)
|
|
58
|
+
|
|
59
|
+
generated.push(outPath)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return generated
|
|
63
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PurgeTSS - gen-ic-launcher-xml
|
|
3
|
+
*
|
|
4
|
+
* Writes the adaptive-icon XML binder to mipmap-anydpi-v26/ic_launcher.xml.
|
|
5
|
+
* Titanium/Android loads this file on API 26+ to pick up the adaptive icon triplet
|
|
6
|
+
* (foreground + background + monochrome).
|
|
7
|
+
*
|
|
8
|
+
* @fileoverview Adaptive-icon XML binder
|
|
9
|
+
* @author César Estrada
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from 'fs'
|
|
13
|
+
import path from 'path'
|
|
14
|
+
|
|
15
|
+
export const IC_LAUNCHER_XML = `<?xml version="1.0" encoding="utf-8"?>
|
|
16
|
+
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
17
|
+
<background android:drawable="@mipmap/ic_launcher_background"/>
|
|
18
|
+
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
|
19
|
+
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
|
|
20
|
+
</adaptive-icon>
|
|
21
|
+
`
|
|
22
|
+
|
|
23
|
+
export function genIcLauncherXml(resRoot) {
|
|
24
|
+
const dir = path.join(resRoot, 'mipmap-anydpi-v26')
|
|
25
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
26
|
+
const outPath = path.join(dir, 'ic_launcher.xml')
|
|
27
|
+
fs.writeFileSync(outPath, IC_LAUNCHER_XML, 'utf8')
|
|
28
|
+
return outPath
|
|
29
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PurgeTSS - gen-ios-dark
|
|
3
|
+
*
|
|
4
|
+
* Produces Titanium's iOS 18+ dark-mode root icon:
|
|
5
|
+
*
|
|
6
|
+
* DefaultIcon-Dark.png 1024×1024, transparent (default) or flattened on dark bg
|
|
7
|
+
*
|
|
8
|
+
* Per Apple HIG (iOS 18+): the dark variant may either (a) omit the background
|
|
9
|
+
* so the system applies its own dark gradient, or (b) use an opaque dark tint.
|
|
10
|
+
* Default here is (a) — transparent — per Apple's recommended approach.
|
|
11
|
+
*
|
|
12
|
+
* Titanium SDK 13.1+ is expected to read this file from the project root, but
|
|
13
|
+
* upstream issue tidev/titanium-sdk#14122 is not yet merged — until then, the
|
|
14
|
+
* user may need to manually place the PNG inside Assets.xcassets/AppIcon.appiconset.
|
|
15
|
+
*
|
|
16
|
+
* @fileoverview iOS 18+ dark icon variant
|
|
17
|
+
* @author César Estrada
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import fs from 'fs'
|
|
21
|
+
import path from 'path'
|
|
22
|
+
import sharp from 'sharp'
|
|
23
|
+
|
|
24
|
+
const CANVAS = 1024
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {string} tightMaster - Main tight master, or --dark-master output
|
|
28
|
+
* @param {string|null} bgColor - Hex color for opaque flatten, or null for transparent
|
|
29
|
+
* @param {number} paddingPct - Padding per side (0-40)
|
|
30
|
+
* @param {string} outRoot - Output directory
|
|
31
|
+
* @returns {Promise<string>} Path to DefaultIcon-Dark.png
|
|
32
|
+
*/
|
|
33
|
+
export async function genIosDark(tightMaster, bgColor, paddingPct, outRoot) {
|
|
34
|
+
fs.mkdirSync(outRoot, { recursive: true })
|
|
35
|
+
|
|
36
|
+
const inner = Math.floor((CANVAS * (100 - 2 * paddingPct)) / 100)
|
|
37
|
+
const outPath = path.join(outRoot, 'DefaultIcon-Dark.png')
|
|
38
|
+
|
|
39
|
+
const resized = await sharp(tightMaster)
|
|
40
|
+
.resize({
|
|
41
|
+
width: inner,
|
|
42
|
+
height: inner,
|
|
43
|
+
fit: 'inside',
|
|
44
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
45
|
+
})
|
|
46
|
+
.toBuffer()
|
|
47
|
+
|
|
48
|
+
const pipeline = sharp({
|
|
49
|
+
create: {
|
|
50
|
+
width: CANVAS,
|
|
51
|
+
height: CANVAS,
|
|
52
|
+
channels: 4,
|
|
53
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
54
|
+
}
|
|
55
|
+
}).composite([{ input: resized, gravity: 'center' }])
|
|
56
|
+
|
|
57
|
+
if (bgColor) {
|
|
58
|
+
// Opaque variant: flatten on dark tint, strip alpha entirely.
|
|
59
|
+
await pipeline
|
|
60
|
+
.flatten({ background: bgColor })
|
|
61
|
+
.removeAlpha()
|
|
62
|
+
.png({ compressionLevel: 9 })
|
|
63
|
+
.toFile(outPath)
|
|
64
|
+
} else {
|
|
65
|
+
// Apple-recommended default: preserve alpha. System paints its own dark gradient.
|
|
66
|
+
await pipeline.png({ compressionLevel: 9 }).toFile(outPath)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return outPath
|
|
70
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PurgeTSS - gen-ios-tinted
|
|
3
|
+
*
|
|
4
|
+
* Produces Titanium's iOS 18+ tinted root icon:
|
|
5
|
+
*
|
|
6
|
+
* DefaultIcon-Tinted.png 1024×1024, grayscale flattened on black
|
|
7
|
+
*
|
|
8
|
+
* Per Apple HIG (iOS 18+): the tinted variant must be a fully opaque grayscale
|
|
9
|
+
* image over a BLACK (#000000) background. iOS composites its own gradient
|
|
10
|
+
* background and accent color on top of the grayscale luminance at runtime.
|
|
11
|
+
*
|
|
12
|
+
* @fileoverview iOS 18+ tinted icon variant
|
|
13
|
+
* @author César Estrada
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from 'fs'
|
|
17
|
+
import path from 'path'
|
|
18
|
+
import sharp from 'sharp'
|
|
19
|
+
|
|
20
|
+
const CANVAS = 1024
|
|
21
|
+
|
|
22
|
+
export async function genIosTinted(tightMaster, paddingPct, outRoot) {
|
|
23
|
+
fs.mkdirSync(outRoot, { recursive: true })
|
|
24
|
+
|
|
25
|
+
const inner = Math.floor((CANVAS * (100 - 2 * paddingPct)) / 100)
|
|
26
|
+
const outPath = path.join(outRoot, 'DefaultIcon-Tinted.png')
|
|
27
|
+
|
|
28
|
+
// Grayscale the master while keeping alpha — iOS needs luminance info,
|
|
29
|
+
// not color, to apply the system accent tint at runtime.
|
|
30
|
+
const resized = await sharp(tightMaster)
|
|
31
|
+
.grayscale()
|
|
32
|
+
.resize({
|
|
33
|
+
width: inner,
|
|
34
|
+
height: inner,
|
|
35
|
+
fit: 'inside',
|
|
36
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
37
|
+
})
|
|
38
|
+
.toBuffer()
|
|
39
|
+
|
|
40
|
+
await sharp({
|
|
41
|
+
create: {
|
|
42
|
+
width: CANVAS,
|
|
43
|
+
height: CANVAS,
|
|
44
|
+
channels: 4,
|
|
45
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
.composite([{ input: resized, gravity: 'center' }])
|
|
49
|
+
.flatten({ background: '#000000' })
|
|
50
|
+
.removeAlpha()
|
|
51
|
+
.png({ compressionLevel: 9 })
|
|
52
|
+
.toFile(outPath)
|
|
53
|
+
|
|
54
|
+
return outPath
|
|
55
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PurgeTSS - gen-ios
|
|
3
|
+
*
|
|
4
|
+
* Produces Titanium's two root-level iOS/Android icons:
|
|
5
|
+
*
|
|
6
|
+
* DefaultIcon.png 1024×1024, alpha preserved (universal fallback)
|
|
7
|
+
* DefaultIcon-ios.png 1024×1024, alpha flattened on bg-color (iOS)
|
|
8
|
+
*
|
|
9
|
+
* Two distinct paddings — `DefaultIcon.png` is the universal fallback (iOS +
|
|
10
|
+
* Android when no adaptive icons exist), so it uses the Android safe-zone
|
|
11
|
+
* padding to stay inside launcher masks. `DefaultIcon-ios.png` is iOS-only
|
|
12
|
+
* (no launcher mask), so it uses the looser aesthetic iOS padding.
|
|
13
|
+
*
|
|
14
|
+
* Apple rejects alpha on App Store icon uploads, so DefaultIcon-ios.png is
|
|
15
|
+
* always flattened onto the bg-color.
|
|
16
|
+
*
|
|
17
|
+
* @fileoverview Root iOS icons for Titanium projects
|
|
18
|
+
* @author César Estrada
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import fs from 'fs'
|
|
22
|
+
import path from 'path'
|
|
23
|
+
import sharp from 'sharp'
|
|
24
|
+
|
|
25
|
+
const CANVAS = 1024
|
|
26
|
+
|
|
27
|
+
export async function genIos(tightMaster, bgColor, androidPadding, iosPadding, outRoot) {
|
|
28
|
+
fs.mkdirSync(outRoot, { recursive: true })
|
|
29
|
+
|
|
30
|
+
const defaultIconPath = path.join(outRoot, 'DefaultIcon.png')
|
|
31
|
+
const defaultIconIosPath = path.join(outRoot, 'DefaultIcon-ios.png')
|
|
32
|
+
|
|
33
|
+
// DefaultIcon.png — alpha preserved, Android safe-zone padding so it stays
|
|
34
|
+
// launcher-mask-safe when Android falls back to it.
|
|
35
|
+
await renderSquare(tightMaster, androidPadding, null, defaultIconPath)
|
|
36
|
+
|
|
37
|
+
// DefaultIcon-ios.png — flattened on bg-color, iOS aesthetic padding.
|
|
38
|
+
await renderSquare(tightMaster, iosPadding, bgColor, defaultIconIosPath)
|
|
39
|
+
|
|
40
|
+
return { defaultIcon: defaultIconPath, defaultIconIos: defaultIconIosPath }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function renderSquare(tightMaster, paddingPct, flattenBg, outPath) {
|
|
44
|
+
const inner = Math.floor((CANVAS * (100 - 2 * paddingPct)) / 100)
|
|
45
|
+
|
|
46
|
+
const resized = await sharp(tightMaster)
|
|
47
|
+
.resize({
|
|
48
|
+
width: inner,
|
|
49
|
+
height: inner,
|
|
50
|
+
fit: 'inside',
|
|
51
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
52
|
+
})
|
|
53
|
+
.toBuffer()
|
|
54
|
+
|
|
55
|
+
const pipeline = sharp({
|
|
56
|
+
create: {
|
|
57
|
+
width: CANVAS,
|
|
58
|
+
height: CANVAS,
|
|
59
|
+
channels: 4,
|
|
60
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
61
|
+
}
|
|
62
|
+
}).composite([{ input: resized, gravity: 'center' }])
|
|
63
|
+
|
|
64
|
+
if (flattenBg) {
|
|
65
|
+
await pipeline.flatten({ background: flattenBg }).removeAlpha().png({ compressionLevel: 9 }).toFile(outPath)
|
|
66
|
+
} else {
|
|
67
|
+
await pipeline.png({ compressionLevel: 9 }).toFile(outPath)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PurgeTSS - gen-marketplace
|
|
3
|
+
*
|
|
4
|
+
* Store-submission artwork:
|
|
5
|
+
*
|
|
6
|
+
* iTunesConnect.png 1024×1024 (App Store)
|
|
7
|
+
* MarketplaceArtwork.png 512×512 (Google Play)
|
|
8
|
+
*
|
|
9
|
+
* Alpha handling depends on whether --bg-color was explicitly provided:
|
|
10
|
+
* - not provided → alpha preserved (matches `ti create` default template)
|
|
11
|
+
* - provided → alpha flattened onto bg-color (safer for dark-mode stores)
|
|
12
|
+
*
|
|
13
|
+
* @fileoverview Marketplace artwork for Titanium branding
|
|
14
|
+
* @author César Estrada
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from 'fs'
|
|
18
|
+
import path from 'path'
|
|
19
|
+
import sharp from 'sharp'
|
|
20
|
+
|
|
21
|
+
export async function genMarketplace(tightMaster, paddingPct, outRoot, opts = {}) {
|
|
22
|
+
const { flatten = false, bgColor = '#FFFFFF' } = opts
|
|
23
|
+
fs.mkdirSync(outRoot, { recursive: true })
|
|
24
|
+
|
|
25
|
+
const itunesConnect = await renderSquare(
|
|
26
|
+
tightMaster,
|
|
27
|
+
paddingPct,
|
|
28
|
+
1024,
|
|
29
|
+
path.join(outRoot, 'iTunesConnect.png'),
|
|
30
|
+
{ flatten, bgColor }
|
|
31
|
+
)
|
|
32
|
+
const marketplaceArtwork = await renderSquare(
|
|
33
|
+
tightMaster,
|
|
34
|
+
paddingPct,
|
|
35
|
+
512,
|
|
36
|
+
path.join(outRoot, 'MarketplaceArtwork.png'),
|
|
37
|
+
{ flatten, bgColor }
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
return { itunesConnect, marketplaceArtwork }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function renderSquare(tight, paddingPct, canvasSize, outPath, { flatten, bgColor }) {
|
|
44
|
+
const inner = Math.floor((canvasSize * (100 - 2 * paddingPct)) / 100)
|
|
45
|
+
|
|
46
|
+
const resized = await sharp(tight)
|
|
47
|
+
.resize({
|
|
48
|
+
width: inner,
|
|
49
|
+
height: inner,
|
|
50
|
+
fit: 'inside',
|
|
51
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
52
|
+
})
|
|
53
|
+
.toBuffer()
|
|
54
|
+
|
|
55
|
+
let pipeline = sharp({
|
|
56
|
+
create: {
|
|
57
|
+
width: canvasSize,
|
|
58
|
+
height: canvasSize,
|
|
59
|
+
channels: 4,
|
|
60
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
.composite([{ input: resized, gravity: 'center' }])
|
|
64
|
+
|
|
65
|
+
if (flatten) {
|
|
66
|
+
pipeline = pipeline.flatten({ background: bgColor }).removeAlpha()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
await pipeline.png({ compressionLevel: 9 }).toFile(outPath)
|
|
70
|
+
return outPath
|
|
71
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PurgeTSS - gen-notification
|
|
3
|
+
*
|
|
4
|
+
* White-on-transparent notification icons × 5 densities.
|
|
5
|
+
*
|
|
6
|
+
* Android status bar applies runtime tint based on the notification's color
|
|
7
|
+
* property, so all non-transparent pixels become white. Color info is discarded.
|
|
8
|
+
*
|
|
9
|
+
* Sizes: mdpi=24, hdpi=36, xhdpi=48, xxhdpi=72, xxxhdpi=96.
|
|
10
|
+
* Output path: drawable-<density>/ic_stat_notify.png
|
|
11
|
+
*
|
|
12
|
+
* Notification icons are NOT masked by the launcher, so they render as drawn.
|
|
13
|
+
* Trim any transparent padding in the master first, then scale the logo to
|
|
14
|
+
* fill the canvas edge-to-edge with a 1px anti-aliasing margin per side.
|
|
15
|
+
*
|
|
16
|
+
* @fileoverview Status-bar notification icons × 5 densities
|
|
17
|
+
* @author César Estrada
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import fs from 'fs'
|
|
21
|
+
import path from 'path'
|
|
22
|
+
import sharp from 'sharp'
|
|
23
|
+
|
|
24
|
+
export const NOTIFICATION_DENSITIES = [
|
|
25
|
+
{ name: 'mdpi', size: 24 },
|
|
26
|
+
{ name: 'hdpi', size: 36 },
|
|
27
|
+
{ name: 'xhdpi', size: 48 },
|
|
28
|
+
{ name: 'xxhdpi', size: 72 },
|
|
29
|
+
{ name: 'xxxhdpi', size: 96 }
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
const EDGE_MARGIN = 2
|
|
33
|
+
|
|
34
|
+
export async function genNotification(tightMaster, resRoot) {
|
|
35
|
+
const generated = []
|
|
36
|
+
|
|
37
|
+
const trimmedMaster = await sharp(tightMaster)
|
|
38
|
+
.trim()
|
|
39
|
+
.png()
|
|
40
|
+
.toBuffer()
|
|
41
|
+
|
|
42
|
+
for (const { name, size } of NOTIFICATION_DENSITIES) {
|
|
43
|
+
const inner = Math.max(1, size - EDGE_MARGIN)
|
|
44
|
+
const dir = path.join(resRoot, `drawable-${name}`)
|
|
45
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
46
|
+
|
|
47
|
+
const outPath = path.join(dir, 'ic_stat_notify.png')
|
|
48
|
+
|
|
49
|
+
const whitened = await sharp(trimmedMaster)
|
|
50
|
+
.resize({
|
|
51
|
+
width: inner,
|
|
52
|
+
height: inner,
|
|
53
|
+
fit: 'inside',
|
|
54
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
55
|
+
})
|
|
56
|
+
.ensureAlpha()
|
|
57
|
+
.linear([0, 0, 0, 1], [255, 255, 255, 0])
|
|
58
|
+
.toBuffer()
|
|
59
|
+
|
|
60
|
+
await sharp({
|
|
61
|
+
create: {
|
|
62
|
+
width: size,
|
|
63
|
+
height: size,
|
|
64
|
+
channels: 4,
|
|
65
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
.composite([{ input: whitened, gravity: 'center' }])
|
|
69
|
+
.png({ compressionLevel: 9 })
|
|
70
|
+
.toFile(outPath)
|
|
71
|
+
|
|
72
|
+
generated.push(outPath)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return generated
|
|
76
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PurgeTSS - gen-splash
|
|
3
|
+
*
|
|
4
|
+
* Android 12+ SplashScreen API icon × 5 densities.
|
|
5
|
+
*
|
|
6
|
+
* Spec: 288dp canvas, icon occupies central 192dp (~67% of canvas).
|
|
7
|
+
* The OS applies a circular mask automatically — keep pixels transparent
|
|
8
|
+
* outside the 192dp safe-zone.
|
|
9
|
+
*
|
|
10
|
+
* Densities: mdpi=288, hdpi=432, xhdpi=576, xxhdpi=864, xxxhdpi=1152.
|
|
11
|
+
* Output path: drawable-<density>/splash_icon.png
|
|
12
|
+
*
|
|
13
|
+
* @fileoverview Android 12+ splash icons × 5 densities
|
|
14
|
+
* @author César Estrada
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from 'fs'
|
|
18
|
+
import path from 'path'
|
|
19
|
+
import sharp from 'sharp'
|
|
20
|
+
|
|
21
|
+
export const SPLASH_DENSITIES = [
|
|
22
|
+
{ name: 'mdpi', size: 288 },
|
|
23
|
+
{ name: 'hdpi', size: 432 },
|
|
24
|
+
{ name: 'xhdpi', size: 576 },
|
|
25
|
+
{ name: 'xxhdpi', size: 864 },
|
|
26
|
+
{ name: 'xxxhdpi', size: 1152 }
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
export async function genSplash(tightMaster, resRoot) {
|
|
30
|
+
const generated = []
|
|
31
|
+
|
|
32
|
+
for (const { name, size } of SPLASH_DENSITIES) {
|
|
33
|
+
const inner = Math.floor((size * 192) / 288)
|
|
34
|
+
const dir = path.join(resRoot, `drawable-${name}`)
|
|
35
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
36
|
+
|
|
37
|
+
const outPath = path.join(dir, 'splash_icon.png')
|
|
38
|
+
|
|
39
|
+
const innerLogo = await sharp(tightMaster)
|
|
40
|
+
.resize({
|
|
41
|
+
width: inner,
|
|
42
|
+
height: inner,
|
|
43
|
+
fit: 'inside',
|
|
44
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
45
|
+
})
|
|
46
|
+
.toBuffer()
|
|
47
|
+
|
|
48
|
+
await sharp({
|
|
49
|
+
create: {
|
|
50
|
+
width: size,
|
|
51
|
+
height: size,
|
|
52
|
+
channels: 4,
|
|
53
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
.composite([{ input: innerLogo, gravity: 'center' }])
|
|
57
|
+
.png({ compressionLevel: 9 })
|
|
58
|
+
.toFile(outPath)
|
|
59
|
+
|
|
60
|
+
generated.push(outPath)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return generated
|
|
64
|
+
}
|