craft-native 0.0.442 → 0.0.443

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.
@@ -122,6 +122,18 @@ export interface CraftAndroidConfig {
122
122
  }>;
123
123
  trustedOrigins?: string[];
124
124
  appIconPath?: string;
125
+ /**
126
+ * The launch splash: a directory laid out as Android resources, holding
127
+ * `drawable-<density>/splash_icon.png` (Android 12's splash icon, 288dp
128
+ * with its content inside the 192dp circle) and `splash_logo.png` (the
129
+ * logo the window background centres before Android 12), with Dark Mode
130
+ * ones in `drawable-night-<density>`. ts-images' `generateSplashScreens`
131
+ * writes exactly this (`android.resources`). Both sit on `backgroundColor`
132
+ * (`backgroundColorDark` at night), the colour the first frame continues.
133
+ * Without it, Android 12 shows the launcher icon and older versions the
134
+ * background colour.
135
+ */
136
+ splashResources?: string;
125
137
  /**
126
138
  * The small icon in the status bar and on every notification the app
127
139
  * shows, push or local: an Android vector drawable (`.xml`) or a `.png`,
@@ -268,6 +280,30 @@ export interface RunOptions {
268
280
  device?: string;
269
281
  output: string;
270
282
  }
283
+ /** A splash file, by its place under `res/` (`drawable-night-xxhdpi/splash_icon.png`). */
284
+ export interface AndroidSplashFile {
285
+ folder: string;
286
+ name: 'splash_icon.png' | 'splash_logo.png';
287
+ path: string;
288
+ }
289
+ /**
290
+ * The splash files in a resources directory: `splash_icon.png` and
291
+ * `splash_logo.png` in each `drawable[-night][-<density>]` folder. A directory
292
+ * with neither is a mistake, not an app without a splash.
293
+ */
294
+ export declare function androidSplashFiles(resources: string): AndroidSplashFile[];
295
+ /**
296
+ * The launch theme the activity starts in, and the theme it swaps to before
297
+ * its first frame. Android 12 and later draw their splash from
298
+ * `windowSplashScreen*`; earlier versions draw the window background, here
299
+ * the logo centred on the background colour. Either way the colour is the
300
+ * app's own `background`, so nothing flashes when the first frame replaces it.
301
+ */
302
+ export declare function androidLaunchThemes(splash: Pick<AndroidSplashFile, 'name'>[]): {
303
+ base: string;
304
+ v31: string;
305
+ background?: string;
306
+ };
271
307
  export declare function syncAndroidWebAssets(source: string, output: string): void;
272
308
  export declare function renderAndroidPermissions(config: CraftAndroidConfig): string;
273
309
  export declare function renderAndroidDeepLinks(config: CraftAndroidConfig): string;
@@ -757,6 +757,49 @@ function requireRegularFile(path, label) {
757
757
  if (!statSync(path).isFile())
758
758
  throw new Error(`${label} must be a file: ${path}`);
759
759
  }
760
+ function androidSplashFiles(resources) {
761
+ if (!existsSync(resources) || !statSync(resources).isDirectory())
762
+ throw new Error(`Splash resources must be a directory: ${resources}`);
763
+ const files = [];
764
+ for (const folder of readdirSync(resources).sort()) {
765
+ if (!/^drawable(?:-night)?(?:-(?:l|m|tv|h|xh|xxh|xxxh)dpi)?$/.test(folder))
766
+ continue;
767
+ for (const name of ["splash_icon.png", "splash_logo.png"]) {
768
+ const path = join3(resources, folder, name);
769
+ if (existsSync(path) && statSync(path).isFile())
770
+ files.push({ folder, name, path });
771
+ }
772
+ }
773
+ if (!files.length)
774
+ throw new Error(`Splash resources hold no drawable*/splash_icon.png or splash_logo.png: ${resources}`);
775
+ return files;
776
+ }
777
+ function androidLaunchThemes(splash) {
778
+ const icon = splash.some((file) => file.name === "splash_icon.png");
779
+ const logo = splash.some((file) => file.name === "splash_logo.png");
780
+ const base = logo ? ` <style name="Theme.CraftApp.Launch" parent="Theme.CraftApp">
781
+ <item name="android:windowBackground">@drawable/craft_launch_background</item>
782
+ </style>
783
+ ` : ` <style name="Theme.CraftApp.Launch" parent="Theme.CraftApp" />
784
+ `;
785
+ const v31 = `<?xml version="1.0" encoding="utf-8"?>
786
+ <resources>
787
+ <style name="Theme.CraftApp.Launch" parent="Theme.CraftApp">
788
+ <item name="android:windowSplashScreenBackground">@color/background</item>${icon ? `
789
+ <item name="android:windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>` : ""}
790
+ </style>
791
+ </resources>
792
+ `;
793
+ const background = logo ? `<?xml version="1.0" encoding="utf-8"?>
794
+ <layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque">
795
+ <item android:drawable="@color/background" />
796
+ <item>
797
+ <bitmap android:gravity="center" android:src="@drawable/splash_logo" />
798
+ </item>
799
+ </layer-list>
800
+ ` : undefined;
801
+ return { base, v31, background };
802
+ }
760
803
  function containsPath(parent, candidate) {
761
804
  const relativePath = relative(parent, candidate);
762
805
  return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute2(relativePath);
@@ -964,6 +1007,7 @@ function writeAndroidConfig(output, config) {
964
1007
  writeFileSync(join3(output, "craft.config.json"), JSON.stringify(config, null, 2));
965
1008
  const runtimeConfig = { ...config };
966
1009
  delete runtimeConfig.appIconPath;
1010
+ delete runtimeConfig.splashResources;
967
1011
  delete runtimeConfig.notificationIconPath;
968
1012
  delete runtimeConfig.googleServicesFile;
969
1013
  writeFileSync(join3(output, "app/src/main/assets/craft.config.json"), JSON.stringify(runtimeConfig, null, 2));
@@ -1266,6 +1310,7 @@ async function init(options) {
1266
1310
  if (config.appIconPath && ![".gif", ".jpg", ".png", ".webp"].includes(appIconExtension ?? "")) {
1267
1311
  throw new Error(`Unsupported Android app icon format: ${appIconExtension || "(none)"}`);
1268
1312
  }
1313
+ const splash = config.splashResources ? androidSplashFiles(config.splashResources) : [];
1269
1314
  if (config.notificationIconPath)
1270
1315
  requireRegularFile(config.notificationIconPath, "Notification icon");
1271
1316
  const notificationIconExtension = config.notificationIconPath ? extname(config.notificationIconPath).toLowerCase() : undefined;
@@ -1475,6 +1520,17 @@ zipStorePath=wrapper/dists
1475
1520
  </vector>
1476
1521
  `);
1477
1522
  }
1523
+ const resDir = join3(output, "app/src/main/res");
1524
+ for (const folder of existsSync(resDir) ? readdirSync(resDir) : []) {
1525
+ if (folder.startsWith("drawable"))
1526
+ for (const name of ["splash_icon.png", "splash_logo.png"])
1527
+ rmSync2(join3(resDir, folder, name), { force: true });
1528
+ }
1529
+ for (const file of splash) {
1530
+ mkdirSync2(join3(resDir, file.folder), { recursive: true });
1531
+ cpSync(file.path, join3(resDir, file.folder, file.name));
1532
+ }
1533
+ const launch = androidLaunchThemes(splash);
1478
1534
  const themesXml = `<?xml version="1.0" encoding="utf-8"?>
1479
1535
  <resources>
1480
1536
  <style name="Theme.CraftApp" parent="Theme.Material3.DayNight.NoActionBar">
@@ -1482,9 +1538,15 @@ zipStorePath=wrapper/dists
1482
1538
  <item name="android:navigationBarColor">@color/primary</item>
1483
1539
  <item name="android:windowBackground">@color/background</item>
1484
1540
  </style>
1485
- </resources>
1541
+ ${launch.base}</resources>
1486
1542
  `;
1487
1543
  writeFileSync(join3(output, "app/src/main/res/values/themes.xml"), themesXml);
1544
+ mkdirSync2(join3(output, "app/src/main/res/values-v31"), { recursive: true });
1545
+ writeFileSync(join3(output, "app/src/main/res/values-v31/themes.xml"), launch.v31);
1546
+ if (launch.background)
1547
+ writeFileSync(join3(output, "app/src/main/res/drawable/craft_launch_background.xml"), launch.background);
1548
+ else
1549
+ rmSync2(join3(output, "app/src/main/res/drawable/craft_launch_background.xml"), { force: true });
1488
1550
  const activityMainXml = config.renderer === "native" ? `<?xml version="1.0" encoding="utf-8"?>
1489
1551
  <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
1490
1552
  android:id="@+id/native_root"
@@ -1708,6 +1770,8 @@ async function run(options) {
1708
1770
  export {
1709
1771
  ANDROID_MIN_SDK,
1710
1772
  HEALTH_CONNECT_PERMISSIONS,
1773
+ androidLaunchThemes,
1774
+ androidSplashFiles,
1711
1775
  build,
1712
1776
  compressNativeSources,
1713
1777
  createAndroidEmulator,
@@ -45,7 +45,8 @@
45
45
  android:windowSoftInputMode="adjustResize"
46
46
  android:supportsPictureInPicture="true"
47
47
  android:resizeableActivity="true"
48
- android:launchMode="singleTask">
48
+ android:launchMode="singleTask"
49
+ android:theme="@style/Theme.CraftApp.Launch">
49
50
  <intent-filter>
50
51
  <action android:name="android.intent.action.MAIN" />
51
52
  <category android:name="android.intent.category.LAUNCHER" />
@@ -76,6 +76,9 @@ class MainActivity : AppCompatActivity() {
76
76
 
77
77
  @SuppressLint("SetJavaScriptEnabled")
78
78
  override fun onCreate(savedInstanceState: Bundle?) {
79
+ // The activity starts in the launch theme (the splash); its own theme
80
+ // takes over before the first frame, on the same background colour.
81
+ setTheme(R.style.Theme_CraftApp)
79
82
  applyConfiguredAppearance()
80
83
  super.onCreate(savedInstanceState)
81
84
 
@@ -4738,6 +4738,9 @@ class MainActivity : AppCompatActivity() {
4738
4738
  }
4739
4739
 
4740
4740
  override fun onCreate(savedInstanceState: Bundle?) {
4741
+ // The activity starts in the launch theme (the splash); its own theme
4742
+ // takes over before the first frame, on the same background colour.
4743
+ setTheme(R.style.Theme_CraftApp)
4741
4744
  applyConfiguredAppearance()
4742
4745
  super.onCreate(savedInstanceState)
4743
4746
  onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
package/dist/cli.js CHANGED
@@ -3228,7 +3228,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
3228
3228
  `);
3229
3229
  }
3230
3230
  // package.json
3231
- var version = "0.0.442";
3231
+ var version = "0.0.443";
3232
3232
 
3233
3233
  // bin/cli.ts
3234
3234
  var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
package/dist/index.cjs CHANGED
@@ -376,7 +376,7 @@ function craftBinaryNotFoundMessage(triedPath) {
376
376
  `);
377
377
  }
378
378
  // package.json
379
- var version = "0.0.442";
379
+ var version = "0.0.443";
380
380
 
381
381
  // src/sdk-version.ts
382
382
  var SDK_VERSION = version;
package/dist/index.js CHANGED
@@ -117,7 +117,7 @@ function craftBinaryNotFoundMessage(triedPath) {
117
117
  `);
118
118
  }
119
119
  // package.json
120
- var version = "0.0.442";
120
+ var version = "0.0.443";
121
121
 
122
122
  // src/sdk-version.ts
123
123
  var SDK_VERSION = version;
@@ -208,7 +208,9 @@ export interface CraftConfig {
208
208
  * screen until the page says it is ready (`splash.hide()`), so the app goes
209
209
  * from launch to content without a blank web view in between. PNG, PDF or
210
210
  * SVG, drawn at its own size in points: give an SVG the width and height it
211
- * should have on screen.
211
+ * should have on screen. A PNG with `@2x` and `@3x` files beside it
212
+ * (`LaunchLogo.png`, `LaunchLogo@2x.png`, `LaunchLogo@3x.png`, as ts-images'
213
+ * `generateSplashScreens` writes them) is sharp on every screen.
212
214
  */
213
215
  splashImagePath?: string;
214
216
  /** The splash logo in Dark Mode, on `backgroundColorDark`. The light one if unset. */
@@ -532,6 +534,16 @@ export declare function renderPrivacyManifest(config: CraftConfig): string;
532
534
  export declare function pngHasAlpha(bytes: Uint8Array): boolean;
533
535
  /** The asset the launch screen and the splash both draw, so one hands over to the other unseen. */
534
536
  export declare const LAUNCH_LOGO = "LaunchLogo";
537
+ /**
538
+ * A bitmap splash's `@2x` and `@3x` files beside it (`LaunchLogo.png`,
539
+ * `LaunchLogo@2x.png`, `LaunchLogo@3x.png`, as ts-images'
540
+ * `generateSplashScreens` writes them), by scale. A bitmap without them is
541
+ * one image, drawn at its pixel size in points.
542
+ */
543
+ export declare function splashScales(source: string): Array<{
544
+ scale: 1 | 2 | 3;
545
+ path: string;
546
+ }>;
535
547
  /** The launch screen: the background colour, and the splash logo on it when there is one. */
536
548
  export declare function renderLaunchScreen(config: Pick<CraftConfig, 'splashImagePath'>): string;
537
549
  /** Near-black or near-white, whichever reads on `background` (WCAG relative luminance). */
@@ -1107,6 +1107,14 @@ function renderAssetCatalog(output, config) {
1107
1107
  renderLaunchLogo(catalog, config);
1108
1108
  }
1109
1109
  var LAUNCH_LOGO = "LaunchLogo";
1110
+ function splashScales(source) {
1111
+ const extension = extname(source);
1112
+ if (!/^\.(?:png|jpe?g)$/i.test(extension))
1113
+ return [{ scale: 1, path: source }];
1114
+ const stem = source.slice(0, -extension.length);
1115
+ const scales = [2, 3].map((scale) => ({ scale, path: `${stem}@${scale}x${extension}` })).filter((item) => existsSync2(item.path));
1116
+ return scales.length ? [{ scale: 1, path: source }, ...scales] : [{ scale: 1, path: source }];
1117
+ }
1110
1118
  function renderLaunchLogo(catalog, config) {
1111
1119
  const imageset = join4(catalog, `${LAUNCH_LOGO}.imageset`);
1112
1120
  rmSync3(imageset, { recursive: true, force: true });
@@ -1123,13 +1131,18 @@ function renderLaunchLogo(catalog, config) {
1123
1131
  const extension = source.split(".").pop().toLowerCase();
1124
1132
  if (extension === "svg" || extension === "pdf")
1125
1133
  vector = true;
1126
- const filename = `${LAUNCH_LOGO}${dark ? "-dark" : ""}.${extension}`;
1127
- cpSync(source, join4(imageset, filename));
1128
- images.push({
1129
- ...dark ? { appearances: [{ appearance: "luminosity", value: "dark" }] } : {},
1130
- filename,
1131
- idiom: "universal"
1132
- });
1134
+ const files = splashScales(source);
1135
+ const scaled = files.length > 1;
1136
+ for (const { scale, path } of files) {
1137
+ const filename = `${LAUNCH_LOGO}${dark ? "-dark" : ""}${scale === 1 ? "" : `@${scale}x`}.${extension}`;
1138
+ cpSync(path, join4(imageset, filename));
1139
+ images.push({
1140
+ ...dark ? { appearances: [{ appearance: "luminosity", value: "dark" }] } : {},
1141
+ filename,
1142
+ idiom: "universal",
1143
+ ...scaled ? { scale: `${scale}x` } : {}
1144
+ });
1145
+ }
1133
1146
  }
1134
1147
  writeFileSync3(join4(imageset, "Contents.json"), `${JSON.stringify({
1135
1148
  images,
@@ -1841,6 +1854,7 @@ export {
1841
1854
  runPostGenerateHook,
1842
1855
  serializeSimulatorSteps,
1843
1856
  showSimulator,
1857
+ splashScales,
1844
1858
  syncNativeFonts,
1845
1859
  syncWebAssets,
1846
1860
  usesNativeBundle
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craft-native",
3
- "version": "0.0.442",
3
+ "version": "0.0.443",
4
4
  "type": "module",
5
5
  "description": "Build desktop apps with web languages - TypeScript SDK for Craft",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",