craft-native 0.0.238 → 0.0.242
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/android/src/index.d.ts +23 -1
- package/dist/android/src/index.js +102 -9
- package/dist/android/src/native-map.d.ts +7 -0
- package/dist/android/templates/CraftBridge.kt.template +2 -1
- package/dist/android/templates/CraftNativeBackground.kt.template +28 -2
- package/dist/android/templates/CraftNativeMapGoogle.kt.template +440 -0
- package/dist/android/templates/CraftNativeMapLibre.kt.template +438 -0
- package/dist/android/templates/CraftNativeMessagingService.kt.template +3 -1
- package/dist/android/templates/CraftNativeNotificationReceiver.kt.template +2 -1
- package/dist/android/templates/LocationRecordingService.kt.template +3 -1
- package/dist/android/templates/MainActivityNative.kt.template +14 -433
- package/dist/android/templates/build.gradle.kts.app.template +1 -1
- package/dist/android/templates/build.gradle.kts.project.template +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/src/index.d.ts +12 -1
- package/dist/ios/src/index.js +32 -7
- package/dist/ios/src/testing.d.ts +14 -2
- package/dist/ios/templates/CraftApp.swift +117 -29
- package/dist/ios/templates/CraftHybrid.swift +193 -29
- package/dist/ios/templates/CraftNativeActions.swift +26 -27
- package/dist/ios/templates/CraftNativeFiles.swift +205 -1
- package/dist/ios/templates/CraftNativeScreen.swift +230 -33
- package/dist/ios/templates/testing/DriverTests.swift.template +37 -0
- package/dist/macos/templates/native_layout.zig +41 -5
- package/package.json +1 -1
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Generates Android apps with native Craft components and optional WebView content.
|
|
5
5
|
*/
|
|
6
|
+
import { type AndroidMapProvider } from './native-map.js';
|
|
7
|
+
export type { AndroidMapProvider } from './native-map.js';
|
|
8
|
+
/**
|
|
9
|
+
* The builder installs a directory of route bundles (stx's `routesDir`) as
|
|
10
|
+
* well as the one-file bundle, and the app runs each screen from its own
|
|
11
|
+
* route's file. Stacks asks for route bundles only when this is true.
|
|
12
|
+
*/
|
|
13
|
+
export declare const nativeRouteBundles = true;
|
|
14
|
+
/** Providers this published builder can generate as real Android map views. */
|
|
15
|
+
export declare const nativeMapProviders: readonly ['google', 'maplibre'];
|
|
6
16
|
export { runKotlinFixture } from './testing.js';
|
|
7
17
|
export { createAndroidEmulator } from './emulator.js';
|
|
8
18
|
export type { AndroidEmulatorOptions, AndroidEmulatorSession } from './emulator.js';
|
|
@@ -71,8 +81,20 @@ export interface CraftAndroidConfig {
|
|
|
71
81
|
}>;
|
|
72
82
|
trustedOrigins?: string[];
|
|
73
83
|
appIconPath?: string;
|
|
84
|
+
/**
|
|
85
|
+
* The small icon in the status bar and on every notification the app
|
|
86
|
+
* shows, push or local: an Android vector drawable (`.xml`) or a `.png`,
|
|
87
|
+
* white on transparent, since Android draws only its shape. Without one,
|
|
88
|
+
* a white Craft mark. Never the launcher icon, which shows as a white
|
|
89
|
+
* square.
|
|
90
|
+
*/
|
|
91
|
+
notificationIconPath?: string;
|
|
92
|
+
/** The `#RRGGBB` Android tints the notification icon and app name with. The system's own colour without it. */
|
|
93
|
+
notificationColor?: string;
|
|
74
94
|
googleServicesFile?: string;
|
|
75
|
-
/**
|
|
95
|
+
/** Native map renderer. MapLibre needs no SDK key; Google remains the default for compatibility. */
|
|
96
|
+
mapProvider?: AndroidMapProvider;
|
|
97
|
+
/** Google Maps Android SDK key, used only by the Google native map host. */
|
|
76
98
|
mapsApiKey?: string;
|
|
77
99
|
devServerURL?: string;
|
|
78
100
|
hasBundledFallback?: boolean;
|
|
@@ -4,6 +4,48 @@ import { cpSync, existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSy
|
|
|
4
4
|
import { dirname as dirname2, extname, isAbsolute as isAbsolute2, join as join3, relative, resolve as resolve2, sep } from "path";
|
|
5
5
|
var {$ } = globalThis.Bun;
|
|
6
6
|
|
|
7
|
+
// ../android/src/native-map.ts
|
|
8
|
+
function androidMapHost(provider = "google") {
|
|
9
|
+
if (provider === "google")
|
|
10
|
+
return {
|
|
11
|
+
template: "CraftNativeMapGoogle.kt.template",
|
|
12
|
+
imports: `import com.google.android.gms.maps.CameraUpdateFactory
|
|
13
|
+
import com.google.android.gms.maps.GoogleMap
|
|
14
|
+
import com.google.android.gms.maps.MapView
|
|
15
|
+
import com.google.android.gms.maps.OnMapReadyCallback
|
|
16
|
+
import com.google.android.gms.maps.model.BitmapDescriptorFactory
|
|
17
|
+
import com.google.android.gms.maps.model.LatLng
|
|
18
|
+
import com.google.android.gms.maps.model.LatLngBounds
|
|
19
|
+
import com.google.android.gms.maps.model.Marker
|
|
20
|
+
import com.google.android.gms.maps.model.MarkerOptions
|
|
21
|
+
import com.google.android.gms.maps.model.Polyline
|
|
22
|
+
import com.google.android.gms.maps.model.PolylineOptions`,
|
|
23
|
+
dependency: "com.google.android.gms:play-services-maps:18.2.0"
|
|
24
|
+
};
|
|
25
|
+
if (provider === "maplibre")
|
|
26
|
+
return {
|
|
27
|
+
template: "CraftNativeMapLibre.kt.template",
|
|
28
|
+
imports: `import org.maplibre.android.MapLibre
|
|
29
|
+
import org.maplibre.android.camera.CameraPosition
|
|
30
|
+
import org.maplibre.android.camera.CameraUpdateFactory
|
|
31
|
+
import org.maplibre.android.geometry.LatLng
|
|
32
|
+
import org.maplibre.android.geometry.LatLngBounds
|
|
33
|
+
import org.maplibre.android.location.LocationComponentActivationOptions
|
|
34
|
+
import org.maplibre.android.maps.MapLibreMap
|
|
35
|
+
import org.maplibre.android.maps.MapView
|
|
36
|
+
import org.maplibre.android.maps.OnMapReadyCallback
|
|
37
|
+
import org.maplibre.android.maps.Style
|
|
38
|
+
import org.maplibre.android.style.expressions.Expression
|
|
39
|
+
import org.maplibre.android.style.layers.CircleLayer
|
|
40
|
+
import org.maplibre.android.style.layers.LineLayer
|
|
41
|
+
import org.maplibre.android.style.layers.SymbolLayer
|
|
42
|
+
import org.maplibre.android.style.layers.PropertyFactory.*
|
|
43
|
+
import org.maplibre.android.style.sources.GeoJsonSource`,
|
|
44
|
+
dependency: "org.maplibre.gl:android-sdk:13.7.1"
|
|
45
|
+
};
|
|
46
|
+
throw new Error(`Unknown Android native map provider: ${provider}. Expected google or maplibre.`);
|
|
47
|
+
}
|
|
48
|
+
|
|
7
49
|
// ../android/src/promise-runtime.ts
|
|
8
50
|
var ANDROID_PROMISE_RUNTIME = `
|
|
9
51
|
if (window.__craftRejectPendingPromises) {
|
|
@@ -315,6 +357,8 @@ async function createAndroidEmulator(options) {
|
|
|
315
357
|
}
|
|
316
358
|
|
|
317
359
|
// ../android/src/index.ts
|
|
360
|
+
var nativeRouteBundles = true;
|
|
361
|
+
var nativeMapProviders = ["google", "maplibre"];
|
|
318
362
|
function syncNativeFonts(fonts, output) {
|
|
319
363
|
const destination = join3(output, "app/src/main/assets/NativeFonts");
|
|
320
364
|
const names = new Set;
|
|
@@ -549,6 +593,7 @@ function normalizeAndroidNetworkConfig(config) {
|
|
|
549
593
|
config.trustedOrigins = [...new Set(trustedOrigins)];
|
|
550
594
|
}
|
|
551
595
|
function validateAndroidConfig(config) {
|
|
596
|
+
androidMapHost(config.mapProvider);
|
|
552
597
|
if (!config.appName.trim())
|
|
553
598
|
throw new Error("Android app name must not be empty");
|
|
554
599
|
if (config.renderer !== undefined && config.renderer !== "web" && config.renderer !== "native") {
|
|
@@ -682,6 +727,7 @@ function writeAndroidConfig(output, config) {
|
|
|
682
727
|
writeFileSync(join3(output, "craft.config.json"), JSON.stringify(config, null, 2));
|
|
683
728
|
const runtimeConfig = { ...config };
|
|
684
729
|
delete runtimeConfig.appIconPath;
|
|
730
|
+
delete runtimeConfig.notificationIconPath;
|
|
685
731
|
delete runtimeConfig.googleServicesFile;
|
|
686
732
|
writeFileSync(join3(output, "app/src/main/assets/craft.config.json"), JSON.stringify(runtimeConfig, null, 2));
|
|
687
733
|
}
|
|
@@ -802,7 +848,9 @@ function renderNativeShellManifest(config) {
|
|
|
802
848
|
<intent-filter>
|
|
803
849
|
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
|
804
850
|
</intent-filter>
|
|
805
|
-
</service
|
|
851
|
+
</service>
|
|
852
|
+
<meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/craft_notification_icon" />${config.notificationColor ? `
|
|
853
|
+
<meta-data android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/craft_notification_color" />` : ""}`);
|
|
806
854
|
}
|
|
807
855
|
if (config.enableHealthConnect) {
|
|
808
856
|
parts.push(` <activity-alias
|
|
@@ -912,6 +960,18 @@ async function init(options) {
|
|
|
912
960
|
if (config.appIconPath && ![".gif", ".jpg", ".png", ".webp"].includes(appIconExtension ?? "")) {
|
|
913
961
|
throw new Error(`Unsupported Android app icon format: ${appIconExtension || "(none)"}`);
|
|
914
962
|
}
|
|
963
|
+
if (config.notificationIconPath)
|
|
964
|
+
requireRegularFile(config.notificationIconPath, "Notification icon");
|
|
965
|
+
const notificationIconExtension = config.notificationIconPath ? extname(config.notificationIconPath).toLowerCase() : undefined;
|
|
966
|
+
if (config.notificationIconPath && ![".png", ".xml"].includes(notificationIconExtension ?? "")) {
|
|
967
|
+
throw new Error(`Unsupported Android notification icon format: ${notificationIconExtension || "(none)"} (use a vector drawable .xml or a white-on-transparent .png)`);
|
|
968
|
+
}
|
|
969
|
+
if (config.notificationIconPath && notificationIconExtension === ".xml" && !/<vector[\s>]/.test(readFileSync2(config.notificationIconPath, "utf8"))) {
|
|
970
|
+
throw new Error("Notification icon .xml must be an Android vector drawable (<vector>)");
|
|
971
|
+
}
|
|
972
|
+
if (config.notificationColor !== undefined && !/^#[0-9a-f]{6}$/i.test(config.notificationColor)) {
|
|
973
|
+
throw new Error(`notificationColor must be #RRGGBB, not ${String(config.notificationColor)}`);
|
|
974
|
+
}
|
|
915
975
|
const dirs = [
|
|
916
976
|
output,
|
|
917
977
|
join3(output, "app/src/main/java", packagePath),
|
|
@@ -936,7 +996,7 @@ async function init(options) {
|
|
|
936
996
|
cpSync(config.googleServicesFile, join3(output, "app/google-services.json"));
|
|
937
997
|
}
|
|
938
998
|
const mainActivityTemplate = readFileSync2(join3(TEMPLATES_DIR, config.renderer === "native" ? "MainActivityNative.kt.template" : "MainActivity.kt.template"), "utf-8");
|
|
939
|
-
const mainActivity = mainActivityTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{APP_NAME\}\}/g, name);
|
|
999
|
+
const mainActivity = mainActivityTemplate.replace(/\{\{NATIVE_MAP_IMPORTS\}\}/g, () => androidMapHost(config.mapProvider).imports).replace(/\{\{NATIVE_MAP_HOST\}\}/g, () => readFileSync2(join3(TEMPLATES_DIR, androidMapHost(config.mapProvider).template), "utf8")).replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{APP_NAME\}\}/g, name);
|
|
940
1000
|
writeFileSync(join3(output, "app/src/main/java", packagePath, "MainActivity.kt"), mainActivity);
|
|
941
1001
|
writeFileSync(join3(output, "app/src/main/java", packagePath, "CraftAppLinks.kt"), readFileSync2(join3(TEMPLATES_DIR, "CraftAppLinks.kt.template"), "utf8").replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
942
1002
|
writeFileSync(join3(output, "app/src/main/java", packagePath, "CraftCrashReporter.kt"), readFileSync2(join3(TEMPLATES_DIR, "CraftCrashReporter.kt.template"), "utf-8").replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{CRAFT_VERSION\}\}/g, CRAFT_ANDROID_VERSION));
|
|
@@ -979,11 +1039,11 @@ async function init(options) {
|
|
|
979
1039
|
mkdirSync2(nativeDir, { recursive: true });
|
|
980
1040
|
writeFileSync(join3(nativeDir, "CraftNative.kt"), craftNative);
|
|
981
1041
|
const serviceTemplate = readFileSync2(join3(TEMPLATES_DIR, "LocationRecordingService.kt.template"), "utf-8");
|
|
982
|
-
writeFileSync(join3(nativeDir, "LocationRecordingService.kt"), serviceTemplate);
|
|
1042
|
+
writeFileSync(join3(nativeDir, "LocationRecordingService.kt"), serviceTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
983
1043
|
const healthTemplate = readFileSync2(join3(TEMPLATES_DIR, config.enableHealthConnect ? "CraftHealthConnect.kt.template" : "CraftHealthConnectStub.kt.template"), "utf-8");
|
|
984
1044
|
writeFileSync(join3(output, "app/src/main/java", packagePath, "CraftHealthConnect.kt"), healthTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
985
1045
|
const manifestTemplate = readFileSync2(join3(TEMPLATES_DIR, "AndroidManifest.xml.template"), "utf-8");
|
|
986
|
-
const manifest = manifestTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{PERMISSIONS\}\}/g, renderAndroidPermissions(config)).replace(/\{\{USES_CLEARTEXT\}\}/g, config.devServerURL?.startsWith("http://") || config.enableLocalNetwork ? "true" : "false").replace(/\{\{DEEP_LINK_INTENT_FILTERS\}\}/g, renderAndroidDeepLinks(config)).replace(/\{\{BACKGROUND_SERVICE\}\}/g, config.enableBackgroundLocation ? ' <service android:name="com.craft.runtime.LocationRecordingService" android:exported="false" android:foregroundServiceType="location" android:stopWithTask="false" />' : "").replace(/\{\{NATIVE_NOTIFICATION_RECEIVER\}\}/g, config.renderer === "native" ? renderNativeShellManifest(config) : "").replace(/\{\{CRASH_REPORTER_PROVIDER\}\}/g, ` <provider android:name=".CraftCrashReporterInit" android:authorities="\${applicationId}.craft-crash" android:exported="false" android:initOrder="1000" />`).replace(/\{\{GOOGLE_MAPS_API_KEY\}\}/g, config.mapsApiKey ? ` <meta-data android:name="com.google.android.geo.API_KEY" android:value="${escapeXml(config.mapsApiKey)}" />` : "").replace(/\{\{HEALTH_CONNECT_QUERIES\}\}/g, config.enableHealthConnect ? ` <queries>
|
|
1046
|
+
const manifest = manifestTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{PERMISSIONS\}\}/g, renderAndroidPermissions(config)).replace(/\{\{USES_CLEARTEXT\}\}/g, config.devServerURL?.startsWith("http://") || config.enableLocalNetwork ? "true" : "false").replace(/\{\{DEEP_LINK_INTENT_FILTERS\}\}/g, renderAndroidDeepLinks(config)).replace(/\{\{BACKGROUND_SERVICE\}\}/g, config.enableBackgroundLocation ? ' <service android:name="com.craft.runtime.LocationRecordingService" android:exported="false" android:foregroundServiceType="location" android:stopWithTask="false" />' : "").replace(/\{\{NATIVE_NOTIFICATION_RECEIVER\}\}/g, config.renderer === "native" ? renderNativeShellManifest(config) : "").replace(/\{\{CRASH_REPORTER_PROVIDER\}\}/g, ` <provider android:name=".CraftCrashReporterInit" android:authorities="\${applicationId}.craft-crash" android:exported="false" android:initOrder="1000" />`).replace(/\{\{GOOGLE_MAPS_API_KEY\}\}/g, config.mapProvider !== "maplibre" && config.mapsApiKey ? ` <meta-data android:name="com.google.android.geo.API_KEY" android:value="${escapeXml(config.mapsApiKey)}" />` : "").replace(/\{\{HEALTH_CONNECT_QUERIES\}\}/g, config.enableHealthConnect ? ` <queries>
|
|
987
1047
|
<package android:name="com.google.android.apps.healthdata" />
|
|
988
1048
|
</queries>` : "").replace(/\{\{HEALTH_CONNECT_RATIONALE\}\}/g, config.enableHealthConnect ? ` <intent-filter>
|
|
989
1049
|
<action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
|
|
@@ -996,8 +1056,8 @@ async function init(options) {
|
|
|
996
1056
|
const projectGradleTemplate = readFileSync2(join3(TEMPLATES_DIR, "build.gradle.kts.project.template"), "utf-8");
|
|
997
1057
|
writeFileSync(join3(output, "build.gradle.kts"), projectGradleTemplate.replace(/\{\{GOOGLE_SERVICES_PLUGIN\}\}/g, hasGoogleServices ? ' id("com.google.gms.google-services") version "4.4.2" apply false' : ""));
|
|
998
1058
|
const appGradleTemplate = readFileSync2(join3(TEMPLATES_DIR, "build.gradle.kts.app.template"), "utf-8");
|
|
999
|
-
const appGradle = appGradleTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{VERSION_NAME\}\}/g, escapeKotlinString(config.version || "1.0.0")).replace(/\{\{VERSION_CODE\}\}/g, String(config.versionCode || 1)).replace(/\{\{MIN_SDK\}\}/g, String(config.minSdk ?? ANDROID_MIN_SDK)).replace(/\{\{COMPILE_SDK\}\}/g, String(config.compileSdk || 36)).replace(/\{\{TARGET_SDK\}\}/g, String(config.targetSdk || 35)).replace(/\{\{GOOGLE_SERVICES_PLUGIN\}\}/g, hasGoogleServices ? ' id("com.google.gms.google-services")' : "").replace(/\{\{FIREBASE_MESSAGING_DEPENDENCY\}\}/g, config.enablePushNotifications ? ' implementation("com.google.firebase:firebase-messaging:24.1.0")' : "").replace(/\{\{REFRESH_LAYOUT_DEPENDENCY\}\}/g, config.renderer === "native" ? ' implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")' : "").replace(/\{\{
|
|
1000
|
-
implementation("
|
|
1059
|
+
const appGradle = appGradleTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{VERSION_NAME\}\}/g, escapeKotlinString(config.version || "1.0.0")).replace(/\{\{VERSION_CODE\}\}/g, String(config.versionCode || 1)).replace(/\{\{MIN_SDK\}\}/g, String(config.minSdk ?? ANDROID_MIN_SDK)).replace(/\{\{COMPILE_SDK\}\}/g, String(config.compileSdk || 36)).replace(/\{\{TARGET_SDK\}\}/g, String(config.targetSdk || 35)).replace(/\{\{GOOGLE_SERVICES_PLUGIN\}\}/g, hasGoogleServices ? ' id("com.google.gms.google-services")' : "").replace(/\{\{FIREBASE_MESSAGING_DEPENDENCY\}\}/g, config.enablePushNotifications ? ' implementation("com.google.firebase:firebase-messaging:24.1.0")' : "").replace(/\{\{REFRESH_LAYOUT_DEPENDENCY\}\}/g, config.renderer === "native" ? ' implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")' : "").replace(/\{\{NATIVE_MAP_DEPENDENCY\}\}/g, config.renderer === "native" ? ` // Native STX Map overlays and trail routes
|
|
1060
|
+
implementation("${androidMapHost(config.mapProvider).dependency}")` : "").replace(/\{\{HEALTH_CONNECT_DEPENDENCIES\}\}/g, config.enableHealthConnect ? ` implementation("androidx.health.connect:connect-client:1.1.0")
|
|
1001
1061
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")` : "").replace(/\{\{JAVASCRIPT_ENGINE_DEPENDENCY\}\}/g, config.renderer === "native" ? ` implementation("androidx.javascriptengine:javascriptengine:1.1.0")
|
|
1002
1062
|
// Native background refresh (WorkManager).
|
|
1003
1063
|
implementation("androidx.work:work-runtime-ktx:2.9.1")` : "");
|
|
@@ -1036,6 +1096,8 @@ zipStorePath=wrapper/dists
|
|
|
1036
1096
|
<color name="primary_dark">#16162b</color>
|
|
1037
1097
|
<color name="accent">#4ade80</color>
|
|
1038
1098
|
<color name="background">${config.backgroundColor}</color>
|
|
1099
|
+
<!-- Transparent is NotificationCompat's COLOR_DEFAULT: the system picks. -->
|
|
1100
|
+
<color name="craft_notification_color">${config.notificationColor ?? "#00000000"}</color>
|
|
1039
1101
|
</resources>
|
|
1040
1102
|
`;
|
|
1041
1103
|
writeFileSync(join3(output, "app/src/main/res/values/colors.xml"), colorsXml);
|
|
@@ -1064,6 +1126,21 @@ zipStorePath=wrapper/dists
|
|
|
1064
1126
|
} else {
|
|
1065
1127
|
writeFileSync(join3(output, "app/src/main/res/drawable/craft_app_icon.xml"), appIconXml);
|
|
1066
1128
|
}
|
|
1129
|
+
for (const extension of [".png", ".xml"])
|
|
1130
|
+
rmSync2(join3(output, `app/src/main/res/drawable/craft_notification_icon${extension}`), { force: true });
|
|
1131
|
+
if (config.notificationIconPath) {
|
|
1132
|
+
cpSync(config.notificationIconPath, join3(output, `app/src/main/res/drawable/craft_notification_icon${notificationIconExtension}`));
|
|
1133
|
+
} else {
|
|
1134
|
+
writeFileSync(join3(output, "app/src/main/res/drawable/craft_notification_icon.xml"), `<?xml version="1.0" encoding="utf-8"?>
|
|
1135
|
+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
1136
|
+
android:width="24dp"
|
|
1137
|
+
android:height="24dp"
|
|
1138
|
+
android:viewportWidth="108"
|
|
1139
|
+
android:viewportHeight="108">
|
|
1140
|
+
<path android:fillColor="#FFFFFFFF" android:pathData="M18,78L42,42l12,18l12,-12l24,30z" />
|
|
1141
|
+
</vector>
|
|
1142
|
+
`);
|
|
1143
|
+
}
|
|
1067
1144
|
const themesXml = `<?xml version="1.0" encoding="utf-8"?>
|
|
1068
1145
|
<resources>
|
|
1069
1146
|
<style name="Theme.CraftApp" parent="Theme.Material3.DayNight.NoActionBar">
|
|
@@ -1167,15 +1244,29 @@ async function build(options) {
|
|
|
1167
1244
|
if (htmlPath || devServer)
|
|
1168
1245
|
throw new Error("A native Android screen cannot use --html-path or --dev-server.");
|
|
1169
1246
|
const destination = join3(output, "app/src/main/assets/native-screen.js");
|
|
1170
|
-
|
|
1171
|
-
|
|
1247
|
+
const routeDestination = join3(output, "app/src/main/assets/native-screens");
|
|
1248
|
+
if (nativeBundlePath) {
|
|
1249
|
+
if (!existsSync(nativeBundlePath))
|
|
1250
|
+
throw new Error(`Native bundle not found: ${nativeBundlePath}`);
|
|
1251
|
+
if (statSync(nativeBundlePath).isDirectory()) {
|
|
1252
|
+
if (resolve2(nativeBundlePath) !== resolve2(routeDestination)) {
|
|
1253
|
+
rmSync2(routeDestination, { recursive: true, force: true });
|
|
1254
|
+
cpSync(nativeBundlePath, routeDestination, { recursive: true });
|
|
1255
|
+
}
|
|
1256
|
+
rmSync2(destination, { force: true });
|
|
1257
|
+
} else {
|
|
1258
|
+
if (resolve2(nativeBundlePath) !== resolve2(destination))
|
|
1259
|
+
cpSync(nativeBundlePath, destination);
|
|
1260
|
+
rmSync2(routeDestination, { recursive: true, force: true });
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1172
1263
|
const icons = join3(output, "app/src/main/assets/craft-icons.json");
|
|
1173
1264
|
if (options.iconsPath) {
|
|
1174
1265
|
requireRegularFile(options.iconsPath, "Icon table");
|
|
1175
1266
|
if (resolve2(options.iconsPath) !== resolve2(icons))
|
|
1176
1267
|
cpSync(options.iconsPath, icons);
|
|
1177
1268
|
}
|
|
1178
|
-
if (!existsSync(destination)) {
|
|
1269
|
+
if (!existsSync(destination) && !existsSync(routeDestination)) {
|
|
1179
1270
|
throw new Error("Native Android mode needs app/src/main/assets/native-screen.js. Compile a .stx screen with `stx native compile` first.");
|
|
1180
1271
|
}
|
|
1181
1272
|
} else if (nativeBundlePath) {
|
|
@@ -1286,6 +1377,8 @@ export {
|
|
|
1286
1377
|
renderAndroidPermissions,
|
|
1287
1378
|
renderAndroidDeepLinks,
|
|
1288
1379
|
open,
|
|
1380
|
+
nativeRouteBundles,
|
|
1381
|
+
nativeMapProviders,
|
|
1289
1382
|
installRuntime,
|
|
1290
1383
|
init,
|
|
1291
1384
|
healthConnectPermissions,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Build-time native map selection. Both providers are native Android views. */
|
|
2
|
+
export type AndroidMapProvider = 'google' | 'maplibre';
|
|
3
|
+
export declare function androidMapHost(provider?: AndroidMapProvider): {
|
|
4
|
+
template: string;
|
|
5
|
+
imports: string;
|
|
6
|
+
dependency: string;
|
|
7
|
+
};
|
|
@@ -3475,7 +3475,8 @@ class CraftBridge(
|
|
|
3475
3475
|
val notificationManager = activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
3476
3476
|
|
|
3477
3477
|
val builder = androidx.core.app.NotificationCompat.Builder(activity, notificationChannelId)
|
|
3478
|
-
.setSmallIcon(
|
|
3478
|
+
.setSmallIcon(R.drawable.craft_notification_icon)
|
|
3479
|
+
.setColor(ContextCompat.getColor(activity, R.color.craft_notification_color))
|
|
3479
3480
|
.setContentTitle(title)
|
|
3480
3481
|
.setContentText(body)
|
|
3481
3482
|
.setPriority(androidx.core.app.NotificationCompat.PRIORITY_DEFAULT)
|
|
@@ -56,6 +56,33 @@ internal object CraftNativeSandbox {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* The native screens' JavaScript: a route's own file when stx wrote one
|
|
61
|
+
* (`native-screens/<route>.js`, its `routesDir`), else the one-file
|
|
62
|
+
* `native-screen.js`. An isolate evaluates all it is given, so a route file
|
|
63
|
+
* spares each screen parsing (and sending the sandbox) every other screen.
|
|
64
|
+
* Read once and kept while memory allows.
|
|
65
|
+
*/
|
|
66
|
+
internal object CraftNativeBundle {
|
|
67
|
+
private val sources = object : android.util.LruCache<String, String>(24 shl 20) {
|
|
68
|
+
override fun sizeOf(key: String, value: String) = value.length
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private fun read(context: Context, asset: String): String? = sources.get(asset)
|
|
72
|
+
?: runCatching { context.assets.open(asset).bufferedReader().use { it.readText() } }.getOrNull()?.also { sources.put(asset, it) }
|
|
73
|
+
|
|
74
|
+
fun source(context: Context, route: String?): String? {
|
|
75
|
+
if (!route.isNullOrBlank() && !route.contains('/') && !route.startsWith('.')) {
|
|
76
|
+
read(context, "native-screens/$route.js")?.let { return it }
|
|
77
|
+
}
|
|
78
|
+
return read(context, "native-screen.js")
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fun isAvailable(context: Context): Boolean =
|
|
82
|
+
runCatching { context.assets.open("native-screen.js").close(); true }.getOrDefault(false)
|
|
83
|
+
|| (context.assets.list("native-screens")?.isNotEmpty() == true)
|
|
84
|
+
}
|
|
85
|
+
|
|
59
86
|
/**
|
|
60
87
|
* Background refresh, as iOS's BGAppRefreshTask gives it: WorkManager wakes
|
|
61
88
|
* the app every `backgroundRefresh.minimumIntervalMinutes` (15 at the
|
|
@@ -153,8 +180,7 @@ internal class CraftNativeHeadlessApp(
|
|
|
153
180
|
fun start() {
|
|
154
181
|
val route = config.optString("nativeApp")
|
|
155
182
|
if (route.isBlank() || !JavaScriptSandbox.isSupported()) return finish(true)
|
|
156
|
-
val bundle =
|
|
157
|
-
?: return finish(true)
|
|
183
|
+
val bundle = CraftNativeBundle.source(context, route) ?: return finish(true)
|
|
158
184
|
CraftNativeSandbox.connect(context) { engine, error ->
|
|
159
185
|
if (closed) return@connect
|
|
160
186
|
if (engine == null || !engine.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_CONSOLE_MESSAGING)) {
|