create-mithril-lynx 2.0.1 → 2.2.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 +44 -6
- package/package.json +1 -1
- package/src/fontsource.js +102 -0
- package/src/index.js +298 -114
- package/templates/android/host/app/src/main/java/package-path/App.kt +0 -2
- package/templates/android/host/app/src/main/java/package-path/MainActivity.kt +0 -2
- package/templates/android/font/AssetFontFaceLoader.kt +0 -39
package/README.md
CHANGED
|
@@ -49,11 +49,49 @@ Every flag, in one table — `npx create-mithril-lynx --help` prints the same li
|
|
|
49
49
|
| `--android` / `--target android` / `--target=android` / `target=android` | Also scaffold the sibling `<name>-android/` Gradle project. |
|
|
50
50
|
| `--android-id <id>` | `applicationId` / `namespace` (default `com.example.<name>`). |
|
|
51
51
|
| `--app-name <name>` | Launcher label (default: the project name). |
|
|
52
|
-
| `--with-font <file.ttf>` |
|
|
53
|
-
| `--font
|
|
52
|
+
| `--with-font <file.ttf>` | Bundle the font into `src/assets/fonts/` and register it with `lynx.addFont()`. No native code: the import is inlined as a `data:` URI (`dataUriLimit: Infinity` in `lynx.config.ts`), which resolves the same way on every host — the real APK, LynxExplorer, or Lynx Go. Confirmed on device: no cold-start cost either (~730ms with the font vs. ~770ms without, on the same device). 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 a weight/style, download that one `.ttf`, and bundle it 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
|
+
| `--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. |
|
|
54
55
|
|
|
55
56
|
Anything not listed — in particular the four Android flags — requires `--android`
|
|
56
|
-
(or one of its aliases); `--with-font`
|
|
57
|
+
(or one of its aliases); `--with-font`/`--find-font` imply it on their own.
|
|
58
|
+
|
|
59
|
+
### More than one font
|
|
60
|
+
|
|
61
|
+
Both flags take a comma-separated list — useful for a body font plus a
|
|
62
|
+
monospace one for code, for example:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npx create-mithril-lynx my-app --blank --android --find-font "Inter,JetBrains Mono"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Each font gets its own `import`/`lynx.addFont()` call in `src/background.ts`.
|
|
69
|
+
Only the **first** one gets the automatic `text { font-family: ...; }` rule
|
|
70
|
+
in `src/style.css` (there's no way for the CLI to guess which elements
|
|
71
|
+
should use which font past that) — the rest are registered and ready to use,
|
|
72
|
+
but you assign them to your own classes by hand, e.g.:
|
|
73
|
+
|
|
74
|
+
```css
|
|
75
|
+
.code {
|
|
76
|
+
font-family: "JetBrains Mono", monospace;
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### `--find-font`, in more detail
|
|
81
|
+
|
|
82
|
+
Fontsource's own API (`api.fontsource.org`) has no free-text search — only
|
|
83
|
+
exact `id`/`family` filters — so `--find-font` downloads the whole font list
|
|
84
|
+
once (~2100 fonts, ~540KB as of 2026-09), caches it for 24h in the OS temp
|
|
85
|
+
directory, and filters client-side on `family`/`id` substrings. Picking a
|
|
86
|
+
family fetches that font's own detail (its real weight/style/subset `.ttf`
|
|
87
|
+
URLs), lets you pick one variant (defaulting to 400/normal if available),
|
|
88
|
+
downloads it to a temp file, and hands it to the same code path
|
|
89
|
+
`--with-font <file>` uses — there's no separate font-loading mechanism to
|
|
90
|
+
maintain.
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npx create-mithril-lynx my-app --blank --android --find-font Inter
|
|
94
|
+
```
|
|
57
95
|
|
|
58
96
|
### What the generated Android host gives you
|
|
59
97
|
|
|
@@ -61,7 +99,7 @@ The sibling `<name>-android/` project is a complete, no-Android-Studio Gradle CL
|
|
|
61
99
|
|
|
62
100
|
- the Gradle wrapper (`gradlew`, `gradlew.bat`, `gradle-wrapper.jar`), so nothing needs to be installed besides a JDK and the Android SDK;
|
|
63
101
|
- `local.properties` with `sdk.dir` auto-detected from `ANDROID_HOME`/`ANDROID_SDK_ROOT` (with a written warning if it can't be found);
|
|
64
|
-
- the Kotlin host: the `Application`, `MainActivity` (async `AssetTemplateProvider`, splash screen)
|
|
102
|
+
- the Kotlin host: the `Application`, `MainActivity` (async `AssetTemplateProvider`, splash screen) — no font-specific code needed, `--with-font` is entirely JS-side;
|
|
65
103
|
- resource files that compile as-is (theme, splash theme, adaptive launcher icon);
|
|
66
104
|
- an opt-in release signing setup driven by a gitignored `keystore.properties`.
|
|
67
105
|
|
|
@@ -90,6 +128,7 @@ This part of the tool is framework-agnostic — it just wraps whatever bundle `r
|
|
|
90
128
|
```
|
|
91
129
|
create-mithril-lynx/
|
|
92
130
|
src/index.js the CLI itself
|
|
131
|
+
src/fontsource.js --find-font: local search over Fontsource's catalog
|
|
93
132
|
templates/
|
|
94
133
|
_shared/
|
|
95
134
|
common/ gitignore, project README — shared by every template
|
|
@@ -107,13 +146,12 @@ create-mithril-lynx/
|
|
|
107
146
|
ts/src/{app-bar.ts, background.ts, screens/{home,detail}.ts}
|
|
108
147
|
android/ only used with --android
|
|
109
148
|
host/ the Gradle project -> <name>-android/
|
|
110
|
-
font/AssetFontFaceLoader.kt copied only when --with-font is used
|
|
111
149
|
app-scripts/android.mjs the bridge -> <name>/scripts/android.mjs
|
|
112
150
|
```
|
|
113
151
|
|
|
114
152
|
`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`.
|
|
115
153
|
|
|
116
|
-
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)
|
|
154
|
+
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`, the font itself is copied into the *JS* project (`src/assets/fonts/`) and wired up with `lynx.addFont()` in `src/background.ts` plus a `text { font-family: ... }` rule in `src/style.css` — the Android host needs no font-specific code at all (see the `--with-font` row above).
|
|
117
155
|
|
|
118
156
|
**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.
|
|
119
157
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// src/fontsource.js
|
|
2
|
+
//
|
|
3
|
+
// Local search over Fontsource's font catalog (https://fontsource.org). The
|
|
4
|
+
// public API (https://api.fontsource.org/v1/fonts) has no free-text search —
|
|
5
|
+
// only exact `id`/`family` filters (confirmed against its own docs) — so
|
|
6
|
+
// this fetches the whole list once (~2100 fonts, ~540KB as of 2026-09) and
|
|
7
|
+
// filters client-side. Cached to disk (24h TTL) so repeated searches in one
|
|
8
|
+
// session, or across nearby `npx`/`bunx` invocations, don't re-download it
|
|
9
|
+
// every time.
|
|
10
|
+
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import os from "node:os";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
|
|
15
|
+
const FONTS_LIST_URL = "https://api.fontsource.org/v1/fonts";
|
|
16
|
+
const FONT_DETAIL_URL = (id) => `https://api.fontsource.org/v1/fonts/${id}`;
|
|
17
|
+
const CACHE_PATH = path.join(os.tmpdir(), "create-mithril-lynx-fontsource-cache.json");
|
|
18
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @returns {Promise<Array<{id: string, family: string, category: string,
|
|
22
|
+
* variable: boolean, license: string, weights: number[], styles: string[]}>>}
|
|
23
|
+
*/
|
|
24
|
+
export async function fetchFontList({ forceRefresh = false } = {}) {
|
|
25
|
+
if (!forceRefresh) {
|
|
26
|
+
try {
|
|
27
|
+
const stat = fs.statSync(CACHE_PATH);
|
|
28
|
+
if (Date.now() - stat.mtimeMs < CACHE_TTL_MS) {
|
|
29
|
+
return JSON.parse(fs.readFileSync(CACHE_PATH, "utf8"));
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
// No cache yet, or unreadable — fetch fresh below.
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const response = await fetch(FONTS_LIST_URL);
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
throw new Error(`Fontsource API returned ${response.status} fetching the font list.`);
|
|
39
|
+
}
|
|
40
|
+
const list = await response.json();
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
fs.writeFileSync(CACHE_PATH, JSON.stringify(list));
|
|
44
|
+
} catch {
|
|
45
|
+
// Best-effort — a failed cache write just means the next call re-fetches.
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return list;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Case-insensitive substring match on `family` (primarily) and `id`.
|
|
53
|
+
* Exact matches sort first, then alphabetically by family.
|
|
54
|
+
*/
|
|
55
|
+
export function searchFonts(list, query) {
|
|
56
|
+
const q = query.trim().toLowerCase();
|
|
57
|
+
if (q === "") return [];
|
|
58
|
+
|
|
59
|
+
const matches = list.filter(
|
|
60
|
+
(font) => font.family.toLowerCase().includes(q) || font.id.toLowerCase().includes(q),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
return matches.sort((a, b) => {
|
|
64
|
+
const aExact = a.family.toLowerCase() === q ? 0 : 1;
|
|
65
|
+
const bExact = b.family.toLowerCase() === q ? 0 : 1;
|
|
66
|
+
if (aExact !== bExact) return aExact - bExact;
|
|
67
|
+
return a.family.localeCompare(b.family);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Full detail for one font — includes `variants[weight][style][subset]`,
|
|
73
|
+
* each holding `{ url: { woff2, woff, ttf } }`. Confirmed against Fontsource's
|
|
74
|
+
* own docs (https://fontsource.org/docs/api/font-id) and a real fetch.
|
|
75
|
+
*/
|
|
76
|
+
export async function fetchFontDetail(id) {
|
|
77
|
+
const response = await fetch(FONT_DETAIL_URL(id));
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
throw new Error(`Fontsource API returned ${response.status} fetching "${id}".`);
|
|
80
|
+
}
|
|
81
|
+
return response.json();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Flattens a font's `variants` object into a flat, pickable list. */
|
|
85
|
+
export function listVariants(detail) {
|
|
86
|
+
const out = [];
|
|
87
|
+
for (const [weight, byStyle] of Object.entries(detail.variants ?? {})) {
|
|
88
|
+
for (const [style, bySubset] of Object.entries(byStyle)) {
|
|
89
|
+
for (const [subset, files] of Object.entries(bySubset)) {
|
|
90
|
+
if (files?.url?.ttf) {
|
|
91
|
+
out.push({ weight: Number(weight), style, subset, url: files.url.ttf });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Latin first (most common default), then by weight, then style.
|
|
97
|
+
return out.sort((a, b) => {
|
|
98
|
+
if (a.subset !== b.subset) return a.subset === "latin" ? -1 : b.subset === "latin" ? 1 : a.subset.localeCompare(b.subset);
|
|
99
|
+
if (a.weight !== b.weight) return a.weight - b.weight;
|
|
100
|
+
return a.style.localeCompare(b.style);
|
|
101
|
+
});
|
|
102
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { execSync } from "node:child_process";
|
|
6
7
|
|
|
7
|
-
import { cancel, confirm, intro, isCancel, outro, select, text } from "@clack/prompts";
|
|
8
|
+
import { cancel, confirm, intro, isCancel, outro, select, spinner, text } from "@clack/prompts";
|
|
9
|
+
|
|
10
|
+
import { fetchFontDetail, fetchFontList, listVariants, searchFonts } from "./fontsource.js";
|
|
8
11
|
|
|
9
12
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
10
13
|
const packageRoot = path.join(scriptDir, "..");
|
|
@@ -132,6 +135,43 @@ function fontFamilyFor(filePath) {
|
|
|
132
135
|
.join(" ");
|
|
133
136
|
}
|
|
134
137
|
|
|
138
|
+
/** `--with-font`/`--find-font` accept a comma-separated list, for bundling
|
|
139
|
+
* more than one font (e.g. a body font and a monospace one for code). */
|
|
140
|
+
function splitList(value) {
|
|
141
|
+
return value
|
|
142
|
+
.split(",")
|
|
143
|
+
.map((v) => v.trim())
|
|
144
|
+
.filter(Boolean);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Renames `file` on any font past the first with the same basename, so
|
|
148
|
+
* e.g. two different "Inter" downloads (different weights) don't collide
|
|
149
|
+
* once copied into src/assets/fonts/. */
|
|
150
|
+
function dedupeFontFiles(fonts) {
|
|
151
|
+
const seen = new Set();
|
|
152
|
+
for (const font of fonts) {
|
|
153
|
+
let file = font.file;
|
|
154
|
+
let n = 2;
|
|
155
|
+
while (seen.has(file)) {
|
|
156
|
+
const ext = path.extname(font.file);
|
|
157
|
+
file = `${path.basename(font.file, ext)}-${n}${ext}`;
|
|
158
|
+
n += 1;
|
|
159
|
+
}
|
|
160
|
+
seen.add(file);
|
|
161
|
+
font.file = file;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** A valid JS identifier (camelCase, "Font" suffix) for a font-family string. */
|
|
166
|
+
function jsIdentifierFor(family) {
|
|
167
|
+
const words = family.split(/[^a-zA-Z0-9]+/).filter(Boolean);
|
|
168
|
+
const camel = words
|
|
169
|
+
.map((word, i) => (i === 0 ? word[0].toLowerCase() + word.slice(1) : word[0].toUpperCase() + word.slice(1)))
|
|
170
|
+
.join("");
|
|
171
|
+
const safe = /^[0-9]/.test(camel) ? `f${camel}` : camel;
|
|
172
|
+
return `${safe || "font"}Font`;
|
|
173
|
+
}
|
|
174
|
+
|
|
135
175
|
function xmlEscape(value) {
|
|
136
176
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
137
177
|
}
|
|
@@ -173,12 +213,20 @@ function parseAndroidArgs(args) {
|
|
|
173
213
|
appName: readOption(args, "app-name"),
|
|
174
214
|
fontPath: readOption(args, "with-font"),
|
|
175
215
|
fontFamily: readOption(args, "font-family"),
|
|
216
|
+
findFontTerm: readOption(args, "find-font"),
|
|
176
217
|
};
|
|
177
218
|
}
|
|
178
219
|
|
|
179
220
|
// Options that consume the following argument, so that value is never mistaken
|
|
180
221
|
// for the project name (e.g. `--with-font ./UbuntuMono.ttf`).
|
|
181
|
-
const OPTIONS_WITH_VALUE = new Set([
|
|
222
|
+
const OPTIONS_WITH_VALUE = new Set([
|
|
223
|
+
"--target",
|
|
224
|
+
"--android-id",
|
|
225
|
+
"--app-name",
|
|
226
|
+
"--with-font",
|
|
227
|
+
"--font-family",
|
|
228
|
+
"--find-font",
|
|
229
|
+
]);
|
|
182
230
|
|
|
183
231
|
function findPositional(args) {
|
|
184
232
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -188,19 +236,118 @@ function findPositional(args) {
|
|
|
188
236
|
continue;
|
|
189
237
|
}
|
|
190
238
|
if (arg === "target=android") continue;
|
|
191
|
-
if (/^(?:target|android-id|app-name|with-font|font-family)=/.test(arg)) continue;
|
|
239
|
+
if (/^(?:target|android-id|app-name|with-font|font-family|find-font)=/.test(arg)) continue;
|
|
192
240
|
return arg;
|
|
193
241
|
}
|
|
194
242
|
return undefined;
|
|
195
243
|
}
|
|
196
244
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
245
|
+
const WEIGHT_NAMES = {
|
|
246
|
+
100: "Thin",
|
|
247
|
+
200: "Extra Light",
|
|
248
|
+
300: "Light",
|
|
249
|
+
400: "Regular",
|
|
250
|
+
500: "Medium",
|
|
251
|
+
600: "Semi Bold",
|
|
252
|
+
700: "Bold",
|
|
253
|
+
800: "Extra Bold",
|
|
254
|
+
900: "Black",
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
function variantLabel(variant) {
|
|
258
|
+
const weightName = WEIGHT_NAMES[variant.weight] ?? String(variant.weight);
|
|
259
|
+
const style = variant.style === "italic" ? " Italic" : "";
|
|
260
|
+
return `${weightName}${style} (${variant.weight} ${variant.style})`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Interactive search-download flow for `--find-font <term>`: queries
|
|
265
|
+
* Fontsource's catalog locally (see src/fontsource.js — the API itself has
|
|
266
|
+
* no free-text search), lets the user pick a family and one weight/style,
|
|
267
|
+
* downloads that .ttf to a temp file, and returns it in the same shape
|
|
268
|
+
* `--with-font <file>` expects, so the rest of the pipeline (entirely
|
|
269
|
+
* JS-side — see patchJsProject()) doesn't need to know which path was used.
|
|
270
|
+
*/
|
|
271
|
+
async function findFontInteractively(term) {
|
|
272
|
+
// Unlike the other prompts in this file, this one isn't skippable by
|
|
273
|
+
// supplying enough flags up front — picking a family and a variant out
|
|
274
|
+
// of a search result is inherently interactive. Gate on the terminal
|
|
275
|
+
// itself, not on whether name/template were also given.
|
|
276
|
+
if (!process.stdin.isTTY) {
|
|
277
|
+
cancel("--find-font needs an interactive terminal to pick a family and variant — use --with-font <file.ttf> in scripts/CI.");
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const s = spinner();
|
|
282
|
+
s.start(`Searching Fontsource for "${term}"…`);
|
|
283
|
+
let list;
|
|
284
|
+
try {
|
|
285
|
+
list = await fetchFontList();
|
|
286
|
+
} catch (error) {
|
|
287
|
+
s.stop("Search failed.");
|
|
288
|
+
cancel(error instanceof Error ? error.message : String(error));
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
const matches = searchFonts(list, term);
|
|
292
|
+
s.stop(`${matches.length} match(es) for "${term}".`);
|
|
293
|
+
|
|
294
|
+
if (matches.length === 0) {
|
|
295
|
+
cancel(`No Fontsource font matches "${term}". Try a different search, or use --with-font <file.ttf> for a font you already have.`);
|
|
296
|
+
process.exit(1);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const chosenId = await select({
|
|
300
|
+
message: "Which font?",
|
|
301
|
+
options: matches.map((f) => ({
|
|
302
|
+
value: f.id,
|
|
303
|
+
label: f.family,
|
|
304
|
+
hint: `${f.category}${f.variable ? ", variable" : ""} · ${f.license}`,
|
|
305
|
+
})),
|
|
306
|
+
});
|
|
307
|
+
if (isCancel(chosenId)) return null;
|
|
308
|
+
|
|
309
|
+
const s2 = spinner();
|
|
310
|
+
s2.start("Fetching variants…");
|
|
311
|
+
let detail;
|
|
312
|
+
try {
|
|
313
|
+
detail = await fetchFontDetail(chosenId);
|
|
314
|
+
} catch (error) {
|
|
315
|
+
s2.stop("Failed.");
|
|
316
|
+
cancel(error instanceof Error ? error.message : String(error));
|
|
317
|
+
process.exit(1);
|
|
318
|
+
}
|
|
319
|
+
const allVariants = listVariants(detail);
|
|
320
|
+
const variants = allVariants.filter((v) => v.subset === "latin");
|
|
321
|
+
s2.stop(`${variants.length || allVariants.length} variant(s) available.`);
|
|
322
|
+
|
|
323
|
+
const pickFrom = variants.length > 0 ? variants : allVariants;
|
|
324
|
+
const chosenVariant = await select({
|
|
325
|
+
message: "Which weight/style?",
|
|
326
|
+
initialValue: pickFrom.find((v) => v.weight === 400 && v.style === "normal") ?? pickFrom[0],
|
|
327
|
+
options: pickFrom.map((v) => ({ value: v, label: variantLabel(v) })),
|
|
328
|
+
});
|
|
329
|
+
if (isCancel(chosenVariant)) return null;
|
|
330
|
+
|
|
331
|
+
const s3 = spinner();
|
|
332
|
+
s3.start(`Downloading ${detail.family} ${variantLabel(chosenVariant)}…`);
|
|
333
|
+
let bytes;
|
|
334
|
+
try {
|
|
335
|
+
const response = await fetch(chosenVariant.url);
|
|
336
|
+
if (!response.ok) throw new Error(`Download failed: HTTP ${response.status}`);
|
|
337
|
+
bytes = Buffer.from(await response.arrayBuffer());
|
|
338
|
+
} catch (error) {
|
|
339
|
+
s3.stop("Download failed.");
|
|
340
|
+
cancel(error instanceof Error ? error.message : String(error));
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
const tempFile = path.join(
|
|
344
|
+
os.tmpdir(),
|
|
345
|
+
`${chosenId}-${chosenVariant.weight}-${chosenVariant.style}-${chosenVariant.subset}.ttf`,
|
|
346
|
+
);
|
|
347
|
+
fs.writeFileSync(tempFile, bytes);
|
|
348
|
+
s3.stop(`Downloaded ${(bytes.length / 1024).toFixed(1)} kB.`);
|
|
349
|
+
|
|
350
|
+
return { sourcePath: tempFile, family: detail.family };
|
|
204
351
|
}
|
|
205
352
|
|
|
206
353
|
// ---------------------------------------------------------------------------
|
|
@@ -246,38 +393,64 @@ async function resolveAndroidOptions(android, { rawName, projectName, canPrompt
|
|
|
246
393
|
}
|
|
247
394
|
appName = appName ?? path.basename(rawName);
|
|
248
395
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
396
|
+
if (android.fontPath != null && android.findFontTerm != null) {
|
|
397
|
+
cancel("--with-font and --find-font are mutually exclusive — pick one.");
|
|
398
|
+
process.exit(1);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Both flags accept a comma-separated list, so more than one font (e.g.
|
|
402
|
+
// a body font and a monospace one for code) can be bundled in one run.
|
|
403
|
+
const fontPaths = android.fontPath != null ? splitList(android.fontPath) : [];
|
|
404
|
+
const findFontTerms = android.findFontTerm != null ? splitList(android.findFontTerm) : [];
|
|
405
|
+
const fontCount = fontPaths.length + findFontTerms.length;
|
|
406
|
+
|
|
407
|
+
if (android.fontFamily != null && fontCount > 1) {
|
|
408
|
+
cancel("--font-family only makes sense with exactly one font — omit it when bundling more than one, or edit src/style.css/background.ts by hand afterwards.");
|
|
409
|
+
process.exit(1);
|
|
410
|
+
}
|
|
411
|
+
if (android.fontFamily != null && fontCount === 0) {
|
|
412
|
+
cancel("--font-family only makes sense together with --with-font or --find-font.");
|
|
413
|
+
process.exit(1);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const fonts = [];
|
|
417
|
+
for (const term of findFontTerms) {
|
|
418
|
+
const found = await findFontInteractively(term);
|
|
419
|
+
if (found == null) return null;
|
|
420
|
+
fonts.push({
|
|
421
|
+
sourcePath: found.sourcePath,
|
|
422
|
+
file: path.basename(found.sourcePath),
|
|
423
|
+
family: android.fontFamily ?? found.family,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
for (const rawPath of fontPaths) {
|
|
427
|
+
const resolved = path.resolve(cwd, rawPath);
|
|
252
428
|
if (!fs.existsSync(resolved)) {
|
|
253
|
-
cancel(`Font file not found: ${
|
|
429
|
+
cancel(`Font file not found: ${rawPath}`);
|
|
254
430
|
process.exit(1);
|
|
255
431
|
}
|
|
256
432
|
if (![".ttf", ".otf", ".ttc"].includes(path.extname(resolved).toLowerCase())) {
|
|
257
|
-
cancel(`"${
|
|
433
|
+
cancel(`"${rawPath}" does not look like a font (.ttf/.otf/.ttc).`);
|
|
258
434
|
process.exit(1);
|
|
259
435
|
}
|
|
260
|
-
|
|
436
|
+
fonts.push({
|
|
261
437
|
sourcePath: resolved,
|
|
262
438
|
file: path.basename(resolved),
|
|
263
439
|
family: android.fontFamily ?? fontFamilyFor(resolved),
|
|
264
|
-
};
|
|
265
|
-
} else if (android.fontFamily != null) {
|
|
266
|
-
cancel("--font-family only makes sense together with --with-font.");
|
|
267
|
-
process.exit(1);
|
|
440
|
+
});
|
|
268
441
|
}
|
|
442
|
+
dedupeFontFiles(fonts);
|
|
269
443
|
|
|
270
|
-
return { androidId, appName,
|
|
444
|
+
return { androidId, appName, fonts };
|
|
271
445
|
}
|
|
272
446
|
|
|
273
447
|
function scaffoldAndroid({ targetDir, rawName, options }) {
|
|
274
|
-
const { androidId, appName,
|
|
448
|
+
const { androidId, appName, fonts } = options;
|
|
275
449
|
|
|
276
450
|
const androidDirName = `${path.basename(rawName.replace(/^@[^/]+\//, ""))}-android`;
|
|
277
451
|
const androidDir = path.join(path.dirname(targetDir), androidDirName);
|
|
278
452
|
const appClass = appClassNameFor(rawName);
|
|
279
453
|
const packagePath = androidId.split(".").join(path.sep);
|
|
280
|
-
const packageDir = path.join(androidDir, "app", "src", "main", "java", packagePath);
|
|
281
454
|
|
|
282
455
|
// 1. Copy the Gradle skeleton and the Kotlin app. `package-path` expands to
|
|
283
456
|
// the real package path, and App.kt is renamed after its class.
|
|
@@ -297,19 +470,15 @@ function scaffoldAndroid({ targetDir, rawName, options }) {
|
|
|
297
470
|
// best-effort
|
|
298
471
|
}
|
|
299
472
|
|
|
300
|
-
// 2.
|
|
301
|
-
//
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
path.join(packageDir, "AssetFontFaceLoader.kt"),
|
|
310
|
-
);
|
|
311
|
-
}
|
|
312
|
-
|
|
473
|
+
// 2. Fonts need no native code at all: lynx.config.ts's dataUriLimit:
|
|
474
|
+
// Infinity (see templates/_shared/ts/lynx.config.ts) already inlines
|
|
475
|
+
// any imported .ttf as a data: URI, and lynx.addFont() resolves a
|
|
476
|
+
// data: URI on every host with no registration — confirmed on device
|
|
477
|
+
// (a stock generated Android host, with no custom Loader/fetcher of
|
|
478
|
+
// any kind, renders the font correctly) and with no cold-start cost
|
|
479
|
+
// (three-run A/B on the same device: 724-745ms with the font vs.
|
|
480
|
+
// 715-826ms without — indistinguishable). See patchJsProject() for
|
|
481
|
+
// where the font actually gets wired up (entirely JS-side).
|
|
313
482
|
const sdkDir = findAndroidSdk();
|
|
314
483
|
|
|
315
484
|
// 3. Text substitutions across the whole Android host (skipping the
|
|
@@ -327,54 +496,7 @@ function scaffoldAndroid({ targetDir, rawName, options }) {
|
|
|
327
496
|
],
|
|
328
497
|
]);
|
|
329
498
|
|
|
330
|
-
// 4. The
|
|
331
|
-
const sourceUri = font != null ? `asset:///fonts/${font.file}` : undefined;
|
|
332
|
-
const fontLoaderImport = font != null ? "import com.lynx.tasm.loader.LynxFontFaceLoader\n" : "";
|
|
333
|
-
const fontLoaderRegistration =
|
|
334
|
-
font != null
|
|
335
|
-
? `${indent(
|
|
336
|
-
[
|
|
337
|
-
"// Must run BEFORE LynxEnv.inst().init(): this is what makes",
|
|
338
|
-
'// "asset:///" resolvable, both for prefetchFont() and for the',
|
|
339
|
-
"// real @font-face resolution during the first layout.",
|
|
340
|
-
"LynxFontFaceLoader.setLoader(AssetFontFaceLoader)",
|
|
341
|
-
].join("\n"),
|
|
342
|
-
8,
|
|
343
|
-
)}\n\n`
|
|
344
|
-
: "";
|
|
345
|
-
const fontImport = font != null ? "import com.lynx.tasm.fontface.FontFaceManager\n" : "";
|
|
346
|
-
const fontPrefetch =
|
|
347
|
-
font != null
|
|
348
|
-
? `${indent(
|
|
349
|
-
[
|
|
350
|
-
"// Warms the Typeface on Lynx's own IO thread pool, before",
|
|
351
|
-
"// renderTemplateUrl() gives the bundle's CSS a chance to resolve",
|
|
352
|
-
"// @font-face during the first layout. FontFaceManager caches by the",
|
|
353
|
-
"// exact src string, so this URI has to be identical to the",
|
|
354
|
-
'// url("...") of the @font-face in src/style.css.',
|
|
355
|
-
"FontFaceManager.getInstance().prefetchFont(",
|
|
356
|
-
" lynxView.lynxContext,",
|
|
357
|
-
` "${sourceUri}",`,
|
|
358
|
-
" null,",
|
|
359
|
-
" object : FontFaceManager.FontFacePrefetchListener {",
|
|
360
|
-
" override fun onComplete(code: Int, msg: String) {}",
|
|
361
|
-
" },",
|
|
362
|
-
")",
|
|
363
|
-
].join("\n"),
|
|
364
|
-
8,
|
|
365
|
-
)}\n\n`
|
|
366
|
-
: "";
|
|
367
|
-
|
|
368
|
-
for (const kt of walkFiles(packageDir)) {
|
|
369
|
-
replaceInFile(kt, [
|
|
370
|
-
["// {{FONT_LOADER_IMPORT}}\n", fontLoaderImport],
|
|
371
|
-
[" // {{FONT_LOADER_REGISTRATION}}\n", fontLoaderRegistration],
|
|
372
|
-
["// {{FONT_IMPORT}}\n", fontImport],
|
|
373
|
-
[" // {{FONT_PREFETCH}}\n", fontPrefetch],
|
|
374
|
-
]);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// 5. The script that joins the two halves, inside the JS project.
|
|
499
|
+
// 4. The script that joins the two halves, inside the JS project.
|
|
378
500
|
const scriptsDir = path.join(targetDir, "scripts");
|
|
379
501
|
fs.mkdirSync(scriptsDir, { recursive: true });
|
|
380
502
|
fs.copyFileSync(path.join(ANDROID_TEMPLATE_ROOT, "app-scripts", "android.mjs"), path.join(scriptsDir, "android.mjs"));
|
|
@@ -388,7 +510,7 @@ function scaffoldAndroid({ targetDir, rawName, options }) {
|
|
|
388
510
|
["{{APP_NAME}}", appName.replace(/["\\]/g, "")],
|
|
389
511
|
]);
|
|
390
512
|
|
|
391
|
-
return { androidDir, androidDirName, androidRelDir, androidId, appName, appClass,
|
|
513
|
+
return { androidDir, androidDirName, androidRelDir, androidId, appName, appClass, fonts, sdkDir };
|
|
392
514
|
}
|
|
393
515
|
|
|
394
516
|
function patchJsProject({ targetDir, android }) {
|
|
@@ -405,23 +527,76 @@ function patchJsProject({ targetDir, android }) {
|
|
|
405
527
|
};
|
|
406
528
|
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
407
529
|
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
530
|
+
// The font: entirely JS-side, no native code, no @font-face.
|
|
531
|
+
//
|
|
532
|
+
// - The .ttf goes into the JS project itself (src/assets/fonts/), so the
|
|
533
|
+
// bundler can import it. templates/_shared/ts/lynx.config.ts already
|
|
534
|
+
// sets `dataUriLimit: Infinity`, which inlines that import as a
|
|
535
|
+
// `data:font/ttf;base64,...` URI directly in the bundle — no separate
|
|
536
|
+
// file, no asset-path puzzle to solve for a packaged native host.
|
|
537
|
+
// - lynx.addFont() (not @font-face) registers it, called from
|
|
538
|
+
// background.ts. A data: URI resolves the same way on every host —
|
|
539
|
+
// confirmed on a real device with a stock generated Android host (no
|
|
540
|
+
// AssetFontFaceLoader, no custom resource fetcher) and with LynxExplorer/
|
|
541
|
+
// Lynx Go, with no cold-start cost (a three-run A/B on the same device
|
|
542
|
+
// measured 724-745ms with the font vs. 715-826ms without —
|
|
543
|
+
// indistinguishable; the old +1-2s regression was specific to
|
|
544
|
+
// @font-face's forced synchronous resolution, which doesn't apply here).
|
|
545
|
+
if (android.fonts.length > 0) {
|
|
546
|
+
const fontsDir = path.join(targetDir, "src", "assets", "fonts");
|
|
547
|
+
fs.mkdirSync(fontsDir, { recursive: true });
|
|
548
|
+
|
|
549
|
+
const usedVarNames = new Set();
|
|
550
|
+
const importLines = [];
|
|
551
|
+
const addFontLines = [];
|
|
552
|
+
for (const { family, sourcePath, file } of android.fonts) {
|
|
553
|
+
fs.copyFileSync(sourcePath, path.join(fontsDir, file));
|
|
554
|
+
|
|
555
|
+
let varName = jsIdentifierFor(family);
|
|
556
|
+
while (usedVarNames.has(varName)) varName = `${varName}2`;
|
|
557
|
+
usedVarNames.add(varName);
|
|
558
|
+
|
|
559
|
+
importLines.push(`import ${varName} from "./assets/fonts/${file}";`);
|
|
560
|
+
addFontLines.push(
|
|
561
|
+
`lynx.addFont({ "font-family": "${family}", src: \`url("\${${varName}}")\` }, () => {});`,
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Only the first font gets an automatic CSS rule (the app-wide
|
|
566
|
+
// default, matching --with-font's single-font behavior exactly) —
|
|
567
|
+
// with more than one, there's no way to guess which elements should
|
|
568
|
+
// use which, so the rest are just registered via lynx.addFont() and
|
|
569
|
+
// left for you to assign in your own CSS classes.
|
|
570
|
+
const defaultFamily = android.fonts[0].family;
|
|
571
|
+
const extraFamilies = android.fonts.slice(1).map((f) => f.family);
|
|
572
|
+
|
|
573
|
+
const bgPath = path.join(targetDir, "src", "background.ts");
|
|
574
|
+
const fontBlock = [
|
|
575
|
+
android.fonts.length === 1
|
|
576
|
+
? `// Font: "${defaultFamily}", bundled from src/assets/fonts/${android.fonts[0].file}. lynx.addFont()`
|
|
577
|
+
: `// Fonts: ${android.fonts.map((f) => `"${f.family}"`).join(", ")}, bundled from src/assets/fonts/. lynx.addFont()`,
|
|
578
|
+
"// registers each directly — dataUriLimit: Infinity (lynx.config.ts) inlines",
|
|
579
|
+
"// every import as a data: URI, which resolves the same way on every host,",
|
|
580
|
+
"// no native code needed.",
|
|
581
|
+
...(extraFamilies.length > 0
|
|
582
|
+
? [
|
|
583
|
+
`// Only "${defaultFamily}" got the automatic text { font-family: ... }`,
|
|
584
|
+
`// rule below — assign ${extraFamilies.map((f) => `"${f}"`).join(" / ")} to your own`,
|
|
585
|
+
"// classes in src/style.css, e.g. `.code { font-family: \"" + extraFamilies[0] + "\"; }`.",
|
|
586
|
+
]
|
|
587
|
+
: []),
|
|
588
|
+
...importLines,
|
|
589
|
+
...addFontLines,
|
|
590
|
+
"",
|
|
421
591
|
"",
|
|
422
592
|
].join("\n");
|
|
423
|
-
const
|
|
424
|
-
fs.writeFileSync(
|
|
593
|
+
const existingBg = fs.existsSync(bgPath) ? fs.readFileSync(bgPath, "utf8") : "";
|
|
594
|
+
fs.writeFileSync(bgPath, `${fontBlock}${existingBg}`);
|
|
595
|
+
|
|
596
|
+
const cssPath = path.join(targetDir, "src", "style.css");
|
|
597
|
+
const cssBlock = [`text {`, ` font-family: "${defaultFamily}", sans-serif;`, `}`, ""].join("\n");
|
|
598
|
+
const existingCss = fs.existsSync(cssPath) ? fs.readFileSync(cssPath, "utf8") : "";
|
|
599
|
+
fs.writeFileSync(cssPath, `${cssBlock}\n${existingCss}`);
|
|
425
600
|
}
|
|
426
601
|
|
|
427
602
|
// README: how the two halves are used together.
|
|
@@ -445,9 +620,10 @@ function patchJsProject({ targetDir, android }) {
|
|
|
445
620
|
"",
|
|
446
621
|
`- Application ID: \`${android.androidId}\``,
|
|
447
622
|
`- Application class: \`${android.appClass}\` · Activity: \`MainActivity\``,
|
|
448
|
-
...
|
|
449
|
-
|
|
450
|
-
|
|
623
|
+
...android.fonts.map(
|
|
624
|
+
(f) =>
|
|
625
|
+
`- Font \`${f.family}\` loaded via \`lynx.addFont()\` from \`src/assets/fonts/${f.file}\` (bundled as a data: URI — no native code involved).`,
|
|
626
|
+
),
|
|
451
627
|
"",
|
|
452
628
|
].join("\n");
|
|
453
629
|
fs.appendFileSync(readmePath, section);
|
|
@@ -471,10 +647,18 @@ Android host:
|
|
|
471
647
|
scaffold the sibling Gradle project <name>-android/
|
|
472
648
|
--android-id <id> applicationId / namespace (default com.example.<name>)
|
|
473
649
|
--app-name <name> launcher label (default: the project name)
|
|
474
|
-
--with-font <file.ttf>
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
650
|
+
--with-font <file.ttf> bundle the font into the JS project and register it
|
|
651
|
+
with lynx.addFont() (works on every host, no native
|
|
652
|
+
code — see README). Comma-separate for more than
|
|
653
|
+
one, e.g. --with-font a.ttf,b.ttf
|
|
654
|
+
--find-font <term> search Fontsource (fontsource.org) for a font,
|
|
655
|
+
pick a family and a weight/style interactively,
|
|
656
|
+
and bundle it the same way as --with-font.
|
|
657
|
+
Comma-separate terms for more than one — quote
|
|
658
|
+
the whole thing if any term has a space, e.g.
|
|
659
|
+
--find-font "Inter,JetBrains Mono"
|
|
660
|
+
--font-family <name> override the family name derived from the file
|
|
661
|
+
name (only valid with exactly one font)
|
|
478
662
|
|
|
479
663
|
Other:
|
|
480
664
|
--no-install don't install dependencies
|
|
@@ -500,9 +684,9 @@ async function main() {
|
|
|
500
684
|
const nonInteractive = positional != null && templateFlag != null;
|
|
501
685
|
const android = parseAndroidArgs(args);
|
|
502
686
|
|
|
503
|
-
// --with-font/--font-family only make sense with an Android
|
|
504
|
-
// rather than ignoring them silently.
|
|
505
|
-
if (android.fontPath != null || android.fontFamily != null) {
|
|
687
|
+
// --with-font/--find-font/--font-family only make sense with an Android
|
|
688
|
+
// host: imply it rather than ignoring them silently.
|
|
689
|
+
if (android.fontPath != null || android.findFontTerm != null || android.fontFamily != null) {
|
|
506
690
|
android.requested = true;
|
|
507
691
|
}
|
|
508
692
|
|
|
@@ -613,7 +797,7 @@ async function main() {
|
|
|
613
797
|
];
|
|
614
798
|
|
|
615
799
|
if (androidResult != null) {
|
|
616
|
-
const { androidDirName, sdkDir,
|
|
800
|
+
const { androidDirName, sdkDir, fonts } = androidResult;
|
|
617
801
|
const notes = [
|
|
618
802
|
`Android host generated in ${androidDirName}/ (Application ID ${androidOptions.androidId}).`,
|
|
619
803
|
"",
|
|
@@ -625,9 +809,9 @@ async function main() {
|
|
|
625
809
|
? "⚠ Android SDK not found: export ANDROID_HOME and edit " +
|
|
626
810
|
`${androidDirName}/local.properties (sdk.dir=...).`
|
|
627
811
|
: `Android SDK found at ${sdkDir}.`,
|
|
628
|
-
|
|
629
|
-
? `Font "${
|
|
630
|
-
: "No custom font — pass --with-font <file.ttf>
|
|
812
|
+
fonts.length > 0
|
|
813
|
+
? `Font${fonts.length > 1 ? "s" : ""} ${fonts.map((f) => `"${f.family}"`).join(", ")} bundled and registered via lynx.addFont() — works on every host.`
|
|
814
|
+
: "No custom font — pass --with-font <file.ttf> (or --find-font <term>) to bundle one.",
|
|
631
815
|
];
|
|
632
816
|
outro(`Done! Next steps:\n\n ${steps.join("\n ")}\n\n${notes.join("\n")}`);
|
|
633
817
|
return;
|
|
@@ -3,7 +3,6 @@ package {{PACKAGE_NAME}}
|
|
|
3
3
|
import android.app.Application
|
|
4
4
|
import com.lynx.service.log.LynxLogService
|
|
5
5
|
import com.lynx.tasm.LynxEnv
|
|
6
|
-
// {{FONT_LOADER_IMPORT}}
|
|
7
6
|
import com.lynx.tasm.service.LynxServiceCenter
|
|
8
7
|
|
|
9
8
|
class {{APP_CLASS}} : Application() {
|
|
@@ -17,7 +16,6 @@ class {{APP_CLASS}} : Application() {
|
|
|
17
16
|
LynxServiceCenter.inst().registerService(LynxLogService)
|
|
18
17
|
LynxLogService.switchLogToSystem(true)
|
|
19
18
|
|
|
20
|
-
// {{FONT_LOADER_REGISTRATION}}
|
|
21
19
|
LynxEnv.inst().init(this, null, null, null)
|
|
22
20
|
}
|
|
23
21
|
}
|
|
@@ -5,7 +5,6 @@ import androidx.appcompat.app.AppCompatActivity
|
|
|
5
5
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
|
6
6
|
import com.lynx.tasm.LynxViewBuilder
|
|
7
7
|
import com.lynx.tasm.ThreadStrategyForRendering
|
|
8
|
-
// {{FONT_IMPORT}}
|
|
9
8
|
import com.lynx.xelement.XElementBehaviors
|
|
10
9
|
|
|
11
10
|
class MainActivity : AppCompatActivity() {
|
|
@@ -28,7 +27,6 @@ class MainActivity : AppCompatActivity() {
|
|
|
28
27
|
val lynxView = builder.build(this)
|
|
29
28
|
setContentView(lynxView)
|
|
30
29
|
|
|
31
|
-
// {{FONT_PREFETCH}}
|
|
32
30
|
// The bundle lives in app/src/main/assets/main-thread.bundle, copied
|
|
33
31
|
// there by `npm run android` (scripts/android.mjs) from the JS
|
|
34
32
|
// project's dist/. The name has to match exactly.
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
package {{PACKAGE_NAME}}
|
|
2
|
-
|
|
3
|
-
import android.graphics.Typeface
|
|
4
|
-
import com.lynx.tasm.behavior.LynxContext
|
|
5
|
-
import com.lynx.tasm.fontface.FontFace
|
|
6
|
-
import com.lynx.tasm.loader.LynxFontFaceLoader
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Resolves `asset:///` in `@font-face`, both for the prefetch and for the real
|
|
10
|
-
* lookup.
|
|
11
|
-
*
|
|
12
|
-
* Lynx's default loader (LynxFontFaceLoader$1, decompiled from
|
|
13
|
-
* lynx-4.1.0.aar) is a no-op that never resolves `asset:///`: in
|
|
14
|
-
* FontFaceManager that scheme is only handled inline inside loadTypeface()
|
|
15
|
-
* when a FONT-type LynxResourceProvider is registered, and the non-http/
|
|
16
|
-
* non-data: branch of prefetchFont() (prefetchFontWithLoader) goes through this
|
|
17
|
-
* Loader only, with no fallback of its own. Without registering this,
|
|
18
|
-
* `asset:///` resolves no way at all.
|
|
19
|
-
*
|
|
20
|
-
* Trade-off to keep in mind: FontFaceManager/LynxFontFaceLoader are public
|
|
21
|
-
* classes (not @RestrictTo) but are not documented for this specific use —
|
|
22
|
-
* they could change without notice in a major Lynx SDK release.
|
|
23
|
-
*/
|
|
24
|
-
object AssetFontFaceLoader : LynxFontFaceLoader.Loader() {
|
|
25
|
-
private const val ASSET_PREFIX = "asset:///"
|
|
26
|
-
|
|
27
|
-
override fun onLoadFontFace(
|
|
28
|
-
context: LynxContext,
|
|
29
|
-
type: FontFace.TYPE,
|
|
30
|
-
src: String,
|
|
31
|
-
): Typeface? {
|
|
32
|
-
if (!src.startsWith(ASSET_PREFIX)) return null
|
|
33
|
-
return try {
|
|
34
|
-
Typeface.createFromAsset(context.context.assets, src.removePrefix(ASSET_PREFIX))
|
|
35
|
-
} catch (e: Exception) {
|
|
36
|
-
null
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|