create-mithril-lynx 0.0.2 → 0.1.1

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.
Files changed (27) hide show
  1. package/README.md +63 -0
  2. package/package.json +2 -2
  3. package/src/index.js +509 -4
  4. package/templates/android/app-scripts/android.mjs +232 -0
  5. package/templates/android/font/AssetFontFaceLoader.kt +39 -0
  6. package/templates/android/host/app/build.gradle.kts +103 -0
  7. package/templates/android/host/app/proguard-rules.pro +51 -0
  8. package/templates/android/host/app/src/main/AndroidManifest.xml +28 -0
  9. package/templates/android/host/app/src/main/java/package-path/App.kt +23 -0
  10. package/templates/android/host/app/src/main/java/package-path/AssetTemplateProvider.kt +27 -0
  11. package/templates/android/host/app/src/main/java/package-path/MainActivity.kt +37 -0
  12. package/templates/android/host/app/src/main/res/drawable/ic_launcher_background.xml +6 -0
  13. package/templates/android/host/app/src/main/res/drawable/ic_launcher_foreground.xml +11 -0
  14. package/templates/android/host/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +5 -0
  15. package/templates/android/host/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +5 -0
  16. package/templates/android/host/app/src/main/res/values/strings.xml +3 -0
  17. package/templates/android/host/app/src/main/res/values/themes.xml +17 -0
  18. package/templates/android/host/build.gradle.kts +4 -0
  19. package/templates/android/host/gitignore +16 -0
  20. package/templates/android/host/gradle/wrapper/gradle-wrapper.jar +0 -0
  21. package/templates/android/host/gradle/wrapper/gradle-wrapper.properties +7 -0
  22. package/templates/android/host/gradle.properties +3 -0
  23. package/templates/android/host/gradlew +251 -0
  24. package/templates/android/host/gradlew.bat +94 -0
  25. package/templates/android/host/keystore.properties.example +15 -0
  26. package/templates/android/host/local.properties +4 -0
  27. package/templates/android/host/settings.gradle.kts +18 -0
@@ -0,0 +1,232 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Bridge between this JS project (the Lynx bundle) and its sibling Android host
4
+ * (`{{ANDROID_DIR}}/`).
5
+ *
6
+ * Automates Part C of ANDROID_APK_GUIDE.md: build the bundle, copy it into
7
+ * `app/src/main/assets/`, then build/install and launch the APK.
8
+ *
9
+ * npm run android build + sync + installDebug + launch on the device
10
+ * npm run android:apk build + sync + assembleDebug (build the APK only)
11
+ * npm run android:release build + sync + assembleRelease (signs if keystore.properties exists)
12
+ * npm run android:sync build + sync into assets only (no Gradle)
13
+ * npm run android:keystore generate release.keystore + keystore.properties (once)
14
+ *
15
+ * Extra flags, passed after `--`:
16
+ * npm run android -- --no-build reuse dist/ as-is (don't rebuild)
17
+ * npm run android -- --no-launch don't launch the app over adb at the end
18
+ * npm run android:apk -- -- --info everything after `--` is forwarded to ./gradlew
19
+ */
20
+ import { spawnSync } from "node:child_process";
21
+ import fs from "node:fs";
22
+ import path from "node:path";
23
+ import { fileURLToPath } from "node:url";
24
+
25
+ const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
26
+
27
+ /** Sibling Android host, resolved when the project was generated. */
28
+ const ANDROID_DIR = path.resolve(projectRoot, "{{ANDROID_REL_DIR}}");
29
+
30
+ const APPLICATION_ID = "{{PACKAGE_NAME}}";
31
+ const ACTIVITY_CLASS = ".MainActivity";
32
+ const BUNDLE_NAME = "main-thread.bundle";
33
+
34
+ const BUNDLE_SRC = path.join(projectRoot, "dist", BUNDLE_NAME);
35
+ const BUNDLE_DEST = path.join(ANDROID_DIR, "app", "src", "main", "assets", BUNDLE_NAME);
36
+
37
+ const USAGE = `
38
+ Usage: npm run android -- [flags] [-- gradle-args]
39
+
40
+ --apk build the debug APK (assembleDebug) instead of installing it
41
+ --release build the release APK (assembleRelease, signed if keystore.properties exists)
42
+ --sync-only only build the bundle and copy it into assets/ (no Gradle)
43
+ --no-build don't rebuild the bundle; use dist/ as it is
44
+ --no-launch don't launch the app over adb at the end
45
+ --keystore generate release.keystore + keystore.properties and exit
46
+ --help print this
47
+ `.trim();
48
+
49
+ const args = process.argv.slice(2);
50
+ const flags = new Set(args.filter((a) => a.startsWith("-")));
51
+
52
+ function afterDoubleDash() {
53
+ const i = args.indexOf("--");
54
+ return i === -1 ? [] : args.slice(i + 1);
55
+ }
56
+
57
+ function fail(message) {
58
+ console.error(`\n ✖ ${message}\n`);
59
+ process.exit(1);
60
+ }
61
+
62
+ function run(command, commandArgs, options = {}) {
63
+ const result = spawnSync(command, commandArgs, {
64
+ cwd: options.cwd ?? projectRoot,
65
+ stdio: options.capture ? "pipe" : "inherit",
66
+ shell: process.platform === "win32",
67
+ encoding: "utf8",
68
+ });
69
+ if (result.error) {
70
+ fail(`Could not run "${command}": ${result.error.message}`);
71
+ }
72
+ if (result.status !== 0) {
73
+ fail(`"${command} ${commandArgs.join(" ")}" exited with code ${result.status}.`);
74
+ }
75
+ return result.stdout ?? "";
76
+ }
77
+
78
+ function detectPackageManager() {
79
+ const userAgent = process.env.npm_config_user_agent ?? "";
80
+ if (userAgent.startsWith("bun")) return "bun";
81
+ if (userAgent.startsWith("pnpm")) return "pnpm";
82
+ if (userAgent.startsWith("yarn")) return "yarn";
83
+ return "npm";
84
+ }
85
+
86
+ function gradlew() {
87
+ return process.platform === "win32" ? "gradlew.bat" : "./gradlew";
88
+ }
89
+
90
+ function requireAndroidDir() {
91
+ if (!fs.existsSync(path.join(ANDROID_DIR, "settings.gradle.kts"))) {
92
+ fail(
93
+ `Can't find the Android host at ${ANDROID_DIR}.\n` +
94
+ ` Generate it with: npm create mithril-lynx@latest <name> --android\n` +
95
+ ` (or pass --android-id/--app-name so it doesn't prompt).`,
96
+ );
97
+ }
98
+ }
99
+
100
+ function syncBundle() {
101
+ if (!fs.existsSync(BUNDLE_SRC)) {
102
+ fail(`${path.relative(projectRoot, BUNDLE_SRC)} doesn't exist. Run "npm run build" first (or drop --no-build).`);
103
+ }
104
+ requireAndroidDir();
105
+ fs.mkdirSync(path.dirname(BUNDLE_DEST), { recursive: true });
106
+ fs.copyFileSync(BUNDLE_SRC, BUNDLE_DEST);
107
+ const kb = (fs.statSync(BUNDLE_DEST).size / 1024).toFixed(1);
108
+ console.log(` → ${path.relative(projectRoot, BUNDLE_DEST)} (${kb} kB)`);
109
+ }
110
+
111
+ function generateKeystore() {
112
+ requireAndroidDir();
113
+ const alias = "release";
114
+ const storeFile = path.join(ANDROID_DIR, "release.keystore");
115
+ const propertiesFile = path.join(ANDROID_DIR, "keystore.properties");
116
+
117
+ if (fs.existsSync(propertiesFile)) {
118
+ fail(`${path.relative(projectRoot, propertiesFile)} already exists. Delete it by hand to regenerate.`);
119
+ }
120
+
121
+ const password = process.env.KEYSTORE_PASSWORD;
122
+ if (!password) {
123
+ fail(
124
+ "The KEYSTORE_PASSWORD environment variable is missing.\n" +
125
+ ` KEYSTORE_PASSWORD='...' npm run android:keystore`,
126
+ );
127
+ }
128
+
129
+ const keytool = spawnSync(
130
+ "keytool",
131
+ [
132
+ "-genkeypair", "-v",
133
+ "-keystore", storeFile,
134
+ "-alias", alias,
135
+ "-keyalg", "RSA",
136
+ "-keysize", "2048",
137
+ "-validity", "10000",
138
+ "-storepass", password,
139
+ "-keypass", password,
140
+ "-dname", "CN={{APP_NAME}}",
141
+ ],
142
+ { stdio: "inherit" },
143
+ );
144
+ if (keytool.error?.code === "ENOENT") {
145
+ fail("Can't find `keytool` on PATH. It ships with JDK 17 — check JAVA_HOME.");
146
+ }
147
+ if (keytool.status !== 0) fail(`keytool exited with code ${keytool.status}.`);
148
+
149
+ fs.writeFileSync(
150
+ propertiesFile,
151
+ [
152
+ "# Generated by scripts/android.mjs — do NOT commit (see the Android host's .gitignore).",
153
+ "storeFile=release.keystore",
154
+ `storePassword=${password}`,
155
+ `keyAlias=${alias}`,
156
+ `keyPassword=${password}`,
157
+ "",
158
+ ].join("\n"),
159
+ );
160
+
161
+ console.log(`\n ✔ ${path.relative(projectRoot, storeFile)}`);
162
+ console.log(` ✔ ${path.relative(projectRoot, propertiesFile)}`);
163
+ console.log("\n Next: npm run android:release\n");
164
+ }
165
+
166
+ function buildBundle() {
167
+ const manager = detectPackageManager();
168
+ console.log(`\n▸ Building the bundle (${manager} run build)…`);
169
+ run(manager, ["run", "build"]);
170
+ }
171
+
172
+ function runGradle(task, extraArgs) {
173
+ console.log(`\n▸ ./gradlew ${task}…`);
174
+ run(gradlew(), [task, ...extraArgs], { cwd: ANDROID_DIR });
175
+ }
176
+
177
+ function launch() {
178
+ console.log("\n▸ Launching on the device…\n");
179
+ run("adb", ["shell", "am", "force-stop", APPLICATION_ID]);
180
+ const output = run("adb", ["shell", "am", "start", "-W", "-n", `${APPLICATION_ID}/${ACTIVITY_CLASS}`], {
181
+ capture: true,
182
+ });
183
+ for (const line of output.split("\n")) {
184
+ if (/TotalTime|WaitTime|LaunchState|Error|Exception/.test(line)) console.log(` ${line.trim()}`);
185
+ }
186
+ console.log("\n Logs: adb logcat | grep -i lynx");
187
+ }
188
+
189
+ function main() {
190
+ if (flags.has("--help") || flags.has("-h")) {
191
+ console.log(USAGE);
192
+ return;
193
+ }
194
+ if (flags.has("--keystore")) {
195
+ generateKeystore();
196
+ return;
197
+ }
198
+
199
+ const syncOnly = flags.has("--sync-only");
200
+
201
+ if (!flags.has("--no-build")) buildBundle();
202
+ syncBundle();
203
+
204
+ if (syncOnly) {
205
+ console.log("\n ✔ Assets synced.\n");
206
+ return;
207
+ }
208
+
209
+ const extraGradleArgs = afterDoubleDash();
210
+ if (flags.has("--release")) {
211
+ runGradle("assembleRelease", extraGradleArgs);
212
+ const apkDir = path.join(ANDROID_DIR, "app", "build", "outputs", "apk", "release");
213
+ for (const apk of fs.existsSync(apkDir) ? fs.readdirSync(apkDir).filter((f) => f.endsWith(".apk")) : []) {
214
+ console.log(`\n ✔ APK: ${path.relative(projectRoot, path.join(apkDir, apk))}`);
215
+ }
216
+ } else if (flags.has("--apk") || process.env.CI) {
217
+ runGradle("assembleDebug", extraGradleArgs);
218
+ console.log(
219
+ `\n ✔ APK: ${path.relative(projectRoot, path.join(ANDROID_DIR, "app", "build", "outputs", "apk", "debug", "app-debug.apk"))}`,
220
+ );
221
+ } else {
222
+ runGradle("installDebug", extraGradleArgs);
223
+ }
224
+
225
+ if (!flags.has("--no-launch") && !flags.has("--apk") && !flags.has("--release")) {
226
+ launch();
227
+ } else {
228
+ console.log("");
229
+ }
230
+ }
231
+
232
+ main();
@@ -0,0 +1,39 @@
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
+ }
@@ -0,0 +1,103 @@
1
+ import java.io.FileInputStream
2
+ import java.util.Properties
3
+
4
+ plugins {
5
+ id("com.android.application")
6
+ id("org.jetbrains.kotlin.android")
7
+ }
8
+
9
+ // Optional release signing, driven by keystore.properties (gitignored).
10
+ // Without that file, `./gradlew assembleRelease` simply produces an unsigned
11
+ // APK instead of failing. To generate it:
12
+ // npm run android:keystore
13
+ val keystorePropertiesFile = rootProject.file("keystore.properties")
14
+ val keystoreProperties = Properties().apply {
15
+ if (keystorePropertiesFile.exists()) {
16
+ FileInputStream(keystorePropertiesFile).use { load(it) }
17
+ }
18
+ }
19
+
20
+ android {
21
+ namespace = "{{PACKAGE_NAME}}"
22
+ compileSdk = 34
23
+
24
+ defaultConfig {
25
+ applicationId = "{{PACKAGE_NAME}}"
26
+ minSdk = 24
27
+ targetSdk = 34
28
+ versionCode = 1
29
+ versionName = "1.0"
30
+ }
31
+
32
+ signingConfigs {
33
+ if (keystorePropertiesFile.exists()) {
34
+ create("release") {
35
+ // storeFile resolves against the Android project root, so
36
+ // "release.keystore" means <android-dir>/release.keystore.
37
+ storeFile = rootProject.file(keystoreProperties.getProperty("storeFile"))
38
+ storePassword = keystoreProperties.getProperty("storePassword")
39
+ keyAlias = keystoreProperties.getProperty("keyAlias")
40
+ keyPassword = keystoreProperties.getProperty("keyPassword")
41
+ }
42
+ }
43
+ }
44
+
45
+ buildTypes {
46
+ debug {
47
+ isMinifyEnabled = false
48
+ }
49
+ release {
50
+ isMinifyEnabled = true
51
+ proguardFiles(
52
+ getDefaultProguardFile("proguard-android-optimize.txt"),
53
+ "proguard-rules.pro",
54
+ )
55
+ if (keystorePropertiesFile.exists()) {
56
+ signingConfig = signingConfigs.getByName("release")
57
+ }
58
+ }
59
+ }
60
+
61
+ compileOptions {
62
+ sourceCompatibility = JavaVersion.VERSION_17
63
+ targetCompatibility = JavaVersion.VERSION_17
64
+ }
65
+
66
+ kotlinOptions {
67
+ jvmTarget = "17"
68
+ }
69
+ }
70
+
71
+ dependencies {
72
+ // The core artifact — LynxView, LynxViewBuilder, the layout engine, etc.
73
+ implementation("org.lynxsdk.lynx:lynx:4.1.0")
74
+
75
+ // OPT-IN: add these only if you use those elements, but they cost little
76
+ // and without them the failure is silent (the element mounts and never
77
+ // responds), so the scaffold ships them.
78
+
79
+ // <input>/<textarea> — without this those elements take up zero size and
80
+ // never open the keyboard. XElementBehaviors is registered in MainActivity.kt.
81
+ implementation("org.lynxsdk.lynx:xelement:4.1.0")
82
+ implementation("org.lynxsdk.lynx:xelement-input:4.1.0")
83
+
84
+ // <overlay> — without this the element mounts without error but is never
85
+ // visible (mithril-lynx-ui's Dialog/Sheet/Popover need it).
86
+ implementation("org.lynxsdk.lynx:xelement-overlay:4.1.0")
87
+
88
+ // Without a registered ILynxLogService, console.log() and JS errors from
89
+ // the bundle are dropped silently — not even logcat shows them. Remove it
90
+ // (along with its registration in the Application class) in a production
91
+ // build.
92
+ implementation("org.lynxsdk.lynx:lynx-service-log:4.1.0")
93
+
94
+ // <refresh> (pull-to-refresh, inside the xelement artifact) internally
95
+ // depends on SmartRefreshLayout, whose touch-dispatch code references
96
+ // ViewPager2 even though you don't use it — without this, every touch on a
97
+ // <refresh> throws NoClassDefFoundError (caught by the engine, but the
98
+ // gesture then never works).
99
+ implementation("androidx.viewpager2:viewpager2:1.1.0")
100
+
101
+ implementation("androidx.appcompat:appcompat:1.7.0")
102
+ implementation("androidx.core:core-splashscreen:1.0.1")
103
+ }
@@ -0,0 +1,51 @@
1
+ # ProGuard/R8 rules for the release build.
2
+
3
+ # ---------------------------------------------------------------------------
4
+ # 1. OPTIONAL Lynx SDK dependencies that are not on the classpath.
5
+ #
6
+ # The `xelement` artifact (and part of the core) references library classes the
7
+ # SDK does NOT declare as transitive dependencies:
8
+ # - Fresco (com.facebook.**) -> SvgDefaultResourceManager's remote SVG path
9
+ # - Gson (com.google.gson.**) -> LynxEnv.GetNativeEnvDebugDescription()
10
+ # - Markdown (com.lynx.markdown.**) -> LynxUIMarkdownShadowNode
11
+ #
12
+ # Without these -dontwarn rules, R8 aborts with "Missing classes detected while
13
+ # running R8" and `./gradlew assembleRelease` fails outright. Verified with
14
+ # lynx:4.1.0 + AGP 8.5.2. They are optional paths a normal mithril-lynx app
15
+ # never uses, so ignoring them is correct (adding the real libraries would only
16
+ # bloat the APK for nothing).
17
+ -dontwarn com.facebook.**
18
+ -dontwarn com.google.gson.**
19
+ -dontwarn com.lynx.markdown.**
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # 1b. Methods Lynx's NATIVE code calls by exact name + signature.
23
+ #
24
+ # The `lynx-base` AAR calls
25
+ # `com.lynx.base.log.LynxLog.log(int, String, String, int, long, int, int)`
26
+ # from C++ via JNI GetStaticMethodID. That class is NOT annotated with
27
+ # @CalledByNative (unlike most of the bridge), so the AAR's consumer rules
28
+ # don't cover it: R8 renames it and startup dies with
29
+ #
30
+ # java.lang.NoSuchMethodError: no static method
31
+ # "Lcom/lynx/base/log/LynxLog;.log(ILjava/lang/String;Ljava/lang/String;IJII)V"
32
+ # JNI DETECTED ERROR IN APPLICATION: mid == null
33
+ # Fatal signal 6 (SIGABRT) <- on the LynxTraceInit thread, before drawing anything
34
+ #
35
+ # Verified on a device (minified release, lynx:4.1.0 + AGP 8.5.2): without this
36
+ # rule the release APK crashes on startup; with it, it starts like the debug one.
37
+ -keep class com.lynx.base.log.LynxLog { *; }
38
+
39
+ # ---------------------------------------------------------------------------
40
+ # 2. Your own classes reached by reflection.
41
+ #
42
+ # The Lynx SDK ships its own "consumer" rules inside the AAR. What it cannot
43
+ # know about is your classes, so anything Lynx reaches by name has to survive
44
+ # minification:
45
+ #
46
+ # - modules registered with builder.registerModule("Name", Class::class.java)
47
+ # - your own @LynxMethod / behaviors
48
+ #
49
+ # Example:
50
+ #
51
+ # -keep class com.example.myapp.MyNativeModule { *; }
@@ -0,0 +1,28 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
3
+
4
+ <uses-permission android:name="android.permission.INTERNET" />
5
+
6
+ <application
7
+ android:name=".{{APP_CLASS}}"
8
+ android:allowBackup="true"
9
+ android:label="@string/app_name"
10
+ android:icon="@mipmap/ic_launcher"
11
+ android:roundIcon="@mipmap/ic_launcher_round"
12
+ android:hardwareAccelerated="true"
13
+ android:usesCleartextTraffic="true"
14
+ android:theme="@style/AppTheme">
15
+
16
+ <activity
17
+ android:name=".MainActivity"
18
+ android:theme="@style/Theme.App.Starting"
19
+ android:exported="true">
20
+ <intent-filter>
21
+ <action android:name="android.intent.action.MAIN" />
22
+ <category android:name="android.intent.category.LAUNCHER" />
23
+ </intent-filter>
24
+ </activity>
25
+
26
+ </application>
27
+
28
+ </manifest>
@@ -0,0 +1,23 @@
1
+ package {{PACKAGE_NAME}}
2
+
3
+ import android.app.Application
4
+ import com.lynx.service.log.LynxLogService
5
+ import com.lynx.tasm.LynxEnv
6
+ // {{FONT_LOADER_IMPORT}}
7
+ import com.lynx.tasm.service.LynxServiceCenter
8
+
9
+ class {{APP_CLASS}} : Application() {
10
+ override fun onCreate() {
11
+ super.onCreate()
12
+
13
+ // Without a registered ILynxLogService, console.log() and JS errors
14
+ // from the bundle are dropped silently — not even logcat shows them.
15
+ // Drop this (and the lynx-service-log dependency) in production if you
16
+ // don't want it.
17
+ LynxServiceCenter.inst().registerService(LynxLogService)
18
+ LynxLogService.switchLogToSystem(true)
19
+
20
+ // {{FONT_LOADER_REGISTRATION}}
21
+ LynxEnv.inst().init(this, null, null, null)
22
+ }
23
+ }
@@ -0,0 +1,27 @@
1
+ package {{PACKAGE_NAME}}
2
+
3
+ import android.content.Context
4
+ import com.lynx.tasm.provider.AbsTemplateProvider
5
+ import java.io.IOException
6
+
7
+ /**
8
+ * Reads the Lynx bundle from assets on a separate thread.
9
+ *
10
+ * Measured on real hardware: reading the same asset SYNCHRONOUSLY in
11
+ * MainActivity.onCreate() (blocking Android's UI thread — distinct from Lynx's
12
+ * internal main/background thread split, but just as harmful to the first
13
+ * frame) cost 2.4-2.8s of cold start under `adb shell am start -W`; with this
14
+ * async provider, ~300ms. This is not optional.
15
+ */
16
+ class AssetTemplateProvider(private val context: Context) : AbsTemplateProvider() {
17
+ override fun loadTemplate(url: String, callback: Callback) {
18
+ Thread {
19
+ try {
20
+ val bytes = context.assets.open(url).use { it.readBytes() }
21
+ callback.onSuccess(bytes)
22
+ } catch (e: IOException) {
23
+ callback.onFailed(e.toString())
24
+ }
25
+ }.start()
26
+ }
27
+ }
@@ -0,0 +1,37 @@
1
+ package {{PACKAGE_NAME}}
2
+
3
+ import android.os.Bundle
4
+ import androidx.appcompat.app.AppCompatActivity
5
+ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
6
+ import com.lynx.tasm.LynxViewBuilder
7
+ import com.lynx.tasm.ThreadStrategyForRendering
8
+ // {{FONT_IMPORT}}
9
+ import com.lynx.xelement.XElementBehaviors
10
+
11
+ class MainActivity : AppCompatActivity() {
12
+ override fun onCreate(savedInstanceState: Bundle?) {
13
+ // Must run BEFORE super.onCreate() — a Splash Screen API requirement.
14
+ installSplashScreen()
15
+ super.onCreate(savedInstanceState)
16
+
17
+ val builder = LynxViewBuilder()
18
+ // <input>/<textarea> are opt-in "xelement" components, not part of the
19
+ // core artifact: without this they measure 0 and never open the
20
+ // keyboard. Remove this line (and the xelement dependencies) if you
21
+ // don't use them.
22
+ builder.addBehaviors(XElementBehaviors().create())
23
+ builder.setThreadStrategyForRendering(ThreadStrategyForRendering.ALL_ON_UI)
24
+ // Reads the bundle from assets on a separate thread — see
25
+ // AssetTemplateProvider's comment: reading it synchronously here cost
26
+ // 2.4-2.8s of cold start measured on a real device, vs ~300ms with this.
27
+ builder.setTemplateProvider(AssetTemplateProvider(this))
28
+ val lynxView = builder.build(this)
29
+ setContentView(lynxView)
30
+
31
+ // {{FONT_PREFETCH}}
32
+ // The bundle lives in app/src/main/assets/main-thread.bundle, copied
33
+ // there by `npm run android` (scripts/android.mjs) from the JS
34
+ // project's dist/. The name has to match exactly.
35
+ lynxView.renderTemplateUrl("main-thread.bundle", "")
36
+ }
37
+ }
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <vector xmlns:android="http://schemas.android.com/apk/res/android"
3
+ android:width="108dp" android:height="108dp"
4
+ android:viewportWidth="108" android:viewportHeight="108">
5
+ <path android:pathData="M0,0h108v108h-108z" android:fillColor="#0F1115" />
6
+ </vector>
@@ -0,0 +1,11 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <vector xmlns:android="http://schemas.android.com/apk/res/android"
3
+ android:width="108dp" android:height="108dp"
4
+ android:viewportWidth="108" android:viewportHeight="108">
5
+ <path
6
+ android:pathData="M30,54 L50,74 L78,34"
7
+ android:strokeColor="#6EE7B7"
8
+ android:strokeWidth="6"
9
+ android:strokeLineCap="round"
10
+ android:strokeLineJoin="round" />
11
+ </vector>
@@ -0,0 +1,5 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
3
+ <background android:drawable="@drawable/ic_launcher_background" />
4
+ <foreground android:drawable="@drawable/ic_launcher_foreground" />
5
+ </adaptive-icon>
@@ -0,0 +1,5 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
3
+ <background android:drawable="@drawable/ic_launcher_background" />
4
+ <foreground android:drawable="@drawable/ic_launcher_foreground" />
5
+ </adaptive-icon>
@@ -0,0 +1,3 @@
1
+ <resources>
2
+ <string name="app_name">{{APP_NAME}}</string>
3
+ </resources>
@@ -0,0 +1,17 @@
1
+ <resources>
2
+
3
+ <!-- The app's normal theme, handed off to once the splash screen ends.
4
+ No ActionBar: Lynx draws the whole app. -->
5
+ <style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar" />
6
+
7
+ <!-- Shown by the system from process start until installSplashScreen()
8
+ gets its default signal — which is this Activity's first drawn frame,
9
+ i.e. exactly the real cold-start window (~700-800ms), not an arbitrary
10
+ fixed delay. -->
11
+ <style name="Theme.App.Starting" parent="Theme.SplashScreen">
12
+ <item name="windowSplashScreenBackground">#0F1115</item>
13
+ <item name="windowSplashScreenAnimatedIcon">@drawable/ic_launcher_foreground</item>
14
+ <item name="postSplashScreenTheme">@style/AppTheme</item>
15
+ </style>
16
+
17
+ </resources>
@@ -0,0 +1,4 @@
1
+ plugins {
2
+ id("com.android.application") version "8.5.2" apply false
3
+ id("org.jetbrains.kotlin.android") version "1.9.24" apply false
4
+ }
@@ -0,0 +1,16 @@
1
+ # Gradle / Android
2
+ .gradle/
3
+ build/
4
+ app/build/
5
+ .cxx/
6
+ local.properties
7
+ *.iml
8
+ .idea/
9
+
10
+ # Release signing — never committed (see keystore.properties.example).
11
+ keystore.properties
12
+ *.keystore
13
+ *.jks
14
+
15
+ # Regenerated by `npm run android` from the sibling JS project.
16
+ app/src/main/assets/main-thread.bundle
@@ -0,0 +1,7 @@
1
+ distributionBase=GRADLE_USER_HOME
2
+ distributionPath=wrapper/dists
3
+ distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip
4
+ networkTimeout=10000
5
+ validateDistributionUrl=true
6
+ zipStoreBase=GRADLE_USER_HOME
7
+ zipStorePath=wrapper/dists
@@ -0,0 +1,3 @@
1
+ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
2
+ android.useAndroidX=true
3
+ kotlin.code.style=official