@craft-native/android 0.0.203 → 0.0.205
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 +30 -8
- package/dist/index.d.ts +1 -0
- package/dist/index.js +91 -1
- package/dist/index.js.map +3 -3
- package/package.json +1 -1
- package/templates/CraftAppLinks.kt.template +9 -2
- package/templates/CraftNativeFlatList.kt.template +117 -0
- package/templates/CraftNativeShell.kt.template +42 -10
- package/templates/MainActivityNative.kt.template +527 -50
package/README.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Build native Android apps with web technologies using Craft.
|
|
4
4
|
|
|
5
|
+
`appLinks` path rules use the website association wildcards: `*` matches zero
|
|
6
|
+
or more characters, `?` matches one, and `?*` matches one or more. The first
|
|
7
|
+
matching rule decides whether a link opens in the app or browser. For example,
|
|
8
|
+
`/gyms/*/?*` excludes a nonempty subpath without excluding `/gyms/trifit/`.
|
|
9
|
+
The `?` and `?*` wildcards require Craft 0.0.204 or later.
|
|
10
|
+
|
|
5
11
|
## Features
|
|
6
12
|
|
|
7
13
|
### Core
|
|
@@ -100,16 +106,21 @@ Native mode maps STX `View`, `SafeAreaView`, `Text`, `Button`, `Link`, `Icon`,
|
|
|
100
106
|
`Pressable`, `TouchableOpacity`, `TouchableHighlight`, `Checkbox`, `TextInput`, `Picker`, `DatePicker`, `Modal`, `Switch`, `Slider`, `ActivityIndicator`, `Image`, `ImageBackground`, `Video`, `Svg`, `G`, `SvgText`, `ScrollView`,
|
|
101
107
|
`Map`, `SectionList`, and `FlatList` to Android views. `Map` uses the Google
|
|
102
108
|
Maps SDK for trail polylines and marker taps; its overlays carry flat
|
|
103
|
-
latitude/longitude `geometry` arrays
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
109
|
+
latitude/longitude `geometry` arrays, `strokeWidth` is expressed in logical
|
|
110
|
+
points, and the camera supports controlled `region`, one-time `initialRegion`,
|
|
111
|
+
and `fitToBounds`. Set `mapsApiKey` in the Android config to emit the SDK manifest key;
|
|
112
|
+
Coordinates must be JSON numbers (numeric strings are not coerced), are
|
|
113
|
+
range-checked at the native boundary, and invalid points or pole-crossing
|
|
114
|
+
regions are ignored. `fitToBounds` waits for the map viewport to
|
|
115
|
+
be measured before caching a successful camera fit, so an early map callback
|
|
116
|
+
does not lose the initial route framing.
|
|
107
117
|
without it, the native map remains an explicit opt-in integration that needs
|
|
108
118
|
the app's own Maps credentials. `SectionList` uses the same
|
|
109
119
|
native scroll host for ordinary `.stx` slot children; grouped section data
|
|
110
120
|
should use `FlatList` until the section-data protocol is available. The
|
|
111
121
|
generated host routes both list names through `CraftNativeScrollView` for this
|
|
112
|
-
slot-based contract. `
|
|
122
|
+
slot-based contract. `ScrollView` also supports `stickyHeaderIndices` and
|
|
123
|
+
`scrollTarget` for vertical native content. `Picker` renders `<select>`/`<option>` values
|
|
113
124
|
with `onValueChange` or `onChange`. `DatePicker` uses the platform `DatePicker`
|
|
114
125
|
and `TimePicker` controls for date, time, datetime, and countdown modes and
|
|
115
126
|
emits local `value`, ISO, and millisecond `timestamp` fields. `Modal` is parent-scoped and supports
|
|
@@ -119,7 +130,10 @@ and `thumbColor`, while `Slider` supports track/thumb tint props and `step`
|
|
|
119
130
|
snapping without collapsing stepped ranges to a fixed pixel-like precision.
|
|
120
131
|
`ActivityIndicator` keeps a visible stopped indicator genuinely
|
|
121
132
|
non-animated when `animating={false}` and `hidesWhenStopped={false}`. `Image`
|
|
122
|
-
reports load lifecycle events and structured failures.
|
|
133
|
+
reports load lifecycle events and structured failures. Native image sources
|
|
134
|
+
accept HTTPS everywhere and loopback HTTP for local development (`localhost`,
|
|
135
|
+
`127.0.0.1`, `10.0.2.2`, `::1`, and `*.localhost`); public HTTP and unsupported
|
|
136
|
+
schemes are rejected. `Video` uses the native
|
|
123
137
|
`VideoView` for URI playback with autoplay, pause, looping, mute, controls, and
|
|
124
138
|
resize-mode props.
|
|
125
139
|
`ImageBackground` loads an image behind ordinary native children while
|
|
@@ -140,8 +154,9 @@ become checked on their first user toggle.
|
|
|
140
154
|
horizontal and inverted layouts, list chrome, and end-reached pagination.
|
|
141
155
|
Flex items support `flex`, `flexGrow`, `flexShrink`, and numeric or percentage
|
|
142
156
|
`flexBasis` for main-axis space distribution while preserving gaps and margins.
|
|
143
|
-
Shared transforms, z-order, view shadows, text shadows,
|
|
144
|
-
are applied with the same style keys as the iOS
|
|
157
|
+
Shared transforms, z-order, view shadows, text shadows, per-edge border
|
|
158
|
+
styles, and linear gradients are applied with the same style keys as the iOS
|
|
159
|
+
renderer. Android `elevation` remains an additional native shadow input.
|
|
145
160
|
`gap`, `rowGap`, and `columnGap` retain fractional density-independent values
|
|
146
161
|
through line distribution and round only the final native frames.
|
|
147
162
|
Numeric lengths and percentage widths/heights resolve against the native parent
|
|
@@ -228,11 +243,18 @@ With `nativeScreens` (paths to the screens that draw them), `tabs` and
|
|
|
228
243
|
`nativeApp` in craft.config.json, the same keys as the iOS builder, the
|
|
229
244
|
native renderer is a whole app rather than one bundle route:
|
|
230
245
|
|
|
246
|
+
`craft android init` and `craft android build` validate every tab and the
|
|
247
|
+
configured start path against `nativeScreens`; invalid native route tables are
|
|
248
|
+
rejected before a project is generated.
|
|
249
|
+
|
|
231
250
|
- a tab bar and a navigation bar (`craft.navigation.setOptions`: title, large
|
|
232
251
|
title, right buttons with badges, `tabBarHidden`), a stack per tab, Android's
|
|
233
252
|
Back key popping it; completed left/right swipes switch between tab roots
|
|
234
253
|
interactively, yield to a horizontal scroll view, and can be disabled per
|
|
235
254
|
screen with `craft.navigation.setOptions({ tabSwipeEnabled: false })`;
|
|
255
|
+
each screen may also set `backgroundColor`; omitting it or passing `null`
|
|
256
|
+
follows the app's light/dark background resource, and the navigation bar and
|
|
257
|
+
large title use the same resolved color;
|
|
236
258
|
- `craft.navigation.open(path)` matched in the iOS shell's order (fixed segments
|
|
237
259
|
before parameters), a tab's path selecting its tab, a path no screen draws
|
|
238
260
|
opening the site in a Custom Tab; deep links and tapped notifications route
|
package/dist/index.d.ts
CHANGED
|
@@ -52,6 +52,7 @@ export interface CraftAndroidConfig {
|
|
|
52
52
|
enableLocalNetwork?: boolean;
|
|
53
53
|
urlSchemes?: string[];
|
|
54
54
|
/** HTTPS hosts owned by the app; path rules are ordered, with `*` matching any path text. */
|
|
55
|
+
/** Ordered path rules: * matches zero or more characters, ? one, and ?* one or more. */
|
|
55
56
|
appLinks?: Array<{
|
|
56
57
|
host: string;
|
|
57
58
|
paths?: Array<{
|
package/dist/index.js
CHANGED
|
@@ -380,6 +380,94 @@ function validateAndroidConfig(config) {
|
|
|
380
380
|
throw new Error("Android targetSdk must not exceed compileSdk");
|
|
381
381
|
}
|
|
382
382
|
}
|
|
383
|
+
function normalizeNativeRoutePath(raw) {
|
|
384
|
+
if (typeof raw !== "string")
|
|
385
|
+
return null;
|
|
386
|
+
let path = raw;
|
|
387
|
+
const cut = path.search(/[?#]/);
|
|
388
|
+
if (cut >= 0)
|
|
389
|
+
path = path.slice(0, cut);
|
|
390
|
+
if (!path.startsWith("/"))
|
|
391
|
+
return null;
|
|
392
|
+
path = path.replace(/\/{2,}/g, "/");
|
|
393
|
+
if (path.length > 1 && path.endsWith("/"))
|
|
394
|
+
path = path.slice(0, -1);
|
|
395
|
+
return path;
|
|
396
|
+
}
|
|
397
|
+
function nativeRouteSegments(path) {
|
|
398
|
+
return path === "/" ? [] : path.slice(1).split("/");
|
|
399
|
+
}
|
|
400
|
+
function matchesNativeRoute(nativeScreens, raw) {
|
|
401
|
+
const path = normalizeNativeRoutePath(raw);
|
|
402
|
+
if (path === null)
|
|
403
|
+
return false;
|
|
404
|
+
const parts = nativeRouteSegments(path);
|
|
405
|
+
return Object.entries(nativeScreens ?? {}).some(([pattern, screen]) => {
|
|
406
|
+
if (!screen)
|
|
407
|
+
return false;
|
|
408
|
+
const normalized = normalizeNativeRoutePath(pattern);
|
|
409
|
+
if (normalized === null)
|
|
410
|
+
return false;
|
|
411
|
+
const route = nativeRouteSegments(normalized);
|
|
412
|
+
for (let index = 0;index < route.length; index++) {
|
|
413
|
+
const segment = route[index];
|
|
414
|
+
if (segment === "*")
|
|
415
|
+
return index === route.length - 1;
|
|
416
|
+
if (index >= parts.length)
|
|
417
|
+
return false;
|
|
418
|
+
if (segment.startsWith(":")) {
|
|
419
|
+
if (!parts[index])
|
|
420
|
+
return false;
|
|
421
|
+
} else if (segment !== parts[index])
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
return route.length === parts.length;
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
function nativeStartPath(config) {
|
|
428
|
+
if (!config.devServerURL)
|
|
429
|
+
return "/";
|
|
430
|
+
try {
|
|
431
|
+
const url = new URL(config.devServerURL);
|
|
432
|
+
return `${url.pathname || "/"}${url.search}`;
|
|
433
|
+
} catch {
|
|
434
|
+
return "/";
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
function validateNativeRouteConfig(config) {
|
|
438
|
+
const screens = Object.entries(config.nativeScreens ?? {});
|
|
439
|
+
for (const [pattern, screen] of screens) {
|
|
440
|
+
if (normalizeNativeRoutePath(pattern) !== pattern || /[?#]/.test(pattern))
|
|
441
|
+
throw new Error(`nativeScreens path "${pattern}" must be an absolute path with no query, hash or trailing slash.`);
|
|
442
|
+
if (nativeRouteSegments(normalizeNativeRoutePath(pattern) ?? "/").slice(0, -1).includes("*"))
|
|
443
|
+
throw new Error(`nativeScreens path "${pattern}" may only end in *.`);
|
|
444
|
+
if (typeof screen !== "string" || !/^[A-Za-z_$][\w$-]*$/.test(screen))
|
|
445
|
+
throw new Error(`nativeScreens screen "${String(screen)}" for "${pattern}" must be a screen name from the native bundle.`);
|
|
446
|
+
}
|
|
447
|
+
const ids = new Set;
|
|
448
|
+
for (const tab of config.tabs ?? []) {
|
|
449
|
+
if (!tab || typeof tab.id !== "string" || !tab.id || typeof tab.title !== "string" || !tab.title)
|
|
450
|
+
throw new Error("Each tab needs an id (its root path) and a title.");
|
|
451
|
+
if (ids.has(tab.id))
|
|
452
|
+
throw new Error(`Tab "${tab.id}" is listed twice.`);
|
|
453
|
+
ids.add(tab.id);
|
|
454
|
+
}
|
|
455
|
+
if (config.renderer === "native" && screens.length > 0) {
|
|
456
|
+
for (const id of ids) {
|
|
457
|
+
if (!matchesNativeRoute(config.nativeScreens, id))
|
|
458
|
+
throw new Error(`Tab "${id}" is not a nativeScreens path; with renderer: "native" every tab must be a native screen.`);
|
|
459
|
+
}
|
|
460
|
+
const start = nativeStartPath(config);
|
|
461
|
+
if (!matchesNativeRoute(config.nativeScreens, start))
|
|
462
|
+
throw new Error(`The start path "${start}" (from devServerURL) is not a nativeScreens path; with renderer: "native" the app must start on a native screen.`);
|
|
463
|
+
}
|
|
464
|
+
if (config.nativeApp !== undefined) {
|
|
465
|
+
if (typeof config.nativeApp !== "string" || !/^[A-Za-z_$][\w$-]*$/.test(config.nativeApp))
|
|
466
|
+
throw new Error(`nativeApp "${String(config.nativeApp)}" must be a route name from the native bundle.`);
|
|
467
|
+
if (config.renderer !== "native" && screens.length === 0)
|
|
468
|
+
throw new Error('nativeApp runs in the native bundle; it needs nativeScreens or renderer: "native".');
|
|
469
|
+
}
|
|
470
|
+
}
|
|
383
471
|
function writeAndroidConfig(output, config) {
|
|
384
472
|
writeFileSync(join2(output, "craft.config.json"), JSON.stringify(config, null, 2));
|
|
385
473
|
const runtimeConfig = { ...config };
|
|
@@ -601,6 +689,7 @@ async function init(options) {
|
|
|
601
689
|
}
|
|
602
690
|
normalizeAndroidNetworkConfig(config);
|
|
603
691
|
validateAndroidConfig(config);
|
|
692
|
+
validateNativeRouteConfig(config);
|
|
604
693
|
if (config.enablePushNotifications && !config.googleServicesFile) {
|
|
605
694
|
throw new Error("Android push notifications require a googleServicesFile");
|
|
606
695
|
}
|
|
@@ -861,6 +950,7 @@ async function build(options) {
|
|
|
861
950
|
}
|
|
862
951
|
const config = JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
863
952
|
validateAndroidConfig(config);
|
|
953
|
+
validateNativeRouteConfig(config);
|
|
864
954
|
if (config.renderer === "native") {
|
|
865
955
|
if (htmlPath || devServer)
|
|
866
956
|
throw new Error("A native Android screen cannot use --html-path or --dev-server.");
|
|
@@ -990,4 +1080,4 @@ export {
|
|
|
990
1080
|
ANDROID_MIN_SDK
|
|
991
1081
|
};
|
|
992
1082
|
|
|
993
|
-
//# debugId=
|
|
1083
|
+
//# debugId=D95CCF804E0EBBCF64756E2164756E21
|