@maccesar/titools 4.22.0 → 4.23.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maccesar/titools",
3
- "version": "4.22.0",
3
+ "version": "4.23.0",
4
4
  "description": "Titanium SDK skills and agents for AI coding assistants, including native game and sound synthesis modules",
5
5
  "main": "lib/index.js",
6
6
  "type": "module",
@@ -468,7 +468,7 @@ Load these only when needed:
468
468
  - [PurgeTSS UI in Classic](references/purgetss-ui-classic.md) - Native JavaScript setup, public exports, animation objects, cleanup, and platform notes
469
469
 
470
470
  ### Release Notes
471
- - [Version History](references/version-history.md) - Release-by-release feature additions and behavior changes (v7.4.0 → v7.17.0)
471
+ - [Version History](references/version-history.md) - Release-by-release feature additions and behavior changes (v7.4.0 → v7.17.1)
472
472
 
473
473
  > **💡 TEXT FONTS (Google Fonts, Roboto, etc.)**
474
474
  > For text fonts, see [Custom Fonts](references/custom-fonts.md).
@@ -25,6 +25,7 @@ For the terse flag reference, see the [`brand` command reference](./cli-commands
25
25
  - [Android dark mode](#android-dark-mode)
26
26
  - [Android 12+ splash artwork](#android-12-splash-artwork)
27
27
  - [Android pre-12 splash](#android-pre-12-splash)
28
+ - [FCM notification icon](#fcm-notification-icon)
28
29
  - [The iOS launch screen and LaunchLogo.png](#the-ios-launch-screen-and-launchlogopng)
29
30
  - [iPhone launch images](#iphone-launch-images)
30
31
  - [iOS 18+ Dark and Tinted variants](#ios-18-dark-and-tinted-variants)
@@ -164,9 +165,10 @@ brand: {
164
165
  appicon: { padding: '10%' }, // appicon.png (128×128)
165
166
  androidSplash: { padding: '26%' }, // assets/android/default.png + images/res-*/default.png × 11
166
167
 
167
- // Opt-in: inert until you edit the Android theme / FCM meta-data by hand.
168
+ // Opt-in: splash_icon stays inert until the Android theme points at it.
169
+ // notificationicon is read by firebase.cloudmessaging, by that exact name.
168
170
  splashIcon: { enabled: false }, // drawable-*/splash_icon.png × 5
169
- notificationIcon: { enabled: false }, // drawable-*/ic_stat_notify.png × 5
171
+ notificationIcon: { enabled: false }, // drawable-*/notificationicon.png × 5
170
172
  ninePatch: { enabled: false } // background.9.png (not implemented yet)
171
173
  }
172
174
  ```
@@ -258,12 +260,12 @@ And these live at the top level:
258
260
  | `appicon` | `appicon` | `appicon.png` (128×128) | `10%` | yes |
259
261
  | `android-splash` | `androidSplash` | `assets/android/default.png` + `images/res-*/default.png` × 11 | `26%` | yes |
260
262
  | `splash-icon` | `splashIcon` | `drawable-*/splash_icon.png` × 5 | — | `--splash-icon` |
261
- | `notification-icon` | `notificationIcon` | `drawable-*/ic_stat_notify.png` × 5 | — | `--notification-icon` |
263
+ | `notification-icon` | `notificationIcon` | `drawable-*/notificationicon.png` × 5 | — | `--notification-icon` |
262
264
  | `nine-patch` | `ninePatch` | `background.9.png` | — | `--nine-patch` (not implemented yet) |
263
265
 
264
266
  `ic_launcher.xml` always travels inside `adaptive`; it is never generated on its own.
265
267
 
266
- Only three pieces are opt-in, and for one reason: they produce nothing useful until you edit XML by hand. `splash_icon.png` is inert without `windowSplashScreenAnimatedIcon` in the theme, and `ic_stat_notify.png` is inert without the FCM `meta-data` entry.
268
+ Three pieces are opt-in, because none of them does anything on its own. `splash_icon.png` is inert until the theme points `windowSplashScreenAnimatedIcon` at it. `notificationicon.png` needs `firebase.cloudmessaging` in the project, plus a `meta-data` entry for the notification messages that do not reach it by name — see [FCM notification icon](#fcm-notification-icon). `background.9.png` is not implemented yet.
267
269
 
268
270
  ### `background` is inherited, `padding` is not
269
271
 
@@ -497,6 +499,34 @@ Earlier versions regenerated only the first file and hid the other 11 behind a `
497
499
  > A solid `windowBackground` wins
498
500
  > If the launch theme sets `android:windowBackground` to a plain color — which is what [Matching the launch background](#matching-the-launch-background) recommends — that color takes precedence over this artwork on Android <12. Drop the `windowBackground` item if you want the image to show instead.
499
501
 
502
+ ## FCM notification icon
503
+
504
+ `notification-icon` is off by default. Turn it on with `brand.notificationIcon.enabled`, or for a single run with `--notification-icon`. It writes five densities to `app/platform/android/res/drawable-*/notificationicon.png` (`platform/android/res/drawable-*/notificationicon.png` in Classic), white on transparent, because **Android tints status-bar icons at runtime**: every non-transparent pixel comes out white and the color in the artwork is discarded.
505
+
506
+ The filename is fixed, like `appicon.png` and `DefaultIcon.png`, because the consumer dictates it. `firebase.cloudmessaging` resolves the drawable by name inside `TiFirebaseMessagingService.showNotification()`, through `getResource("notificationicon")`, and that lookup is the only path a **data** message has. When the drawable is missing the module falls back to `appicon` — the opaque launcher icon, which the status bar renders as a white blob, since only the alpha channel survives the tint.
507
+
508
+ **Notification** messages take the configurable path instead. Two `meta-data` entries go under `<application>` in `tiapp.xml`:
509
+
510
+ ```xml
511
+ <meta-data android:name="com.google.firebase.messaging.default_notification_icon"
512
+ android:resource="@drawable/notificationicon"/>
513
+ <meta-data android:name="com.google.firebase.messaging.default_notification_color"
514
+ android:resource="@color/notification_tint"/>
515
+ ```
516
+
517
+ The second one colors the notification's accent, not the icon. Create or merge `app/platform/android/res/values/colors.xml` (`platform/android/res/values/colors.xml` in Classic):
518
+
519
+ ```xml
520
+ <?xml version="1.0" encoding="utf-8"?>
521
+ <resources>
522
+ <color name="notification_tint">#0B1326</color>
523
+ </resources>
524
+ ```
525
+
526
+ > **Renamed in v7.17.1**
527
+ >
528
+ > The piece used to write `ic_stat_notify.png`, the Android convention for status-bar drawables but not the name the module looks up. Under the old name a data message never found the icon, whatever the `meta-data` said. A project that wired `@drawable/ic_stat_notify` by hand changes that one line and deletes the five stale files.
529
+
500
530
  ## The iOS launch screen and LaunchLogo.png
501
531
 
502
532
  Titanium builds `LaunchLogo.imageset` itself on every iOS build, resizing one source into the five sizes it needs. It looks for `LaunchLogo.png` first and falls back to `DefaultIcon.png`.
@@ -744,6 +774,10 @@ cp docs/my-logo-mono.svg purgetss/brand/logo-mono.svg
744
774
  purgetss brand
745
775
  ```
746
776
 
777
+ ### The notification shows a white square, or the app icon instead of my notification icon
778
+
779
+ `firebase.cloudmessaging` did not find the drawable and fell back to `appicon`, which the status bar tints into a solid blob. Three causes, in order of likelihood: the piece was never generated (`brand.notificationIcon.enabled` is `false` by default — run `purgetss brand --only notification-icon`), the project still carries `ic_stat_notify.png` from before v7.17.1, or the `meta-data` entry points at the old name. See [FCM notification icon](#fcm-notification-icon).
780
+
747
781
  ### iOS rejects the app icon upload ("contains transparency")
748
782
 
749
783
  Apple requires App Store icons to have no alpha channel. `DefaultIcon-ios.png` is always flattened on `brand.background` for that reason. If you edited the file manually and reintroduced alpha, re-run `purgetss brand`.
@@ -82,7 +82,8 @@ module.exports = {
82
82
  adaptive: { padding: '18%' }, legacyIcon: { padding: '10%' },
83
83
  appicon: { padding: '10%' }, androidSplash: { padding: '26%' },
84
84
 
85
- // Opt-in: inert until you edit the Android theme / FCM meta-data by hand.
85
+ // Opt-in: splash_icon stays inert until the Android theme points at it.
86
+ // notificationicon is read by firebase.cloudmessaging, by that exact name.
86
87
  splashIcon: { enabled: false }, notificationIcon: { enabled: false },
87
88
  ninePatch: { enabled: false }
88
89
  },
@@ -236,7 +237,7 @@ Radius precedence and the outputs intentionally left unmasked are documented in
236
237
 
237
238
  | Flag | Purpose |
238
239
  | --- | --- |
239
- | `--notification-icon` | Also emit `ic_stat_notify.png × 5`. |
240
+ | `--notification-icon` | Also emit `notificationicon.png × 5`. |
240
241
  | `--splash-icon` | Also emit `splash_icon.png × 5`. |
241
242
  | `--nine-patch` | Declared but not implemented yet; prints a warning and writes nothing. |
242
243
  **Logo variants & overrides**
@@ -70,9 +70,10 @@ module.exports = {
70
70
  appicon: { padding: '10%' }, // appicon.png (128×128)
71
71
  androidSplash: { padding: '26%' }, // assets/android/default.png + images/res-*/default.png × 11
72
72
 
73
- // Opt-in: inert until you edit the Android theme / FCM meta-data by hand.
73
+ // Opt-in: splash_icon stays inert until the Android theme points at it.
74
+ // notificationicon is read by firebase.cloudmessaging, by that exact name.
74
75
  splashIcon: { enabled: false }, // drawable-*/splash_icon.png × 5
75
- notificationIcon: { enabled: false }, // drawable-*/ic_stat_notify.png × 5
76
+ notificationIcon: { enabled: false }, // drawable-*/notificationicon.png × 5
76
77
  ninePatch: { enabled: false } // background.9.png (not implemented yet)
77
78
  },
78
79
  // Sources in purgetss/images/ are 4x masters: a 1024px file yields
@@ -8,6 +8,10 @@ When in doubt about whether a class, flag, or config key exists in the user's in
8
8
 
9
9
  ---
10
10
 
11
+ ## v7.17.1
12
+ - **The `notification-icon` piece writes `notificationicon.png`, not `ic_stat_notify.png`.** `firebase.cloudmessaging` hardcodes that name — `TiFirebaseMessagingService.showNotification()` calls `getResource("notificationicon")` — and it is the only path a **data** message has. The `default_notification_icon` meta-data is the configurable one and covers **notification** messages alone, so under the old name no manifest entry could rescue a data message: the module fell back to the opaque `appicon`, which the status bar renders as a white blob. The filename is fixed rather than a config key, for the same reason `appicon.png` and `DefaultIcon.png` are. Projects that wired `@drawable/ic_stat_notify` by hand need to update that one meta-data line and delete the five stale files. See [app-branding.md](app-branding.md).
13
+ - The comment above the opt-in pieces in the generated config now names each consumer instead of claiming both are inert without hand-edited XML. Only `splash_icon` is.
14
+
11
15
  ## v7.17.0
12
16
  - The `images:` section rejects unknown keys instead of ignoring them, at both levels: the five top-level keys (`quality`, `format`, `autoSync`, `confirmOverwrites`, `files`) and the three inside each `files[]` entry (`filename`, `width`, `height`, with `filename` required). Nothing is generated when validation fails. Never suggest `width`, `opacity`, `padding` or `output` as config keys — they are CLI flags and now abort the run. See [multi-density-images.md](multi-density-images.md).
13
17
  - The generated `images:` block gained four comment lines explaining the 4× master convention, and the `quality` comment now names the formats it reaches: `webp`, `jpeg`, `avif`, `tiff`. PNG uses `compressionLevel: 9` and GIF takes no quality parameter, so neither is affected. Both copies changed — the one patched into an existing config and the one `init` seeds.
@@ -11,7 +11,6 @@ This file is about the community module [`firebase.cloudmessaging`](https://gith
11
11
  <!-- TOC-START -->
12
12
  ## Contents
13
13
 
14
- - [Contents](#contents)
15
14
  - [1. The four doors](#1-the-four-doors)
16
15
  - [2. The payload decides how much of the module runs](#2-the-payload-decides-how-much-of-the-module-runs)
17
16
  - [3. A tap does not fire `didReceiveMessage`](#3-a-tap-does-not-fire-didreceivemessage)
@@ -34,7 +33,7 @@ This file is about the community module [`firebase.cloudmessaging`](https://gith
34
33
  | Backgrounded, person taps the notification | `Ti.App` `resumed`, then read the Intent | nothing |
35
34
  | **Already in front**, person taps a notification sitting in the tray | `newintent` on the root activity | `launchMode="singleTop"` |
36
35
 
37
- A module that fires `didOpenNotification` replaces rows two to four with one listener. See [If your module fires `didOpenNotification`](#if-your-module-fires-didopennotification).
36
+ A module that fires `didOpenNotification` replaces rows three and four with one listener. Row two still arrives in the Intent, because the module does not exist yet when the notification is opened. See [If your module fires `didOpenNotification`](#if-your-module-fires-didopennotification).
38
37
 
39
38
  Verified case by case on a physical Android phone, reading a log line that named which door each message came through rather than watching the screen.
40
39
 
@@ -93,6 +92,12 @@ That last symptom is worth naming because it sends people down the wrong road. I
93
92
 
94
93
  Firebase's own server documentation teaches the `notification` block, which is why this is such a common shape to arrive at.
95
94
 
95
+ ### The icon, whichever route posts it
96
+
97
+ Both routes need a drawable named `notificationicon`, because the module resolves it by that exact name: `TiFirebaseMessagingService.showNotification()` calls `getResource("notificationicon")`, and that lookup is the only path a data message has. When it is missing the module falls back to `appicon` — the opaque launcher icon, which the status bar tints into a white square, since only the alpha channel survives. A notification message takes the configurable path instead, the `com.google.firebase.messaging.default_notification_icon` meta-data, which is why the same project can show a correct icon on one route and a blob on the other.
98
+
99
+ PurgeTSS generates the five densities, white on transparent and under the right name, with `purgetss brand --only notification-icon` — off by default. Its `app-branding.md` § "FCM notification icon" carries both meta-data entries and the `colors.xml` resource that `default_notification_color` needs.
100
+
96
101
  ## 3. A tap does not fire `didReceiveMessage`
97
102
 
98
103
  When the module posts the notification and the person taps it, `PushHandlerActivity` puts the payload on the launcher Intent as the `fcm_data` extra and starts the activity. It does not fire `didReceiveMessage`.
@@ -134,7 +139,16 @@ Take the activity name from the built manifest rather than guessing it: `build/a
134
139
 
135
140
  ### If your module fires `didOpenNotification`
136
141
 
137
- Everything above is what the published module needs. A module that fires `didOpenNotification` from `setNotificationData()` makes all of it unnecessary: no `singleTop`, no `newintent`, no reading the Intent on resume. One listener covers every tap, whatever the app was doing.
142
+ Everything above is what the published module needs. A module that fires `didOpenNotification` from `setNotificationData()` drops the `singleTop`, the `newintent` listener and the Intent read on resume. Two routes remain instead of four, and they are exclusive:
143
+
144
+ | What the app was doing | Where the payload arrives |
145
+ | --- | --- |
146
+ | Open, in front, message arrives | `didReceiveMessage` |
147
+ | Backgrounded, person taps | `didOpenNotification` |
148
+ | Already in front, person taps a notification in the tray | `didOpenNotification` |
149
+ | Killed, person taps | `fcm_data` extra on the launch Intent |
150
+
151
+ Verified on a physical Android 15 phone and an API 35 emulator, each row confirmed by a log line naming the route. The first three ran back to back on one process id with a game in progress, which survived; only the fourth starts a new process.
138
152
 
139
153
  ```javascript
140
154
  modulo.addEventListener('didOpenNotification', (e) => {
@@ -148,6 +162,8 @@ The Intent is still how a cold start arrives, so keep that path. Check `timodule
148
162
 
149
163
  Do not run both. Where the module also puts the payload on the Intent, `resumed` finds it again on Titanium's second pass and the panel opens twice. That is a real symptom, not a hypothetical: it shows up on Android 10 and not on Android 15, because the two versions differ in whether the launcher Intent gets delivered to a task that already exists.
150
164
 
165
+ To test the fourth row, swipe the app out of Recents. `adb shell am kill` looks like the same thing and is not: it kills the process but Android keeps the activity's saved state, and on restore Titanium puts `tiLaunchIntentExtras` from the previous launch back over the new Intent, taking `fcm_data` with it. The `TiRootActivity` checkpoint tells you which one you got — `savedInstanceState: null` is a real cold start, anything else is a restore. `adb shell am force-stop` is worse: it leaves the app in the stopped state, where Android stops delivering FCM messages until someone launches it by hand.
166
+
151
167
  ## 5. Why two splash screens flash by
152
168
 
153
169
  Tap a notification and you may see one or two blank pages slide past before the app appears, and the splash icon never finishes animating. Opening the same app from the launcher icon looks fine.
@@ -297,6 +313,7 @@ Do not test with the app added to the battery whitelist. That measures a phone y
297
313
  | --- | --- | --- |
298
314
  | Tapping restarts the app from zero and loses state | Top-level `notification` block; the system owns the intent | Section 2 |
299
315
  | `image`, `big_text`, `channelId`, `sound`, `vibrate` ignored | Same | Section 2 |
316
+ | A white square or the launcher icon in the status bar | No `notificationicon` drawable; the module fell back to `appicon` | Section 2 |
300
317
  | Foreground and cold start work, backgrounded tap does nothing | Nobody reads the Intent on resume | Section 3 |
301
318
  | Nothing at all happens when tapping a stored notification while using the app | No `resumed`, no `newintent` listener, or no `singleTop` | Section 4 |
302
319
  | Blank pages slide past before the app appears | `PushHandlerActivity` has no theme; Titanium spawns a duplicate root activity | Section 5 |