create-mithril-lynx 2.3.0 → 2.5.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/README.md +36 -25
- package/package.json +1 -1
- package/src/fonts-wire.js +53 -53
- package/src/index.js +108 -59
- package/templates/_shared/ts/src/rspeedy-env.d.ts +9 -0
- package/templates/android/app-scripts/android-prepare.mjs +45 -0
- package/templates/android/host/app/src/main/AndroidManifest.xml +1 -0
- package/templates/android/host/app/src/main/java/package-path/MainActivity.kt +27 -0
- package/templates/android/host/app/src/main/java/package-path/MithrilLynxNavigationModule.kt +28 -0
- package/templates/android/host/app/src/main/res/xml/config.xml +8 -0
- package/templates/android/host/app/src/main/res/xml/file_paths.xml +10 -0
- package/templates/android/host/app/src/main/res/xml/network_security_config.xml +9 -0
- package/templates/basic-activity/ts/src/app-bar.ts +3 -4
- package/templates/basic-activity/ts/src/background.ts +8 -0
- /package/templates/android/host/app/src/main/res/values/{themes.xml → styles.xml} +0 -0
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ Every flag, in one table — `npx create-mithril-lynx --help` prints the same li
|
|
|
50
50
|
| `--android-id <id>` | `applicationId` / `namespace` (default `com.example.<name>`). |
|
|
51
51
|
| `--app-name <name>` | Launcher label (default: the project name). |
|
|
52
52
|
| `--with-font <file.ttf>` | Bundle the font into `src/assets/fonts/` and register it with `lynx.addFont()` in a loop. **DEV** (`npm run dev` / Lynx Go): `require()` inlines a `data:` URI so the font shows up in Explorer. **PROD** (`npm run build` / APK): `asset:///fonts/<file>` resolved by `AssetFontFaceLoader` on the host (keeps the bundle small — embedding `data:` URIs in the APK re-introduces the cold-start cost tracked in [lynx#9431](https://github.com/lynx-family/lynx/issues/9431)). Comma-separate for more than one file. |
|
|
53
|
-
| `--find-font <term>` | Search [Fontsource](https://fontsource.org)'s catalog for `<term>`, prompt you to pick a family and
|
|
53
|
+
| `--find-font <term>` | Search [Fontsource](https://fontsource.org)'s catalog for `<term>`, prompt you to pick a family and one or more weights/styles, download each `.ttf`, and bundle them exactly like `--with-font`. Needs a real terminal (the picking is inherently interactive — use `--with-font <file.ttf>` in scripts/CI). Mutually exclusive with `--with-font`. Comma-separate terms for more than one — quote the whole thing if any term has a space. |
|
|
54
54
|
| `--font-family <name>` | Override the family name derived from the font's file name (or from Fontsource, with `--find-font`). Only valid with exactly one font. |
|
|
55
55
|
|
|
56
56
|
Anything not listed — in particular the four Android flags — requires `--android`
|
|
@@ -67,18 +67,23 @@ npx create-mithril-lynx my-app --blank --android --find-font "Inter,JetBrains Mo
|
|
|
67
67
|
|
|
68
68
|
Each font is one entry in a `FONTS` array in `src/background.ts`, registered
|
|
69
69
|
with a single `for…of` + `lynx.addFont()` loop (DEV `require` / PROD
|
|
70
|
-
`asset:///` — see the `--with-font` row above).
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
`asset:///` — see the `--with-font` row above).
|
|
71
|
+
|
|
72
|
+
The CSS side lives in a separate, **generated** file, `src/fonts.css`, which
|
|
73
|
+
`src/style.css` pulls in with a single `@import "fonts.css";` line at the top.
|
|
74
|
+
`fonts.css` holds `text { font-family: ...; }` for the **first** font and, when
|
|
75
|
+
there is more than one font, one class per family/variant:
|
|
75
76
|
|
|
76
77
|
```css
|
|
77
|
-
.
|
|
78
|
-
|
|
79
|
-
}
|
|
78
|
+
.Ubuntu_Regular { font-family: "Ubuntu Regular", sans-serif; }
|
|
79
|
+
.Ubuntu_Bold { font-family: "Ubuntu Bold", sans-serif; }
|
|
80
|
+
.Inter_Regular { font-family: "Inter Regular", sans-serif; }
|
|
80
81
|
```
|
|
81
82
|
|
|
83
|
+
`fonts.css` is owned by the tooling and rewritten from the full font list on
|
|
84
|
+
every run (scaffold or `add-font`) — don't edit it by hand; put your own rules
|
|
85
|
+
in `style.css`. (Unused classes can be pruned by a later PostCSS pass.)
|
|
86
|
+
|
|
82
87
|
### Adding fonts after the project exists
|
|
83
88
|
|
|
84
89
|
```bash
|
|
@@ -88,11 +93,12 @@ npx create-mithril-lynx add-font --find-font "Roboto Mono"
|
|
|
88
93
|
```
|
|
89
94
|
|
|
90
95
|
Copying the `.ttf` into `src/assets/fonts/` (and the Android host's
|
|
91
|
-
`assets/fonts/` when present) is always safe.
|
|
92
|
-
`style.css`
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
+
`assets/fonts/` when present) is always safe. `src/fonts.css` is always
|
|
97
|
+
regenerated, and `style.css` only ever gets the one `@import "fonts.css";`
|
|
98
|
+
line (added if missing). Wiring `background.ts` is automatic **only while it
|
|
99
|
+
still matches the stock template** (including a previously generated `FONTS`
|
|
100
|
+
block on top of it); if you've edited it, the command prints the exact block
|
|
101
|
+
to paste by hand and leaves your file untouched.
|
|
96
102
|
|
|
97
103
|
### `--find-font`, in more detail
|
|
98
104
|
|
|
@@ -101,11 +107,13 @@ exact `id`/`family` filters — so `--find-font` downloads the whole font list
|
|
|
101
107
|
once (~2100 fonts, ~540KB as of 2026-09), caches it for 24h in the OS temp
|
|
102
108
|
directory, and filters client-side on `family`/`id` substrings. Picking a
|
|
103
109
|
family fetches that font's own detail (its real weight/style/subset `.ttf`
|
|
104
|
-
URLs), lets you pick one
|
|
105
|
-
downloads
|
|
110
|
+
URLs), lets you pick one or more variants (space to toggle; defaulting to 400/normal if available),
|
|
111
|
+
downloads each to a temp file, and hands them to the same code path
|
|
106
112
|
`--with-font <file>` uses — there's no separate font-loading mechanism to
|
|
107
113
|
maintain.
|
|
108
114
|
|
|
115
|
+
Every picked variant is registered as `<Family> <Variant>` (`Inter Regular`, `Inter Bold`, `Inter Light Italic`…), with a matching class in `src/fonts.css` (`.Inter_Bold`). The first one (400/normal if picked) is the default `text` font.
|
|
116
|
+
|
|
109
117
|
```bash
|
|
110
118
|
npx create-mithril-lynx my-app --blank --android --find-font Inter
|
|
111
119
|
```
|
|
@@ -116,8 +124,8 @@ The sibling `<name>-android/` project is a complete, no-Android-Studio Gradle CL
|
|
|
116
124
|
|
|
117
125
|
- the Gradle wrapper (`gradlew`, `gradlew.bat`, `gradle-wrapper.jar`), so nothing needs to be installed besides a JDK and the Android SDK;
|
|
118
126
|
- `local.properties` with `sdk.dir` auto-detected from `ANDROID_HOME`/`ANDROID_SDK_ROOT` (with a written warning if it can't be found);
|
|
119
|
-
- the Kotlin host: the `Application` (registers `AssetFontFaceLoader`), `MainActivity` (async `AssetTemplateProvider`, splash screen, `NoopGenericResourceFetcher` for the fast `data:` font path — [lynx#9431](https://github.com/lynx-family/lynx/issues/9431));
|
|
120
|
-
-
|
|
127
|
+
- the Kotlin host: the `Application` (registers `AssetFontFaceLoader`), `MainActivity` (async `AssetTemplateProvider`, splash screen, `NoopGenericResourceFetcher` for the fast `data:` font path — [lynx#9431](https://github.com/lynx-family/lynx/issues/9431)), and a predefined native bridge for `mithril-lynx/route`'s Android system Back support;
|
|
128
|
+
- a project-local `android/` folder with editable `AndroidManifest.xml`, `strings.xml`, `styles.xml`, `config.xml`, `network_security_config.xml`, and `file_paths.xml`, plus `bun run android:prepare` to copy them into the Gradle host;
|
|
121
129
|
- an opt-in release signing setup driven by a gitignored `keystore.properties`.
|
|
122
130
|
|
|
123
131
|
And in the JS project, a `scripts/android.mjs` bridge plus npm scripts:
|
|
@@ -127,10 +135,11 @@ npm run android # build + sync + installDebug + launch, printing Tota
|
|
|
127
135
|
npm run android:apk # build + sync + assembleDebug
|
|
128
136
|
npm run android:release # build + sync + assembleRelease (signed if keystore.properties exists)
|
|
129
137
|
npm run android:sync # build + sync bundle into app/src/main/assets/, no Gradle
|
|
138
|
+
bun run android:prepare # manually sync android/*.xml -> sibling Android host
|
|
130
139
|
npm run android:keystore # generate release.keystore + keystore.properties (KEYSTORE_PASSWORD=...)
|
|
131
140
|
```
|
|
132
141
|
|
|
133
|
-
|
|
142
|
+
The packaging path still wraps whatever bundle `rspeedy build` produces. Its one framework-aware convenience is the predefined, otherwise dormant system Back bridge used by `mithril-lynx/route`; the Basic Activity template connects it automatically.
|
|
134
143
|
|
|
135
144
|
## Templates
|
|
136
145
|
|
|
@@ -138,7 +147,7 @@ This part of the tool is framework-agnostic — it just wraps whatever bundle `r
|
|
|
138
147
|
|---|---|
|
|
139
148
|
| **Hello World** (recommended) | The Mithril.js analog of Lynx's official React hello-world ([`lynx-examples/examples/hello-world`](https://github.com/lynx-family/lynx-examples/tree/main/examples/hello-world)) — same layout, same "tap the logo" interaction, running through Mithril hyperscript instead of JSX. |
|
|
140
149
|
| **Blank** | A single centered line of text. Nothing else — the starting point when you don't want any of Hello World's styling/assets in your way. |
|
|
141
|
-
| **Basic Activity** | Two screens (Main → Details) wired with [`mithril-lynx/route`](https://github.com/carlos-sweb/mithril-lynx/blob/main/ROUTE.md) — an in-memory router (`m.route`, reimplemented for an environment with no URL bar). Includes a reusable app-bar
|
|
150
|
+
| **Basic Activity** | Two screens (Main → Details) wired with [`mithril-lynx/route`](https://github.com/carlos-sweb/mithril-lynx/blob/main/ROUTE.md) — an in-memory router (`m.route`, reimplemented for an environment with no URL bar). Includes a reusable app-bar back affordance and, with `--android`, predefined system Back/predictive Back integration through `route.listenBackButton()`. |
|
|
142
151
|
|
|
143
152
|
## Package structure
|
|
144
153
|
|
|
@@ -164,15 +173,17 @@ create-mithril-lynx/
|
|
|
164
173
|
ts/src/{app-bar.ts, background.ts, screens/{home,detail}.ts}
|
|
165
174
|
android/ only used with --android
|
|
166
175
|
host/ the Gradle project -> <name>-android/
|
|
167
|
-
(includes AssetFontFaceLoader
|
|
168
|
-
NoopGenericResourceFetcher for lynx#9431
|
|
169
|
-
|
|
170
|
-
|
|
176
|
+
(includes AssetFontFaceLoader,
|
|
177
|
+
NoopGenericResourceFetcher for lynx#9431, and the
|
|
178
|
+
MithrilLynxNavigationModule system-Back bridge)
|
|
179
|
+
app-scripts/{android,android-prepare}.mjs
|
|
180
|
+
bridges in <name>/scripts/ (syncs bundle/fonts;
|
|
181
|
+
prepares <name>/android/*.xml into the host)
|
|
171
182
|
```
|
|
172
183
|
|
|
173
184
|
`src/index.js` copies, in order, `templates/_shared/common` → `templates/_shared/ts` → `templates/<template>/common` → `templates/<template>/ts` into the target directory — later copies overwrite same-named files from earlier ones, which is exactly how **Basic Activity**'s own routing-based `src/background.ts` replaces `_shared/ts`'s generic single-view one (Hello World and Blank don't ship their own, so they keep the shared file). It then renames `gitignore` to `.gitignore` (npm doesn't publish dotfiles reliably otherwise) and replaces `{{PROJECT_NAME}}`/`{{MITHRIL_LYNX_VERSION}}` placeholders in `package.json` and `README.md`.
|
|
174
185
|
|
|
175
|
-
With `--android` it then copies `templates/android/host` into a sibling `<name>-android/` (renaming `package-path` to the real package directory and `App.kt` to the Application class) and substitutes `{{PACKAGE_NAME}}`, `{{APP_CLASS}}`, `{{APP_NAME}}`, `{{ANDROID_DIR}}` and `{{SDK_DIR}}` across the tree. Binary files (the Gradle wrapper jar, a `--with-font` `.ttf`) are never text-substituted. With `--with-font`, each font is copied into the JS project (`src/assets/fonts/`) *and* seeded into the Android host's `assets/fonts/`, wired up with a `FONTS` loop + `lynx.addFont()` in `src/background.ts` plus a `text { font-family
|
|
186
|
+
With `--android` it then copies `templates/android/host` into a sibling `<name>-android/` (renaming `package-path` to the real package directory and `App.kt` to the Application class) and substitutes `{{PACKAGE_NAME}}`, `{{APP_CLASS}}`, `{{APP_NAME}}`, `{{ANDROID_DIR}}` and `{{SDK_DIR}}` across the tree. It also seeds the generated project's `android/` folder with `AndroidManifest.xml`, `strings.xml`, `styles.xml`, `config.xml`, `network_security_config.xml`, and `file_paths.xml`. `bun run build` (and the Android build commands that invoke it) automatically synchronizes those local copies to the Gradle host first; use `bun run android:prepare` (or `npm run android:prepare`) when you only want that synchronization. Binary files (the Gradle wrapper jar, a `--with-font` `.ttf`) are never text-substituted. With `--with-font`, each font is copied into the JS project (`src/assets/fonts/`) *and* seeded into the Android host's `assets/fonts/`, wired up with a `FONTS` loop + `lynx.addFont()` in `src/background.ts` plus a generated `src/fonts.css` (`text { font-family }` for the first font, one class per font when there are several) imported from `src/style.css`. `scripts/android.mjs` keeps `assets/fonts/` in sync on every `npm run android` / `android:sync`.
|
|
176
187
|
|
|
177
188
|
**Not carried over from v1 (yet)**: the JavaScript variant. The old tool generated either TypeScript or JavaScript for every template; this rewrite ships TypeScript only for now — doubling every template for a parallel JS copy wasn't part of this pass.
|
|
178
189
|
|
package/package.json
CHANGED
package/src/fonts-wire.js
CHANGED
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
* host's assets/fonts/ when present) — that part is always safe.
|
|
8
8
|
* 2. If src/background.ts still matches a stock template (optionally with
|
|
9
9
|
* our generated FONTS block on top), rewrite the font block in place.
|
|
10
|
-
* 3.
|
|
10
|
+
* 3. Always (re)generate src/fonts.css — a system-owned file holding the
|
|
11
|
+
* `text { font-family }` default and one class per family/variant — and
|
|
12
|
+
* make sure src/style.css has `@import "fonts.css";`. Users never edit
|
|
13
|
+
* fonts.css by hand; every add-font run rewrites it from the full list.
|
|
11
14
|
* 4. Otherwise print the exact lines the user must paste by hand.
|
|
12
15
|
*/
|
|
13
16
|
import fs from "node:fs";
|
|
@@ -33,7 +36,6 @@ export function normalizeSource(text) {
|
|
|
33
36
|
|
|
34
37
|
export function buildFontBlock(fonts) {
|
|
35
38
|
const defaultFamily = fonts[0].family;
|
|
36
|
-
const extraFamilies = fonts.slice(1).map((f) => f.family);
|
|
37
39
|
const requireEntries = fonts
|
|
38
40
|
.map(
|
|
39
41
|
({ family, file }) =>
|
|
@@ -55,13 +57,8 @@ export function buildFontBlock(fonts) {
|
|
|
55
57
|
"// • DEV (Lynx Go / `npm run dev`): require() → data: URI inline",
|
|
56
58
|
"// • PROD (Android APK / `npm run build`): asset:///fonts/<file>",
|
|
57
59
|
"// resolved by AssetFontFaceLoader (lynx#9431 workaround).",
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
`// Only "${defaultFamily}" got the automatic text { font-family: ... }`,
|
|
61
|
-
`// rule below — assign ${extraFamilies.map((f) => `"${f}"`).join(" / ")} to your own`,
|
|
62
|
-
'// classes in src/style.css, e.g. `.code { font-family: "' + extraFamilies[0] + '"; }`.',
|
|
63
|
-
]
|
|
64
|
-
: []),
|
|
60
|
+
"// The default `text { font-family }` and one .Family_Variant class per font",
|
|
61
|
+
"// live in src/fonts.css, which is generated — don't edit it by hand.",
|
|
65
62
|
"type FontEntry = { family: string; file: string; url: string };",
|
|
66
63
|
"const FONTS: FontEntry[] = import.meta.env.DEV",
|
|
67
64
|
"\t? [",
|
|
@@ -78,8 +75,35 @@ export function buildFontBlock(fonts) {
|
|
|
78
75
|
].join("\n");
|
|
79
76
|
}
|
|
80
77
|
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
/** `Ubuntu Light Italic` → `Ubuntu_Light_Italic` (usable as a CSS class name). */
|
|
79
|
+
export function cssClassName(family) {
|
|
80
|
+
return family.trim().replace(/[^\w-]+/g, "_");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const FONTS_CSS_FILE = "fonts.css";
|
|
84
|
+
const FONTS_CSS_IMPORT = `@import "${FONTS_CSS_FILE}";`;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Contents of src/fonts.css. With one font: just the `text` default. With
|
|
88
|
+
* more than one: also a `.Family_Variant { font-family }` class per font, so
|
|
89
|
+
* a later (postcss) pass can drop the classes the app never uses.
|
|
90
|
+
*/
|
|
91
|
+
export function buildFontsCss(fonts) {
|
|
92
|
+
const lines = [
|
|
93
|
+
"/* GENERATED by create-mithril-lynx (--with-font / --find-font / add-font).",
|
|
94
|
+
" * Owned by the tooling: it is rewritten from the full font list on every",
|
|
95
|
+
" * add-font run, so do not edit it by hand — put your own rules in style.css. */",
|
|
96
|
+
"text {",
|
|
97
|
+
` font-family: "${fonts[0].family}", sans-serif;`,
|
|
98
|
+
"}",
|
|
99
|
+
"",
|
|
100
|
+
];
|
|
101
|
+
if (fonts.length > 1) {
|
|
102
|
+
for (const { family } of fonts) {
|
|
103
|
+
lines.push(`.${cssClassName(family)} {`, ` font-family: "${family}", sans-serif;`, "}", "");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return lines.join("\n");
|
|
83
107
|
}
|
|
84
108
|
|
|
85
109
|
/** Match our generated font block at the top of background.ts (if any). */
|
|
@@ -132,31 +156,14 @@ function loadBaseBackgrounds() {
|
|
|
132
156
|
];
|
|
133
157
|
}
|
|
134
158
|
|
|
135
|
-
function loadBaseStyles() {
|
|
136
|
-
const out = [];
|
|
137
|
-
for (const rel of [
|
|
138
|
-
"templates/blank/common/src/style.css",
|
|
139
|
-
"templates/hello-world/common/src/style.css",
|
|
140
|
-
"templates/basic-activity/common/src/style.css",
|
|
141
|
-
]) {
|
|
142
|
-
out.push(normalizeSource(fs.readFileSync(path.join(packageRoot, rel), "utf8")));
|
|
143
|
-
}
|
|
144
|
-
return out;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
159
|
export function isStockBackground(restSource) {
|
|
148
160
|
const normalized = normalizeSource(restSource);
|
|
149
161
|
return loadBaseBackgrounds().some((base) => base === normalized);
|
|
150
162
|
}
|
|
151
163
|
|
|
152
|
-
export function isStockCss(restSource) {
|
|
153
|
-
const normalized = normalizeSource(restSource);
|
|
154
|
-
return loadBaseStyles().some((base) => base === normalized);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
164
|
/**
|
|
158
165
|
* Copy font files into the JS project (and Android host assets when found).
|
|
159
|
-
* Always safe — does not touch background.ts / style.css.
|
|
166
|
+
* Always safe — does not touch background.ts / style.css / fonts.css.
|
|
160
167
|
*/
|
|
161
168
|
export function copyFontFiles({ projectRoot, fonts, androidDir = null }) {
|
|
162
169
|
const jsFontsDir = path.join(projectRoot, "src", "assets", "fonts");
|
|
@@ -231,36 +238,29 @@ export function applyBackgroundFonts(projectRoot, fonts) {
|
|
|
231
238
|
}
|
|
232
239
|
|
|
233
240
|
/**
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
241
|
+
* Write src/fonts.css from the full font list and make sure src/style.css
|
|
242
|
+
* imports it. style.css is only ever touched to add the one `@import` line
|
|
243
|
+
* (and to drop the `text { font-family }` block older versions put there).
|
|
244
|
+
* Returns { ok, reason?, manualBlock? } — ok=false only when style.css
|
|
245
|
+
* couldn't be found to add the import.
|
|
237
246
|
*/
|
|
238
|
-
export function
|
|
239
|
-
const
|
|
247
|
+
export function applyFontsCss(projectRoot, fonts) {
|
|
248
|
+
const srcDir = path.join(projectRoot, "src");
|
|
249
|
+
fs.mkdirSync(srcDir, { recursive: true });
|
|
250
|
+
fs.writeFileSync(path.join(srcDir, FONTS_CSS_FILE), buildFontsCss(fonts));
|
|
251
|
+
|
|
252
|
+
const cssPath = path.join(srcDir, "style.css");
|
|
240
253
|
if (!fs.existsSync(cssPath)) {
|
|
241
|
-
return {
|
|
242
|
-
ok: false,
|
|
243
|
-
reason: "src/style.css not found",
|
|
244
|
-
manualBlock: buildCssFontBlock(defaultFamily),
|
|
245
|
-
};
|
|
254
|
+
return { ok: false, reason: "src/style.css not found", manualBlock: `${FONTS_CSS_IMPORT}\n` };
|
|
246
255
|
}
|
|
247
256
|
const raw = fs.readFileSync(cssPath, "utf8");
|
|
248
|
-
const {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if (hadExistingFonts && existingFamily != null) {
|
|
257
|
+
const { block, rest } = stripCssFontBlock(raw);
|
|
258
|
+
if (/@import\s+(?:url\()?["']?(?:\.\/)?fonts\.css["']?\)?\s*;/.test(rest)) {
|
|
259
|
+
if (block != null) fs.writeFileSync(cssPath, rest);
|
|
252
260
|
return { ok: true, skipped: true };
|
|
253
261
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
return {
|
|
257
|
-
ok: false,
|
|
258
|
-
reason: "src/style.css no longer matches the stock template",
|
|
259
|
-
manualBlock: buildCssFontBlock(defaultFamily),
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
fs.writeFileSync(cssPath, `${buildCssFontBlock(defaultFamily)}${rest}`);
|
|
262
|
+
// @import must come before any other rule, so it goes at the very top.
|
|
263
|
+
fs.writeFileSync(cssPath, `${FONTS_CSS_IMPORT}\n\n${rest}`);
|
|
264
264
|
return { ok: true };
|
|
265
265
|
}
|
|
266
266
|
|
package/src/index.js
CHANGED
|
@@ -5,14 +5,14 @@ import path from "node:path";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { execSync } from "node:child_process";
|
|
7
7
|
|
|
8
|
-
import { cancel, confirm, intro, isCancel, outro, select, spinner, text } from "@clack/prompts";
|
|
8
|
+
import { cancel, confirm, intro, isCancel, multiselect, outro, select, spinner, text } from "@clack/prompts";
|
|
9
9
|
|
|
10
10
|
import { fetchFontDetail, fetchFontList, listVariants, searchFonts } from "./fontsource.js";
|
|
11
11
|
import {
|
|
12
12
|
applyBackgroundFonts,
|
|
13
|
-
|
|
14
|
-
buildCssFontBlock,
|
|
13
|
+
applyFontsCss,
|
|
15
14
|
buildFontBlock,
|
|
15
|
+
cssClassName,
|
|
16
16
|
copyFontFiles,
|
|
17
17
|
findAndroidDir,
|
|
18
18
|
findProjectRoot,
|
|
@@ -137,6 +137,26 @@ function appClassNameFor(rawName) {
|
|
|
137
137
|
return name === "MainActivity" ? `${name}App` : name;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
/** Android files users edit from the JS project, before syncing them into the
|
|
141
|
+
* sibling Gradle host with `bun run android:prepare`. */
|
|
142
|
+
const ANDROID_DEVELOPMENT_FILES = [
|
|
143
|
+
["AndroidManifest.xml", ["AndroidManifest.xml"]],
|
|
144
|
+
["strings.xml", ["res", "values", "strings.xml"]],
|
|
145
|
+
["styles.xml", ["res", "values", "styles.xml"]],
|
|
146
|
+
["config.xml", ["res", "xml", "config.xml"]],
|
|
147
|
+
["network_security_config.xml", ["res", "xml", "network_security_config.xml"]],
|
|
148
|
+
["file_paths.xml", ["res", "xml", "file_paths.xml"]],
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
function seedAndroidDevelopmentFiles(targetDir, androidDir) {
|
|
152
|
+
const sourceRoot = path.join(androidDir, "app", "src", "main");
|
|
153
|
+
const destinationRoot = path.join(targetDir, "android");
|
|
154
|
+
fs.mkdirSync(destinationRoot, { recursive: true });
|
|
155
|
+
for (const [name, sourceParts] of ANDROID_DEVELOPMENT_FILES) {
|
|
156
|
+
fs.copyFileSync(path.join(sourceRoot, ...sourceParts), path.join(destinationRoot, name));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
140
160
|
function fontFamilyFor(filePath) {
|
|
141
161
|
return path
|
|
142
162
|
.basename(filePath, path.extname(filePath))
|
|
@@ -255,6 +275,12 @@ const WEIGHT_NAMES = {
|
|
|
255
275
|
900: "Black",
|
|
256
276
|
};
|
|
257
277
|
|
|
278
|
+
/** "Bold", "Light Italic" — the part appended to the family name. */
|
|
279
|
+
function variantName(variant) {
|
|
280
|
+
const weightName = WEIGHT_NAMES[variant.weight] ?? String(variant.weight);
|
|
281
|
+
return `${weightName}${variant.style === "italic" ? " Italic" : ""}`;
|
|
282
|
+
}
|
|
283
|
+
|
|
258
284
|
function variantLabel(variant) {
|
|
259
285
|
const weightName = WEIGHT_NAMES[variant.weight] ?? String(variant.weight);
|
|
260
286
|
const style = variant.style === "italic" ? " Italic" : "";
|
|
@@ -264,8 +290,8 @@ function variantLabel(variant) {
|
|
|
264
290
|
/**
|
|
265
291
|
* Interactive search-download flow for `--find-font <term>`: queries
|
|
266
292
|
* Fontsource's catalog locally (see src/fontsource.js — the API itself has
|
|
267
|
-
* no free-text search), lets the user pick a family and one
|
|
268
|
-
* downloads
|
|
293
|
+
* no free-text search), lets the user pick a family and one or more weights/styles,
|
|
294
|
+
* downloads each .ttf to a temp file, and returns them in the same shape
|
|
269
295
|
* `--with-font <file>` expects, so the rest of the pipeline (entirely
|
|
270
296
|
* JS-side — see patchJsProject()) doesn't need to know which path was used.
|
|
271
297
|
*/
|
|
@@ -322,33 +348,45 @@ async function findFontInteractively(term) {
|
|
|
322
348
|
s2.stop(`${variants.length || allVariants.length} variant(s) available.`);
|
|
323
349
|
|
|
324
350
|
const pickFrom = variants.length > 0 ? variants : allVariants;
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
351
|
+
const defaultVariant = pickFrom.find((v) => v.weight === 400 && v.style === "normal") ?? pickFrom[0];
|
|
352
|
+
const chosenVariants = await multiselect({
|
|
353
|
+
message: "Which weights/styles? (space to toggle, enter to confirm)",
|
|
354
|
+
initialValues: [defaultVariant],
|
|
355
|
+
required: true,
|
|
328
356
|
options: pickFrom.map((v) => ({ value: v, label: variantLabel(v) })),
|
|
329
357
|
});
|
|
330
|
-
if (isCancel(
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
try {
|
|
336
|
-
const response = await fetch(chosenVariant.url);
|
|
337
|
-
if (!response.ok) throw new Error(`Download failed: HTTP ${response.status}`);
|
|
338
|
-
bytes = Buffer.from(await response.arrayBuffer());
|
|
339
|
-
} catch (error) {
|
|
340
|
-
s3.stop("Download failed.");
|
|
341
|
-
cancel(error instanceof Error ? error.message : String(error));
|
|
342
|
-
process.exit(1);
|
|
343
|
-
}
|
|
344
|
-
const tempFile = path.join(
|
|
345
|
-
os.tmpdir(),
|
|
346
|
-
`${chosenId}-${chosenVariant.weight}-${chosenVariant.style}-${chosenVariant.subset}.ttf`,
|
|
358
|
+
if (isCancel(chosenVariants)) return null;
|
|
359
|
+
// Regular first (it becomes the family's plain name), then by weight/style.
|
|
360
|
+
chosenVariants.sort(
|
|
361
|
+
(a, b) =>
|
|
362
|
+
(b === defaultVariant) - (a === defaultVariant) || a.weight - b.weight || a.style.localeCompare(b.style),
|
|
347
363
|
);
|
|
348
|
-
fs.writeFileSync(tempFile, bytes);
|
|
349
|
-
s3.stop(`Downloaded ${(bytes.length / 1024).toFixed(1)} kB.`);
|
|
350
364
|
|
|
351
|
-
|
|
365
|
+
const results = [];
|
|
366
|
+
for (const [index, variant] of chosenVariants.entries()) {
|
|
367
|
+
const s3 = spinner();
|
|
368
|
+
s3.start(`Downloading ${detail.family} ${variantLabel(variant)}…`);
|
|
369
|
+
let bytes;
|
|
370
|
+
try {
|
|
371
|
+
const response = await fetch(variant.url);
|
|
372
|
+
if (!response.ok) throw new Error(`Download failed: HTTP ${response.status}`);
|
|
373
|
+
bytes = Buffer.from(await response.arrayBuffer());
|
|
374
|
+
} catch (error) {
|
|
375
|
+
s3.stop("Download failed.");
|
|
376
|
+
cancel(error instanceof Error ? error.message : String(error));
|
|
377
|
+
process.exit(1);
|
|
378
|
+
}
|
|
379
|
+
const tempFile = path.join(
|
|
380
|
+
os.tmpdir(),
|
|
381
|
+
`${chosenId}-${variant.weight}-${variant.style}-${variant.subset}.ttf`,
|
|
382
|
+
);
|
|
383
|
+
fs.writeFileSync(tempFile, bytes);
|
|
384
|
+
s3.stop(`Downloaded ${(bytes.length / 1024).toFixed(1)} kB.`);
|
|
385
|
+
// Each file is registered under its own family name (addFont has no
|
|
386
|
+
// weight/style axis here): "Ubuntu Regular", "Ubuntu Bold Italic"…
|
|
387
|
+
results.push({ sourcePath: tempFile, base: detail.family, family: `${detail.family} ${variantName(variant)}` });
|
|
388
|
+
}
|
|
389
|
+
return results;
|
|
352
390
|
}
|
|
353
391
|
|
|
354
392
|
// ---------------------------------------------------------------------------
|
|
@@ -418,7 +456,7 @@ async function resolveFontsFromFlags(android, { baseDir, requireFonts }) {
|
|
|
418
456
|
|
|
419
457
|
if (android.fontFamily != null && fontCount > 1) {
|
|
420
458
|
cancel(
|
|
421
|
-
"--font-family only makes sense with exactly one font — omit it when bundling more than one, or
|
|
459
|
+
"--font-family only makes sense with exactly one font — omit it when bundling more than one, or use the classes in src/fonts.css.",
|
|
422
460
|
);
|
|
423
461
|
process.exit(1);
|
|
424
462
|
}
|
|
@@ -438,11 +476,20 @@ async function resolveFontsFromFlags(android, { baseDir, requireFonts }) {
|
|
|
438
476
|
for (const term of findFontTerms) {
|
|
439
477
|
const found = await findFontInteractively(term);
|
|
440
478
|
if (found == null) return null;
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
479
|
+
for (const variant of found) {
|
|
480
|
+
fonts.push({
|
|
481
|
+
sourcePath: variant.sourcePath,
|
|
482
|
+
file: path.basename(variant.sourcePath),
|
|
483
|
+
// --font-family replaces the family part; the variant suffix stays
|
|
484
|
+
// unless there is only one variant.
|
|
485
|
+
family:
|
|
486
|
+
android.fontFamily == null
|
|
487
|
+
? variant.family
|
|
488
|
+
: found.length === 1
|
|
489
|
+
? android.fontFamily
|
|
490
|
+
: variant.family.replace(variant.base, android.fontFamily),
|
|
491
|
+
});
|
|
492
|
+
}
|
|
446
493
|
}
|
|
447
494
|
for (const rawPath of fontPaths) {
|
|
448
495
|
const resolved = path.resolve(baseDir, rawPath);
|
|
@@ -505,7 +552,6 @@ async function runAddFont(args) {
|
|
|
505
552
|
const { fonts: existingInBg } = stripFontBlock(
|
|
506
553
|
fs.existsSync(bgPath) ? fs.readFileSync(bgPath, "utf8") : "",
|
|
507
554
|
);
|
|
508
|
-
const hadExistingFonts = existingInBg.length > 0;
|
|
509
555
|
const merged = mergeFontLists(existingInBg, incoming, dedupeFontFiles);
|
|
510
556
|
|
|
511
557
|
const androidDir = findAndroidDir(projectRoot);
|
|
@@ -535,30 +581,19 @@ async function runAddFont(args) {
|
|
|
535
581
|
printManualBlock("src/background.ts", bgResult.manualBlock);
|
|
536
582
|
}
|
|
537
583
|
|
|
538
|
-
const cssResult =
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
console.log("
|
|
545
|
-
} else {
|
|
546
|
-
console.log(` ✖ Could not auto-edit src/style.css (${cssResult.reason}).`);
|
|
547
|
-
console.log(" Paste this at the top of src/style.css (or assign the family on your own classes):");
|
|
584
|
+
const cssResult = applyFontsCss(projectRoot, merged);
|
|
585
|
+
console.log(" ✔ Regenerated src/fonts.css (generated — don't edit it by hand)");
|
|
586
|
+
if (cssResult.ok && !cssResult.skipped) {
|
|
587
|
+
console.log(' ✔ Added @import "fonts.css"; to src/style.css');
|
|
588
|
+
} else if (!cssResult.ok) {
|
|
589
|
+
console.log(` ✖ Could not add the import to src/style.css (${cssResult.reason}).`);
|
|
590
|
+
console.log(" Add this line at the very top of your stylesheet:");
|
|
548
591
|
printManualBlock("src/style.css", cssResult.manualBlock);
|
|
549
592
|
}
|
|
550
593
|
|
|
551
|
-
|
|
552
|
-
// When we had no prior fonts, slice(1) are extras beyond the CSS default.
|
|
553
|
-
// When we had prior fonts, newly added ones after existingInBg.length need a note.
|
|
554
|
-
const newlyAddedFamilies = incoming.map((f) => f.family);
|
|
555
|
-
if (hadExistingFonts && newlyAddedFamilies.length > 0) {
|
|
594
|
+
if (merged.length > 1) {
|
|
556
595
|
console.log(
|
|
557
|
-
` ·
|
|
558
|
-
);
|
|
559
|
-
} else if (!hadExistingFonts && extras.length > 0) {
|
|
560
|
-
console.log(
|
|
561
|
-
` · Extra font${extras.length > 1 ? "s" : ""} ${extras.map((f) => `"${f}"`).join(", ")} — set font-family on your own CSS classes.`,
|
|
596
|
+
` · Use the classes in src/fonts.css: ${merged.map((f) => "." + cssClassName(f.family)).join(", ")}`,
|
|
562
597
|
);
|
|
563
598
|
}
|
|
564
599
|
|
|
@@ -626,10 +661,16 @@ function scaffoldAndroid({ targetDir, rawName, options }) {
|
|
|
626
661
|
}
|
|
627
662
|
}
|
|
628
663
|
|
|
629
|
-
// 5.
|
|
664
|
+
// 5. Give app developers editable Android XML in their project, instead of
|
|
665
|
+
// making them traverse into the sibling Gradle host. android:prepare copies
|
|
666
|
+
// these exact files back into their Android resource locations.
|
|
667
|
+
seedAndroidDevelopmentFiles(targetDir, androidDir);
|
|
668
|
+
|
|
669
|
+
// 6. The scripts that join the two halves, inside the JS project.
|
|
630
670
|
const scriptsDir = path.join(targetDir, "scripts");
|
|
631
671
|
fs.mkdirSync(scriptsDir, { recursive: true });
|
|
632
672
|
fs.copyFileSync(path.join(ANDROID_TEMPLATE_ROOT, "app-scripts", "android.mjs"), path.join(scriptsDir, "android.mjs"));
|
|
673
|
+
fs.copyFileSync(path.join(ANDROID_TEMPLATE_ROOT, "app-scripts", "android-prepare.mjs"), path.join(scriptsDir, "android-prepare.mjs"));
|
|
633
674
|
|
|
634
675
|
const androidRelDir = path.relative(targetDir, androidDir).split(path.sep).join("/");
|
|
635
676
|
replaceInFile(path.join(scriptsDir, "android.mjs"), [
|
|
@@ -639,6 +680,7 @@ function scaffoldAndroid({ targetDir, rawName, options }) {
|
|
|
639
680
|
// A name with quotes would break keytool's -dname=CN=...
|
|
640
681
|
["{{APP_NAME}}", appName.replace(/["\\]/g, "")],
|
|
641
682
|
]);
|
|
683
|
+
replaceInFile(path.join(scriptsDir, "android-prepare.mjs"), [["{{ANDROID_REL_DIR}}", androidRelDir]]);
|
|
642
684
|
|
|
643
685
|
return { androidDir, androidDirName, androidRelDir, androidId, appName, appClass, fonts, sdkDir };
|
|
644
686
|
}
|
|
@@ -647,12 +689,18 @@ function patchJsProject({ targetDir, android }) {
|
|
|
647
689
|
// package.json: the Android scripts.
|
|
648
690
|
const pkgPath = path.join(targetDir, "package.json");
|
|
649
691
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
692
|
+
const buildCommand = pkg.scripts.build;
|
|
650
693
|
pkg.scripts = {
|
|
651
694
|
...pkg.scripts,
|
|
695
|
+
// Keep project-owned Android XML authoritative for every build path:
|
|
696
|
+
// `bun run build`, `npm run build`, and android.mjs's package-manager
|
|
697
|
+
// invocation all execute this command before rspeedy starts.
|
|
698
|
+
build: "node scripts/android-prepare.mjs && " + buildCommand,
|
|
652
699
|
android: "node scripts/android.mjs",
|
|
653
700
|
"android:apk": "node scripts/android.mjs --apk",
|
|
654
701
|
"android:release": "node scripts/android.mjs --release",
|
|
655
702
|
"android:sync": "node scripts/android.mjs --sync-only",
|
|
703
|
+
"android:prepare": "node scripts/android-prepare.mjs",
|
|
656
704
|
"android:keystore": "node scripts/android.mjs --keystore",
|
|
657
705
|
};
|
|
658
706
|
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
@@ -670,9 +718,7 @@ function patchJsProject({ targetDir, android }) {
|
|
|
670
718
|
const existingBg = fs.existsSync(bgPath) ? fs.readFileSync(bgPath, "utf8") : "";
|
|
671
719
|
fs.writeFileSync(bgPath, `${buildFontBlock(android.fonts)}${existingBg}`);
|
|
672
720
|
|
|
673
|
-
|
|
674
|
-
const existingCss = fs.existsSync(cssPath) ? fs.readFileSync(cssPath, "utf8") : "";
|
|
675
|
-
fs.writeFileSync(cssPath, `${buildCssFontBlock(android.fonts[0].family)}${existingCss}`);
|
|
721
|
+
applyFontsCss(targetDir, android.fonts);
|
|
676
722
|
}
|
|
677
723
|
|
|
678
724
|
// README: how the two halves are used together.
|
|
@@ -688,6 +734,7 @@ function patchJsProject({ targetDir, android }) {
|
|
|
688
734
|
"npm run android # build the bundle, copy it to assets, install and launch on the device",
|
|
689
735
|
"npm run android:apk # build the debug APK only",
|
|
690
736
|
"npm run android:sync # bundle -> assets only, no Gradle",
|
|
737
|
+
"bun run android:prepare # android/*.xml -> sibling Android host",
|
|
691
738
|
"",
|
|
692
739
|
"# Signed release APK (generate the keystore once):",
|
|
693
740
|
"KEYSTORE_PASSWORD='...' npm run android:keystore",
|
|
@@ -696,6 +743,8 @@ function patchJsProject({ targetDir, android }) {
|
|
|
696
743
|
"",
|
|
697
744
|
`- Application ID: \`${android.androidId}\``,
|
|
698
745
|
`- Application class: \`${android.appClass}\` · Activity: \`MainActivity\``,
|
|
746
|
+
"- Android XML: edit `android/*.xml`; every `bun run build` synchronizes those files to the sibling host before bundling. Run `bun run android:prepare` when you only need the synchronization.",
|
|
747
|
+
"- Android system Back bridge: the host pre-registers `MithrilLynxNavigationModule`; connect a `mithril-lynx/route` app with `route.listenBackButton({ onCanGoBackChange: (value) => NativeModules.MithrilLynxNavigationModule?.setCanGoBack(value) })`. The Basic Activity template is already connected.",
|
|
699
748
|
...android.fonts.map(
|
|
700
749
|
(f) =>
|
|
701
750
|
`- Font \`${f.family}\` (\`src/assets/fonts/${f.file}\`): \`lynx.addFont()\` — DEV inlines a \`data:\` URI for Lynx Go; PROD uses \`asset:///fonts/${f.file}\` via \`AssetFontFaceLoader\` ([lynx#9431](https://github.com/lynx-family/lynx/issues/9431)).`,
|
|
@@ -1,3 +1,12 @@
|
|
|
1
1
|
/// <reference types="@lynx-js/rspeedy/client" />
|
|
2
2
|
/// <reference types="@lynx-js/types" />
|
|
3
3
|
/// <reference types="@lynx-js/type-element-api" />
|
|
4
|
+
|
|
5
|
+
declare module "@lynx-js/types" {
|
|
6
|
+
interface NativeModules {
|
|
7
|
+
/** Android host bridge used by mithril-lynx/route.listenBackButton(). */
|
|
8
|
+
MithrilLynxNavigationModule?: {
|
|
9
|
+
setCanGoBack(canGoBack: boolean): void;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Copies the Android resources kept beside the JS source into the sibling
|
|
4
|
+
* Gradle host. Edit <project>/android/*.xml, then run:
|
|
5
|
+
*
|
|
6
|
+
* bun run android:prepare
|
|
7
|
+
*/
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
|
|
12
|
+
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
13
|
+
const sourceDir = path.join(projectRoot, "android");
|
|
14
|
+
const androidDir = path.resolve(projectRoot, "{{ANDROID_REL_DIR}}");
|
|
15
|
+
|
|
16
|
+
const files = [
|
|
17
|
+
["AndroidManifest.xml", ["app", "src", "main", "AndroidManifest.xml"]],
|
|
18
|
+
["strings.xml", ["app", "src", "main", "res", "values", "strings.xml"]],
|
|
19
|
+
["styles.xml", ["app", "src", "main", "res", "values", "styles.xml"]],
|
|
20
|
+
["config.xml", ["app", "src", "main", "res", "xml", "config.xml"]],
|
|
21
|
+
["network_security_config.xml", ["app", "src", "main", "res", "xml", "network_security_config.xml"]],
|
|
22
|
+
["file_paths.xml", ["app", "src", "main", "res", "xml", "file_paths.xml"]],
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function fail(message) {
|
|
26
|
+
console.error(`\n ✖ ${message}\n`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!fs.existsSync(path.join(androidDir, "settings.gradle.kts"))) {
|
|
31
|
+
fail(`Can't find the Android host at ${androidDir}.`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
for (const [name, destination] of files) {
|
|
35
|
+
const source = path.join(sourceDir, name);
|
|
36
|
+
if (!fs.existsSync(source)) {
|
|
37
|
+
fail(`Missing ${path.relative(projectRoot, source)}. Restore it before preparing the Android host.`);
|
|
38
|
+
}
|
|
39
|
+
const target = path.join(androidDir, ...destination);
|
|
40
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
41
|
+
fs.copyFileSync(source, target);
|
|
42
|
+
console.log(` → ${path.relative(projectRoot, target)}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
console.log("\n ✔ Android resources prepared.\n");
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
package {{PACKAGE_NAME}}
|
|
2
2
|
|
|
3
3
|
import android.os.Bundle
|
|
4
|
+
import androidx.activity.OnBackPressedCallback
|
|
4
5
|
import androidx.appcompat.app.AppCompatActivity
|
|
5
6
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
|
7
|
+
import com.lynx.react.bridge.JavaOnlyArray
|
|
6
8
|
import com.lynx.tasm.LynxBooleanOption
|
|
7
9
|
import com.lynx.tasm.LynxViewBuilder
|
|
8
10
|
import com.lynx.tasm.ThreadStrategyForRendering
|
|
9
11
|
import com.lynx.xelement.XElementBehaviors
|
|
10
12
|
|
|
11
13
|
class MainActivity : AppCompatActivity() {
|
|
14
|
+
private var backCallback: OnBackPressedCallback? = null
|
|
15
|
+
|
|
12
16
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
13
17
|
// Must run BEFORE super.onCreate() — a Splash Screen API requirement.
|
|
14
18
|
installSplashScreen()
|
|
@@ -20,6 +24,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
20
24
|
// keyboard. Remove this line (and the xelement dependencies) if you
|
|
21
25
|
// don't use them.
|
|
22
26
|
builder.addBehaviors(XElementBehaviors().create())
|
|
27
|
+
// Lets mithril-lynx/route tell Android when its in-memory history can
|
|
28
|
+
// handle Back. See MithrilLynxNavigationModule and the callback below.
|
|
29
|
+
builder.registerModule("MithrilLynxNavigationModule", MithrilLynxNavigationModule::class.java)
|
|
23
30
|
// lynx-family/lynx's own explorer/android registers a
|
|
24
31
|
// GenericResourceFetcher unconditionally (LynxViewShellActivity —
|
|
25
32
|
// "used inside LynxEngine for resource loading capabilities of
|
|
@@ -37,9 +44,29 @@ class MainActivity : AppCompatActivity() {
|
|
|
37
44
|
val lynxView = builder.build(this)
|
|
38
45
|
setContentView(lynxView)
|
|
39
46
|
|
|
47
|
+
// Disabled until route.listenBackButton() reports that JS has an
|
|
48
|
+
// in-app history entry. When disabled, Android's normal Back behavior
|
|
49
|
+
// remains in charge and closes the activity from the first screen.
|
|
50
|
+
val callback = object : OnBackPressedCallback(false) {
|
|
51
|
+
override fun handleOnBackPressed() {
|
|
52
|
+
lynxView.sendGlobalEvent("mithrilLynx:back", JavaOnlyArray())
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
onBackPressedDispatcher.addCallback(this, callback)
|
|
56
|
+
backCallback = callback
|
|
57
|
+
|
|
40
58
|
// The bundle lives in app/src/main/assets/main-thread.bundle, copied
|
|
41
59
|
// there by `npm run android` (scripts/android.mjs) from the JS
|
|
42
60
|
// project's dist/. The name has to match exactly.
|
|
43
61
|
lynxView.renderTemplateUrl("main-thread.bundle", "")
|
|
44
62
|
}
|
|
63
|
+
|
|
64
|
+
fun setCanGoBack(canGoBack: Boolean) {
|
|
65
|
+
backCallback?.isEnabled = canGoBack
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
override fun onDestroy() {
|
|
69
|
+
backCallback = null
|
|
70
|
+
super.onDestroy()
|
|
71
|
+
}
|
|
45
72
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
package {{PACKAGE_NAME}}
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.os.Handler
|
|
5
|
+
import android.os.Looper
|
|
6
|
+
import com.lynx.jsbridge.LynxMethod
|
|
7
|
+
import com.lynx.jsbridge.LynxModule
|
|
8
|
+
import com.lynx.tasm.behavior.LynxContext
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* JS -> Android half of mithril-lynx/route's system Back integration.
|
|
12
|
+
*
|
|
13
|
+
* MainActivity supplies the Android -> JS half with sendGlobalEvent(). Keeping
|
|
14
|
+
* the callback disabled when the route history is empty preserves Android's
|
|
15
|
+
* default behavior on the app's first screen, including predictive Back.
|
|
16
|
+
*/
|
|
17
|
+
class MithrilLynxNavigationModule(context: Context) : LynxModule(context) {
|
|
18
|
+
private val activity = (context as? LynxContext)?.activity as? MainActivity
|
|
19
|
+
|
|
20
|
+
@LynxMethod
|
|
21
|
+
fun setCanGoBack(canGoBack: Boolean) {
|
|
22
|
+
// Lynx invokes native modules from its JS thread; Android's callback
|
|
23
|
+
// belongs to the main thread.
|
|
24
|
+
Handler(Looper.getMainLooper()).post {
|
|
25
|
+
activity?.setCanGoBack(canGoBack)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!--
|
|
3
|
+
Project-owned Android configuration entry point. Android does not consume a
|
|
4
|
+
generic config.xml by itself; use it for XML metadata that a manifest entry
|
|
5
|
+
or a native integration needs, keeping that configuration beside the other
|
|
6
|
+
editable Android files in <project>/android/.
|
|
7
|
+
-->
|
|
8
|
+
<config />
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!--
|
|
3
|
+
Ready for an androidx.core.content.FileProvider declaration in
|
|
4
|
+
AndroidManifest.xml when the app needs to share a file with another app.
|
|
5
|
+
-->
|
|
6
|
+
<paths>
|
|
7
|
+
<files-path name="files" path="." />
|
|
8
|
+
<cache-path name="cache" path="." />
|
|
9
|
+
<external-files-path name="external_files" path="." />
|
|
10
|
+
</paths>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!--
|
|
3
|
+
The generated manifest references this resource. It deliberately matches the
|
|
4
|
+
previous host default (cleartext allowed) so existing local HTTP workflows do
|
|
5
|
+
not change. Restrict this before a production release when possible.
|
|
6
|
+
-->
|
|
7
|
+
<network-security-config>
|
|
8
|
+
<base-config cleartextTrafficPermitted="true" />
|
|
9
|
+
</network-security-config>
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import m from "mithril-runtime";
|
|
2
2
|
|
|
3
3
|
// A reusable app bar: a title, plus a back affordance when `onBack` is
|
|
4
|
-
// given. No m.route.Link involved for
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// route.back() explicitly.
|
|
4
|
+
// given. No m.route.Link involved for this on-screen affordance: it calls
|
|
5
|
+
// route.back() explicitly. The generated Android host's system Back bridge
|
|
6
|
+
// reaches the same history separately through route.listenBackButton().
|
|
8
7
|
export function AppBar(title: string, onBack?: () => void) {
|
|
9
8
|
return m("view", { class: "AppBar" }, [
|
|
10
9
|
onBack != null ? m("text", { class: "AppBar-back", ontap: onBack }, "←") : null,
|
|
@@ -21,6 +21,14 @@ route("/", {
|
|
|
21
21
|
"/detail": DetailHost,
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
+
// The host generated by --android forwards the system Back button through a
|
|
25
|
+
// Lynx global event. The module is optional so this template still runs in
|
|
26
|
+
// LynxExplorer, or when generated without its native Android host.
|
|
27
|
+
route.listenBackButton({
|
|
28
|
+
onCanGoBackChange: (canGoBack) =>
|
|
29
|
+
NativeModules.MithrilLynxNavigationModule?.setCanGoBack(canGoBack),
|
|
30
|
+
});
|
|
31
|
+
|
|
24
32
|
declare const module: {
|
|
25
33
|
hot?: {
|
|
26
34
|
accept(path: string, callback: () => void): void;
|
|
File without changes
|