@blotch/angular-lynx-native 22.2.2 → 22.2.3
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 +14 -9
- package/dist/ios/frameworks.d.ts +9 -2
- package/dist/ios/frameworks.d.ts.map +1 -1
- package/dist/ios/frameworks.js +13 -3
- package/package.json +1 -1
- package/templates/android/README.md +3 -1
- package/templates/android/app/build.gradle.kts +18 -0
- package/templates/android/app/src/main/AndroidManifest.xml +1 -0
- package/templates/android/app/src/main/java/dev/angularlynx/shell/MainActivity.kt +61 -1
- package/templates/android/app/src/main/res/xml/network_security_config.xml +15 -0
- package/templates/ios/App/AppDelegate.swift +1 -0
- package/templates/ios/App/LynxViewController.swift +43 -3
- package/templates/ios/App/XElementRegistry.swift +51 -0
- package/templates/ios/App.xcodeproj/project.pbxproj +18 -0
package/README.md
CHANGED
|
@@ -14,20 +14,25 @@ An ejected project (`ios/` or `android/` with a `.angular-lynx-ejected` marker,
|
|
|
14
14
|
|
|
15
15
|
## The shells
|
|
16
16
|
|
|
17
|
-
Both are one full-screen `LynxView` running `main.lynx.bundle`; Angular Router does all navigation inside it. Bundles and assets load from inside the app, so RSpeedy's default asset prefix (`/static/...`) works with no configuration.
|
|
17
|
+
Both are one full-screen `LynxView` running `main.lynx.bundle`; Angular Router does all navigation inside it. Bundles and assets load from inside the app, so RSpeedy's default asset prefix (`/static/...`) works with no configuration. The `LynxView` sits inside the safe area, so CSS `env(safe-area-inset-*)` is 0 in it.
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
|
24
|
-
|
|
|
19
|
+
Both send the same global props, which the runtime's services read: `platform`, `theme` (again whenever the phone switches between light and dark), `appLocale` (the phone's language as a BCP 47 tag), `isNotchScreen`, and the safe-area insets `safeAreaTop`, `safeAreaBottom`, `safeAreaLeft` and `safeAreaRight`. Lynx Explorer sends `theme` and `isNotchScreen` too, but no `appLocale`. See [`templates/android/README.md`](templates/android/README.md) for the Android shell's details.
|
|
20
|
+
|
|
21
|
+
Both link Lynx's XElement library, which holds `<input>`, `<textarea>`, `<overlay>`, `<svg>`, `<blur-view>`, `<viewpager>`, `<refresh>`, the `<scroll-coordinator>` family, `<webview>` and `<video>`. Lynx's core doesn't include these, and a host that doesn't register them renders nothing for those tags (on Android, nothing after them either). XElement's AnimaX and Markdown elements are left out: the runtime doesn't support them.
|
|
22
|
+
|
|
23
|
+
| | iOS | Android |
|
|
24
|
+
| ---------------- | ---------------------------------------------------- | -------------------------------------------------------- |
|
|
25
|
+
| Language | Swift, scene lifecycle | Kotlin |
|
|
26
|
+
| Lynx | 4.1.0, prebuilt XCFrameworks (below) | 4.1.0 AARs from Maven Central |
|
|
27
|
+
| XElement | Linked and registered (`App/XElementRegistry.swift`) | Linked and registered (`XElementBehaviors`) |
|
|
28
|
+
| Toolchain | Xcode 27, iOS 15+ | AGP 9.4, Gradle 9.6, JDK 17+, SDK 36, minSdk 24 |
|
|
29
|
+
| Generated config | `Config/App.xcconfig`, `App/Info.plist` markers | `angular-lynx.properties`, `AndroidManifest.xml` markers |
|
|
25
30
|
|
|
26
31
|
## Lynx for iOS
|
|
27
32
|
|
|
28
|
-
Lynx publishes iOS only as source pods, which take minutes to compile and need CocoaPods (and so Ruby). `scripts/build-lynx-ios-frameworks.sh` compiles them once into XCFrameworks (about
|
|
33
|
+
Lynx publishes iOS only as source pods, which take minutes to compile and need CocoaPods (and so Ruby). `scripts/build-lynx-ios-frameworks.sh` compiles them once into XCFrameworks (about 26 MB zipped, with debug symbols in a separate zip): Lynx, PrimJS, XElement and their dependencies. The builder downloads the zip pinned in `src/ios/frameworks.ts` (URL and SHA-256) on first use and caches it in `~/.cache/angular-lynx/lynx-ios/<version>-<build>`.
|
|
29
34
|
|
|
30
|
-
The zip for Lynx 4.1.0 is the [`lynx-ios-4.1.0` release](https://github.com/Blotch-Smart-Frames/AngularLynx/releases/tag/lynx-ios-4.1.0). For a new Lynx version, bump the versions in the script, run the **Lynx iOS frameworks** workflow with `publish` (or the script locally), and pin the new zip's URL and SHA-256. To test a locally built folder before publishing it:
|
|
35
|
+
The zip for Lynx 4.1.0 is the [`lynx-ios-4.1.0-2` release](https://github.com/Blotch-Smart-Frames/AngularLynx/releases/tag/lynx-ios-4.1.0-2). The build number counts rebuilds of one Lynx version whose frameworks changed (2 added XElement), and is part of the cache folder, so machines with an older zip download the new one. For a new Lynx version, bump the versions in the script and reset `BUILD` to 1; to change the frameworks of the same version, bump `BUILD` (in the script and in `LYNX_IOS`). Then run the **Lynx iOS frameworks** workflow with `publish` (or the script locally), and pin the new zip's URL and SHA-256. To test a locally built folder before publishing it:
|
|
31
36
|
|
|
32
37
|
```sh
|
|
33
38
|
npm run build:lynx-ios-frameworks -w packages/native # needs Xcode, and Ruby >= 2.7 with Bundler for CocoaPods
|
package/dist/ios/frameworks.d.ts
CHANGED
|
@@ -9,7 +9,14 @@
|
|
|
9
9
|
*/
|
|
10
10
|
export declare const LYNX_IOS: {
|
|
11
11
|
readonly version: "4.1.0";
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Counts rebuilds of one Lynx version whose frameworks changed (2 added
|
|
14
|
+
* XElement); BUILD in the script. It's part of the cache folder, so a
|
|
15
|
+
* machine that cached an older zip downloads the new one instead of
|
|
16
|
+
* linking frameworks the shell no longer matches.
|
|
17
|
+
*/
|
|
18
|
+
readonly build: 2;
|
|
19
|
+
readonly url: "https://github.com/Blotch-Smart-Frames/AngularLynx/releases/download/lynx-ios-4.1.0-2/lynx-ios-4.1.0-2.zip";
|
|
13
20
|
readonly sha256: string | undefined;
|
|
14
21
|
};
|
|
15
22
|
/**
|
|
@@ -17,7 +24,7 @@ export declare const LYNX_IOS: {
|
|
|
17
24
|
* scripts/generate-ios-project.ts) and the frameworks build both read this
|
|
18
25
|
* list, so it is the only place to change it.
|
|
19
26
|
*/
|
|
20
|
-
export declare const LYNX_IOS_FRAMEWORKS: readonly ["Lynx", "LynxBase", "LynxServiceAPI", "LynxService", "PrimJS", "SDWebImage", "SDWebImageWebPCoder", "libwebp"];
|
|
27
|
+
export declare const LYNX_IOS_FRAMEWORKS: readonly ["Lynx", "LynxBase", "LynxServiceAPI", "LynxService", "PrimJS", "SDWebImage", "SDWebImageWebPCoder", "libwebp", "XElement", "ServalSVG", "MJRefresh"];
|
|
21
28
|
/**
|
|
22
29
|
* Overrides the download with a local folder of XCFrameworks, e.g. the
|
|
23
30
|
* `out/LynxFrameworks` folder build-lynx-ios-frameworks.sh writes. For testing
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"frameworks.d.ts","sourceRoot":"","sources":["../../src/ios/frameworks.ts"],"names":[],"mappings":"AAcA;;;;;;;;GAQG;AACH,eAAO,MAAM,QAAQ;;;
|
|
1
|
+
{"version":3,"file":"frameworks.d.ts","sourceRoot":"","sources":["../../src/ios/frameworks.ts"],"names":[],"mappings":"AAcA;;;;;;;;GAQG;AACH,eAAO,MAAM,QAAQ;;IAEnB;;;;;OAKG;;;qBAIC,MAAM,GACN,SAAS;CACL,CAAC;AAEX;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,gKAYtB,CAAC;AAEX;;;;GAIG;AACH,eAAO,MAAM,cAAc,gCAAgC,CAAC;AAS5D,+EAA+E;AAC/E,eAAO,MAAM,qBAAqB,GAChC,SAAS,MAAM,EACf,KAAK,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,KAC7B,OAAO,CAAC,MAAM,CAmDhB,CAAC;AAaF,uEAAuE;AACvE,eAAO,MAAM,qBAAqB,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,SAM5D,CAAC"}
|
package/dist/ios/frameworks.js
CHANGED
|
@@ -14,8 +14,15 @@ import { run } from "../process.js";
|
|
|
14
14
|
*/
|
|
15
15
|
export const LYNX_IOS = {
|
|
16
16
|
version: '4.1.0',
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Counts rebuilds of one Lynx version whose frameworks changed (2 added
|
|
19
|
+
* XElement); BUILD in the script. It's part of the cache folder, so a
|
|
20
|
+
* machine that cached an older zip downloads the new one instead of
|
|
21
|
+
* linking frameworks the shell no longer matches.
|
|
22
|
+
*/
|
|
23
|
+
build: 2,
|
|
24
|
+
url: 'https://github.com/Blotch-Smart-Frames/AngularLynx/releases/download/lynx-ios-4.1.0-2/lynx-ios-4.1.0-2.zip',
|
|
25
|
+
sha256: '88d0bbea842da734c3513df2a14e641d85cac645d3f432781af0da7c035e4646',
|
|
19
26
|
};
|
|
20
27
|
/**
|
|
21
28
|
* The frameworks in the zip. The template project (APP_PROJECT in
|
|
@@ -31,6 +38,9 @@ export const LYNX_IOS_FRAMEWORKS = [
|
|
|
31
38
|
'SDWebImage',
|
|
32
39
|
'SDWebImageWebPCoder',
|
|
33
40
|
'libwebp',
|
|
41
|
+
'XElement',
|
|
42
|
+
'ServalSVG',
|
|
43
|
+
'MJRefresh',
|
|
34
44
|
];
|
|
35
45
|
/**
|
|
36
46
|
* Overrides the download with a local folder of XCFrameworks, e.g. the
|
|
@@ -46,7 +56,7 @@ export const resolveLynxFrameworks = async (logFile, log) => {
|
|
|
46
56
|
assertComplete(override, `${FRAMEWORKS_ENV} (${override})`);
|
|
47
57
|
return path.resolve(override);
|
|
48
58
|
}
|
|
49
|
-
const dir = path.join(cacheRoot(), LYNX_IOS.version);
|
|
59
|
+
const dir = path.join(cacheRoot(), `${LYNX_IOS.version}-${LYNX_IOS.build}`);
|
|
50
60
|
const frameworks = path.join(dir, 'LynxFrameworks');
|
|
51
61
|
if (existsSync(path.join(dir, '.complete'))) {
|
|
52
62
|
return frameworks;
|
package/package.json
CHANGED
|
@@ -26,17 +26,19 @@ Build commands and outputs (no flavors, so the names are fixed):
|
|
|
26
26
|
## How the shell loads the app
|
|
27
27
|
|
|
28
28
|
- **Bundles** come from `assets/` (`ResourceFetchers.kt`, `BundleAssets.kt`). A URL without a scheme (`/static/…`, `static/…`), `asset:///…` and `file:///android_asset/…` all mean "the file at that path in `assets/`", so the bundle works with Rspeedy's default asset prefix (`/`) and doesn't need a platform-specific one. Absolute `http(s)` URLs are fetched with OkHttp.
|
|
29
|
+
- **`fetch()` and `XMLHttpRequest`** go through Lynx's HTTP service (`LynxHttpService`, registered in `ShellApplication.kt`), which uses OkHttp. Plain HTTP is allowed to `localhost` and `127.0.0.1` only (`res/xml/network_security_config.xml`), for a server on the device or one on your machine through `adb reverse`; every other host needs HTTPS, Android's default.
|
|
29
30
|
- **Images** are loaded by Fresco. The media fetcher rewrites bundled image URLs to `asset:///…`, which Fresco reads from the APK.
|
|
30
31
|
- **Lazy routes** need nothing extra: the build plugin keeps each lazy chunk inside `main.lynx.bundle` (see `split-chunks.ts` in the build plugin). Bundles requested by URL at runtime still go through the template resource fetcher.
|
|
31
32
|
|
|
32
33
|
## Back button
|
|
33
34
|
|
|
34
|
-
Back is off by default, so it leaves the app like any root screen. The runtime turns it on while the Router has history by calling `NativeModules.AngularLynxShell.setBackHandlerEnabled(true)` (
|
|
35
|
+
Back is off by default, so it leaves the app like any root screen. The runtime turns it on while the Router has history by calling `NativeModules.AngularLynxShell.setBackHandlerEnabled(true)`, and off again on the first screen (`LynxBackButton` in `packages/runtime/src/lib/router/lynx-back-button.ts`, which `provideRouter()` starts; the call goes through the background thread, where `NativeModules` lives). While it's on, a back press sends the global event `angularLynxBack` instead, which the runtime turns into `Location.back()`. The app has to say beforehand because Android decides synchronously, at the start of the gesture, whether an app handles back (predictive back animates differently). Layer 6's `native/back-button.yaml` checks both ends.
|
|
35
36
|
|
|
36
37
|
## Versions
|
|
37
38
|
|
|
38
39
|
Pinned in `app/build.gradle.kts`, `build.gradle.kts` and `gradle/wrapper/gradle-wrapper.properties`:
|
|
39
40
|
|
|
40
41
|
- Lynx 4.1.0 and PrimJS 4.1.1 (Maven Central), Fresco 2.3.0 (the version Lynx's image service is built against), OkHttp 4.12.
|
|
42
|
+
- Lynx's XElement 4.1.0 (`<input>`, `<textarea>`, `<overlay>`, `<svg>`, `<blur-view>`, `<viewpager>`, `<refresh>`, the `<scroll-coordinator>` family, `<webview>`, `<video>`), without its AnimaX and Markdown modules, which the runtime doesn't support. `MainActivity` registers the elements with `XElementBehaviors`. XElement depends on appcompat and material 1.0.0, whose vectordrawable libraries share a namespace that AGP 9 rejects, so appcompat 1.7.1 and material 1.12.0 are pinned.
|
|
41
43
|
- AGP 9.4.1 with its built-in Kotlin, Gradle 9.6.1 (AGP 9.4's minimum), JDK 17 or newer.
|
|
42
44
|
- `compileSdk` and `targetSdk` 36, `minSdk` 24. Google Play raises its target API requirement every August; bump `targetSdk` with it.
|
|
@@ -137,4 +137,22 @@ dependencies {
|
|
|
137
137
|
implementation("com.facebook.fresco:webpsupport:$frescoVersion")
|
|
138
138
|
implementation("com.facebook.fresco:animated-base:$frescoVersion")
|
|
139
139
|
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
|
140
|
+
|
|
141
|
+
// XElement: <input>, <textarea>, <overlay>, <svg>, <blur-view>,
|
|
142
|
+
// <viewpager>, <refresh>, the <scroll-coordinator> family, <webview> and
|
|
143
|
+
// <video>. They aren't in Lynx's core, and without them those tags render
|
|
144
|
+
// nothing. MainActivity registers them with XElementBehaviors. The
|
|
145
|
+
// umbrella artifact depends on one module per element; AnimaX and
|
|
146
|
+
// Markdown are elements the runtime doesn't support, and AnimaX alone
|
|
147
|
+
// adds about 5 MB of native code per ABI.
|
|
148
|
+
implementation("org.lynxsdk.lynx:xelement:$lynxVersion") {
|
|
149
|
+
exclude(group = "org.lynxsdk.lynx", module = "xelement-animax")
|
|
150
|
+
exclude(group = "org.lynxsdk.lynx", module = "xelement-markdown")
|
|
151
|
+
}
|
|
152
|
+
// XElement's modules depend on appcompat and material 1.0.0, which bring
|
|
153
|
+
// vectordrawable 1.0.0 and vectordrawable-animated 1.0.0. Those two share
|
|
154
|
+
// the namespace androidx.vectordrawable, and AGP 9's manifest merger
|
|
155
|
+
// fails the build on that; newer versions give them distinct namespaces.
|
|
156
|
+
implementation("androidx.appcompat:appcompat:1.7.1")
|
|
157
|
+
implementation("com.google.android.material:material:1.12.0")
|
|
140
158
|
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
package dev.angularlynx.shell
|
|
2
2
|
|
|
3
|
+
import android.content.res.Configuration
|
|
3
4
|
import android.os.Bundle
|
|
4
5
|
import android.util.Log
|
|
5
6
|
import android.widget.FrameLayout
|
|
6
7
|
import androidx.activity.ComponentActivity
|
|
7
8
|
import androidx.activity.OnBackPressedCallback
|
|
8
9
|
import androidx.activity.enableEdgeToEdge
|
|
10
|
+
import androidx.core.graphics.Insets
|
|
11
|
+
import androidx.core.os.ConfigurationCompat
|
|
9
12
|
import androidx.core.view.ViewCompat
|
|
10
13
|
import androidx.core.view.WindowInsetsCompat
|
|
11
14
|
import com.lynx.react.bridge.JavaOnlyArray
|
|
@@ -14,6 +17,7 @@ import com.lynx.tasm.LynxError
|
|
|
14
17
|
import com.lynx.tasm.LynxView
|
|
15
18
|
import com.lynx.tasm.LynxViewBuilder
|
|
16
19
|
import com.lynx.tasm.LynxViewClient
|
|
20
|
+
import com.lynx.xelement.XElementBehaviors
|
|
17
21
|
|
|
18
22
|
/**
|
|
19
23
|
* The whole app: one full-screen LynxView running the Angular app. Angular
|
|
@@ -22,6 +26,11 @@ import com.lynx.tasm.LynxViewClient
|
|
|
22
26
|
class MainActivity : ComponentActivity(), ShellModule.Host {
|
|
23
27
|
private lateinit var lynxView: LynxView
|
|
24
28
|
|
|
29
|
+
// The system bar and cutout insets, and whether the screen has a cutout,
|
|
30
|
+
// as the window last reported them. Sent in the global props.
|
|
31
|
+
private var safeArea = Insets.NONE
|
|
32
|
+
private var hasCutout = false
|
|
33
|
+
|
|
25
34
|
// Disabled until the Angular app enables it (see ShellModule), so by
|
|
26
35
|
// default back leaves the app like any other root screen.
|
|
27
36
|
private val backHandler = object : OnBackPressedCallback(false) {
|
|
@@ -43,6 +52,10 @@ class MainActivity : ComponentActivity(), ShellModule.Host {
|
|
|
43
52
|
setEnableGenericResourceFetcher(LynxBooleanOption.TRUE)
|
|
44
53
|
setGenericResourceFetcher(AssetGenericResourceFetcher(this@MainActivity))
|
|
45
54
|
registerModule(ShellModule.NAME, ShellModule::class.java, this@MainActivity)
|
|
55
|
+
// Lynx's core has no <input>, <overlay>, <svg>, <blur-view> etc.;
|
|
56
|
+
// these come from XElement (see app/build.gradle.kts). Lynx
|
|
57
|
+
// Explorer registers them the same way.
|
|
58
|
+
addBehaviors(XElementBehaviors().create())
|
|
46
59
|
}.build(this)
|
|
47
60
|
lynxView.addLynxViewClient(object : LynxViewClient() {
|
|
48
61
|
override fun onReceivedError(error: LynxError?) {
|
|
@@ -51,7 +64,9 @@ class MainActivity : ComponentActivity(), ShellModule.Host {
|
|
|
51
64
|
})
|
|
52
65
|
|
|
53
66
|
// Keep the app inside the safe area (status bar, navigation bar and
|
|
54
|
-
// camera cutout)
|
|
67
|
+
// camera cutout). Lynx has no way to give CSS env(safe-area-inset-*)
|
|
68
|
+
// values on Android, so they're 0, which is right for a page that's
|
|
69
|
+
// already inside the safe area. The iOS shell matches.
|
|
55
70
|
val container = FrameLayout(this)
|
|
56
71
|
container.addView(lynxView, FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
|
|
57
72
|
ViewCompat.setOnApplyWindowInsetsListener(container) { view, insets ->
|
|
@@ -61,11 +76,24 @@ class MainActivity : ComponentActivity(), ShellModule.Host {
|
|
|
61
76
|
WindowInsetsCompat.Type.ime(),
|
|
62
77
|
)
|
|
63
78
|
view.setPadding(safe.left, safe.top, safe.right, safe.bottom)
|
|
79
|
+
// The keyboard isn't part of the safe area, as on iOS.
|
|
80
|
+
val bars = insets.getInsets(
|
|
81
|
+
WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout(),
|
|
82
|
+
)
|
|
83
|
+
val cutout = insets.displayCutout != null
|
|
84
|
+
if (bars != safeArea || cutout != hasCutout) {
|
|
85
|
+
safeArea = bars
|
|
86
|
+
hasCutout = cutout
|
|
87
|
+
lynxView.updateGlobalProps(globalProps(resources.configuration))
|
|
88
|
+
}
|
|
64
89
|
WindowInsetsCompat.CONSUMED
|
|
65
90
|
}
|
|
66
91
|
setContentView(container)
|
|
67
92
|
|
|
68
93
|
onBackPressedDispatcher.addCallback(this, backHandler)
|
|
94
|
+
// Sent before the bundle loads, so the app starts with the theme and
|
|
95
|
+
// the language. The insets follow once the window reports them.
|
|
96
|
+
lynxView.updateGlobalProps(globalProps(resources.configuration))
|
|
69
97
|
lynxView.renderTemplateUrl(BundleAssets.MAIN_BUNDLE, "")
|
|
70
98
|
}
|
|
71
99
|
|
|
@@ -73,6 +101,38 @@ class MainActivity : ComponentActivity(), ShellModule.Host {
|
|
|
73
101
|
backHandler.isEnabled = enabled
|
|
74
102
|
}
|
|
75
103
|
|
|
104
|
+
// The manifest's configChanges keeps the activity, and the running app,
|
|
105
|
+
// through a dark mode switch, so the new theme is sent from here. A
|
|
106
|
+
// language change still restarts the activity, which sends the new
|
|
107
|
+
// appLocale: Angular reads LOCALE_ID only once.
|
|
108
|
+
override fun onConfigurationChanged(newConfig: Configuration) {
|
|
109
|
+
super.onConfigurationChanged(newConfig)
|
|
110
|
+
lynxView.updateGlobalProps(globalProps(newConfig))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The global props the runtime reads: `theme` (LynxTheme), `isNotchScreen`
|
|
115
|
+
* (LynxSafeArea) and `appLocale` (LynxLocale), plus the safe-area insets
|
|
116
|
+
* in dp. The iOS shell sends the same keys, and Lynx Explorer sends theme
|
|
117
|
+
* and isNotchScreen too.
|
|
118
|
+
*/
|
|
119
|
+
private fun globalProps(config: Configuration): Map<String, Any> {
|
|
120
|
+
val density = resources.displayMetrics.density
|
|
121
|
+
val night = config.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
|
|
122
|
+
val props = mutableMapOf<String, Any>(
|
|
123
|
+
"platform" to "android",
|
|
124
|
+
"theme" to if (night) "Dark" else "Light",
|
|
125
|
+
"isNotchScreen" to hasCutout,
|
|
126
|
+
"safeAreaTop" to safeArea.top / density,
|
|
127
|
+
"safeAreaBottom" to safeArea.bottom / density,
|
|
128
|
+
"safeAreaLeft" to safeArea.left / density,
|
|
129
|
+
"safeAreaRight" to safeArea.right / density,
|
|
130
|
+
)
|
|
131
|
+
// The user's language as a BCP 47 tag, such as "fr-FR".
|
|
132
|
+
ConfigurationCompat.getLocales(config)[0]?.let { props["appLocale"] = it.toLanguageTag() }
|
|
133
|
+
return props
|
|
134
|
+
}
|
|
135
|
+
|
|
76
136
|
// Lynx pauses timers and animations in the background and fires the
|
|
77
137
|
// page's visibility events from these.
|
|
78
138
|
override fun onResume() {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!--
|
|
3
|
+
Allows plain HTTP to this device's own loopback address, and nowhere else.
|
|
4
|
+
Loopback traffic never leaves the device, so there's nothing to intercept:
|
|
5
|
+
it reaches a server on the device itself, or one on the development machine
|
|
6
|
+
through `adb reverse`. Every other host keeps Android's default, which
|
|
7
|
+
blocks cleartext. Without this, fetch() to http://localhost failed with
|
|
8
|
+
"CLEARTEXT communication not permitted", which Lynx reports as status 499.
|
|
9
|
+
-->
|
|
10
|
+
<network-security-config>
|
|
11
|
+
<domain-config cleartextTrafficPermitted="true">
|
|
12
|
+
<domain includeSubdomains="false">localhost</domain>
|
|
13
|
+
<domain includeSubdomains="false">127.0.0.1</domain>
|
|
14
|
+
</domain-config>
|
|
15
|
+
</network-security-config>
|
|
@@ -13,6 +13,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
|
|
13
13
|
_ application: UIApplication,
|
|
14
14
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
|
15
15
|
) -> Bool {
|
|
16
|
+
XElementRegistry.register()
|
|
16
17
|
let config = LynxConfig(provider: BundleResourceFetcher.shared)
|
|
17
18
|
LynxEnv.sharedInstance().prepareConfig(config)
|
|
18
19
|
// LynxService's image service decodes through SDWebImage, which only
|
|
@@ -30,6 +30,25 @@ final class LynxViewController: UIViewController {
|
|
|
30
30
|
layoutLynxView()
|
|
31
31
|
lynxView.updateGlobalProps(with: globalProps())
|
|
32
32
|
lynxView.loadTemplate(fromURL: "main.lynx.bundle", initData: nil)
|
|
33
|
+
|
|
34
|
+
// Send the new theme when the phone switches between light and dark
|
|
35
|
+
// while the app is open. Before iOS 17, traitCollectionDidChange does.
|
|
36
|
+
if #available(iOS 17.0, *) {
|
|
37
|
+
registerForTraitChanges([UITraitUserInterfaceStyle.self]) {
|
|
38
|
+
(controller: LynxViewController, _: UITraitCollection) in
|
|
39
|
+
controller.lynxView?.updateGlobalProps(with: controller.globalProps())
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@available(iOS, deprecated: 17.0, message: "registerForTraitChanges in viewDidLoad replaces it")
|
|
45
|
+
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
|
46
|
+
super.traitCollectionDidChange(previousTraitCollection)
|
|
47
|
+
if #unavailable(iOS 17.0),
|
|
48
|
+
traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection)
|
|
49
|
+
{
|
|
50
|
+
lynxView?.updateGlobalProps(with: globalProps())
|
|
51
|
+
}
|
|
33
52
|
}
|
|
34
53
|
|
|
35
54
|
override func viewDidLayoutSubviews() {
|
|
@@ -39,7 +58,13 @@ final class LynxViewController: UIViewController {
|
|
|
39
58
|
|
|
40
59
|
/// Keeps the LynxView inside the safe area, so a freshly generated app
|
|
41
60
|
/// doesn't draw under the notch or the home indicator. The insets are also
|
|
42
|
-
/// passed as global props for apps that want to
|
|
61
|
+
/// passed as global props for apps that want to know them.
|
|
62
|
+
///
|
|
63
|
+
/// CSS env(safe-area-inset-*) is 0 on every side, which is right for a page
|
|
64
|
+
/// that's already inside the safe area, and matches Android, where Lynx
|
|
65
|
+
/// never sets it. Lynx reads it from the key window when a LynxView is
|
|
66
|
+
/// created, and viewDidLoad runs before SceneDelegate makes the window key.
|
|
67
|
+
/// Creating the LynxView later would make env() add the insets a second time.
|
|
43
68
|
private func layoutLynxView() {
|
|
44
69
|
guard let lynxView else { return }
|
|
45
70
|
let frame = view.bounds.inset(by: view.safeAreaInsets)
|
|
@@ -54,15 +79,30 @@ final class LynxViewController: UIViewController {
|
|
|
54
79
|
lynxView.triggerLayout()
|
|
55
80
|
}
|
|
56
81
|
|
|
82
|
+
/// The global props the runtime reads: `theme` (LynxTheme), `isNotchScreen`
|
|
83
|
+
/// (LynxSafeArea) and `appLocale` (LynxLocale), plus the safe-area insets.
|
|
84
|
+
/// The Android shell sends the same keys, and Lynx Explorer sends theme and
|
|
85
|
+
/// isNotchScreen too.
|
|
57
86
|
private func globalProps() -> LynxTemplateData {
|
|
58
87
|
let insets = view.safeAreaInsets
|
|
59
|
-
|
|
88
|
+
var props: [String: Any] = [
|
|
60
89
|
"platform": "ios",
|
|
61
90
|
"safeAreaTop": insets.top,
|
|
62
91
|
"safeAreaBottom": insets.bottom,
|
|
63
92
|
"safeAreaLeft": insets.left,
|
|
64
93
|
"safeAreaRight": insets.right,
|
|
94
|
+
// Lynx Explorer's test: anything beyond a plain 20pt status bar, as on
|
|
95
|
+
// iPhones with a notch or Dynamic Island.
|
|
96
|
+
"isNotchScreen": insets.top > 20 || insets.bottom > 0 || insets.left > 0
|
|
97
|
+
|| insets.right > 0,
|
|
65
98
|
"theme": traitCollection.userInterfaceStyle == .dark ? "Dark" : "Light",
|
|
66
|
-
]
|
|
99
|
+
]
|
|
100
|
+
// The user's first language as a BCP 47 tag, such as "fr-FR". iOS
|
|
101
|
+
// restarts the app when the language changes, so there's no update to send.
|
|
102
|
+
if let language = Locale.preferredLanguages.first {
|
|
103
|
+
props["appLocale"] = language
|
|
104
|
+
}
|
|
105
|
+
// Without useBoolLiterals, Lynx turns a Bool into 1 or 0.
|
|
106
|
+
return LynxTemplateData(dictionary: props, useBoolLiterals: true)
|
|
67
107
|
}
|
|
68
108
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import Lynx
|
|
2
|
+
import XElement
|
|
3
|
+
|
|
4
|
+
/// Registers XElement's elements: `<input>`, `<textarea>`, `<overlay>`,
|
|
5
|
+
/// `<svg>`, `<blur-view>`, `<viewpager>`, `<refresh>`, the
|
|
6
|
+
/// `<scroll-coordinator>` family, `<webview>` and `<video>`. Lynx's core
|
|
7
|
+
/// doesn't include them, and a tag nothing is registered for renders nothing.
|
|
8
|
+
///
|
|
9
|
+
/// Lynx Explorer gets these registrations from XElement's `Behavior` subspec,
|
|
10
|
+
/// but that subspec also links Markdown, an element the runtime doesn't
|
|
11
|
+
/// support, so the frameworks build leaves it out
|
|
12
|
+
/// (scripts/build-lynx-ios-frameworks.sh) and the shell registers the same
|
|
13
|
+
/// classes under the same names here, before the first LynxConfig or LynxView
|
|
14
|
+
/// exists.
|
|
15
|
+
enum XElementRegistry {
|
|
16
|
+
static func register() {
|
|
17
|
+
let elements: [(String, AnyClass)] = [
|
|
18
|
+
("input", LynxUIInput.self),
|
|
19
|
+
("textarea", LynxUITextArea.self),
|
|
20
|
+
("overlay", LynxUIOverlay.self),
|
|
21
|
+
("svg", LynxUISVG.self),
|
|
22
|
+
("blur-view", LynxUIBlurView.self),
|
|
23
|
+
("viewpager", LynxUIViewPager.self),
|
|
24
|
+
("viewpager-item", LynxUIViewPagerItem.self),
|
|
25
|
+
("refresh", LynxUIRefresh.self),
|
|
26
|
+
("refresh-header", LynxUIRefreshHeader.self),
|
|
27
|
+
("scroll-coordinator", LynxUIScrollCoordinator.self),
|
|
28
|
+
("scroll-coordinator-header", LynxUIScrollCoordinatorHeader.self),
|
|
29
|
+
("scroll-coordinator-slot", LynxUIScrollCoordinatorSlot.self),
|
|
30
|
+
("scroll-coordinator-slot-drag", LynxUIScrollCoordinatorSlotDrag.self),
|
|
31
|
+
("scroll-coordinator-toolbar", LynxUIScrollCoordinatorToolbar.self),
|
|
32
|
+
("webview", LynxUIWebView.self),
|
|
33
|
+
("video", LynxUIVideo.self),
|
|
34
|
+
]
|
|
35
|
+
for (name, ui) in elements {
|
|
36
|
+
LynxComponentRegistry.registerUI(ui, withName: name)
|
|
37
|
+
}
|
|
38
|
+
// The elements whose size comes from their content (a text field's
|
|
39
|
+
// text, an overlay's screen, a refresh's header) also measure themselves
|
|
40
|
+
// in layout, through a shadow node.
|
|
41
|
+
let shadowNodes: [(String, AnyClass)] = [
|
|
42
|
+
("input", LynxUIInputShadowNode.self),
|
|
43
|
+
("textarea", LynxUITextAreaShadowNode.self),
|
|
44
|
+
("overlay", LynxUIOverlayShadowNode.self),
|
|
45
|
+
("refresh", LynxUIRefreshShadowNode.self),
|
|
46
|
+
]
|
|
47
|
+
for (name, node) in shadowNodes {
|
|
48
|
+
LynxComponentRegistry.registerShadowNode(node, withName: name)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -11,10 +11,14 @@
|
|
|
11
11
|
0E3870431039EBF246629EDC /* LynxBase.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 03FA19016485E5D5DFE1595F /* LynxBase.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
|
12
12
|
24F6F7C0ED16226B5F4CEC64 /* libwebp.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 22AE3F7CF67B188787336BFA /* libwebp.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
|
13
13
|
3222B5E4F829B616E7F195E6 /* LynxService.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 59F6406B1F8C89E22DDE0B86 /* LynxService.xcframework */; };
|
|
14
|
+
333153D6D20AE3F4F4F0F232 /* XElement.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CA3E9DB48B8E7102AFB3F9D3 /* XElement.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
|
14
15
|
3C05B6061112B7F48C5D3539 /* LynxServiceAPI.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 1DB5C3D66078CC41360F16AF /* LynxServiceAPI.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
|
16
|
+
481B830F1B162EF6665B22C7 /* ServalSVG.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1555CF4C65B98EB4D3757A5B /* ServalSVG.xcframework */; };
|
|
15
17
|
4C8131B26C3C8894FC77643D /* LynxService.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 59F6406B1F8C89E22DDE0B86 /* LynxService.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
|
16
18
|
4CFD715805AA40F2DF9A81AF /* LynxServiceAPI.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1DB5C3D66078CC41360F16AF /* LynxServiceAPI.xcframework */; };
|
|
17
19
|
52C2300A66557A4A04A633FD /* SDWebImage.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 0CDAE1A45DE3FB3C238352B1 /* SDWebImage.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
|
20
|
+
55C00449AF3D55EB900A1B9A /* MJRefresh.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A479BE709FC284B487B4B3EE /* MJRefresh.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
|
21
|
+
5D986CA1D5DAB3B9F6D299F5 /* ServalSVG.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 1555CF4C65B98EB4D3757A5B /* ServalSVG.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
|
18
22
|
5F80D1C6C1436E9CB4F93155 /* PrimJS.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6B827DF13C44D4B05B7DAE8C /* PrimJS.xcframework */; };
|
|
19
23
|
7F265E09F76C87AD1CB5DECB /* LynxBundle in Resources */ = {isa = PBXBuildFile; fileRef = D84EC48E5D673EC745AF0F7D /* LynxBundle */; };
|
|
20
24
|
8BCF0DFFF316AA01FC859C29 /* Lynx.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 06FE05FCCC1F0C34FAAAAE10 /* Lynx.xcframework */; };
|
|
@@ -23,7 +27,9 @@
|
|
|
23
27
|
C153F098EA734F20BDC0E7F1 /* libwebp.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22AE3F7CF67B188787336BFA /* libwebp.xcframework */; };
|
|
24
28
|
C92615D6F55A8C1D8D6489E7 /* PrimJS.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 6B827DF13C44D4B05B7DAE8C /* PrimJS.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
|
25
29
|
CF3E40650CC18BF7B0B1AB22 /* SDWebImage.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0CDAE1A45DE3FB3C238352B1 /* SDWebImage.xcframework */; };
|
|
30
|
+
E0F04E5FF5F7548B94615806 /* MJRefresh.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A479BE709FC284B487B4B3EE /* MJRefresh.xcframework */; };
|
|
26
31
|
ED8182CEA14283E4D2AB28E9 /* SDWebImageWebPCoder.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 938799E9025522AF0DB08F41 /* SDWebImageWebPCoder.xcframework */; };
|
|
32
|
+
F9F12589EAE493E887916AB0 /* XElement.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = CA3E9DB48B8E7102AFB3F9D3 /* XElement.xcframework */; };
|
|
27
33
|
/* End PBXBuildFile section */
|
|
28
34
|
|
|
29
35
|
/* Begin PBXCopyFilesBuildPhase section */
|
|
@@ -41,6 +47,9 @@
|
|
|
41
47
|
52C2300A66557A4A04A633FD /* SDWebImage.xcframework in Embed Frameworks */,
|
|
42
48
|
96D79D9E007FEA2813F7072F /* SDWebImageWebPCoder.xcframework in Embed Frameworks */,
|
|
43
49
|
24F6F7C0ED16226B5F4CEC64 /* libwebp.xcframework in Embed Frameworks */,
|
|
50
|
+
333153D6D20AE3F4F4F0F232 /* XElement.xcframework in Embed Frameworks */,
|
|
51
|
+
5D986CA1D5DAB3B9F6D299F5 /* ServalSVG.xcframework in Embed Frameworks */,
|
|
52
|
+
55C00449AF3D55EB900A1B9A /* MJRefresh.xcframework in Embed Frameworks */,
|
|
44
53
|
);
|
|
45
54
|
name = "Embed Frameworks";
|
|
46
55
|
runOnlyForDeploymentPostprocessing = 0;
|
|
@@ -51,6 +60,7 @@
|
|
|
51
60
|
03FA19016485E5D5DFE1595F /* LynxBase.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = LynxBase.xcframework; sourceTree = "<group>"; };
|
|
52
61
|
06FE05FCCC1F0C34FAAAAE10 /* Lynx.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = Lynx.xcframework; sourceTree = "<group>"; };
|
|
53
62
|
0CDAE1A45DE3FB3C238352B1 /* SDWebImage.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = SDWebImage.xcframework; sourceTree = "<group>"; };
|
|
63
|
+
1555CF4C65B98EB4D3757A5B /* ServalSVG.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = ServalSVG.xcframework; sourceTree = "<group>"; };
|
|
54
64
|
15B4BAB9D017652EC88BA5A1 /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
55
65
|
1DB5C3D66078CC41360F16AF /* LynxServiceAPI.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = LynxServiceAPI.xcframework; sourceTree = "<group>"; };
|
|
56
66
|
22AE3F7CF67B188787336BFA /* libwebp.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = libwebp.xcframework; sourceTree = "<group>"; };
|
|
@@ -58,6 +68,8 @@
|
|
|
58
68
|
59F6406B1F8C89E22DDE0B86 /* LynxService.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = LynxService.xcframework; sourceTree = "<group>"; };
|
|
59
69
|
6B827DF13C44D4B05B7DAE8C /* PrimJS.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = PrimJS.xcframework; sourceTree = "<group>"; };
|
|
60
70
|
938799E9025522AF0DB08F41 /* SDWebImageWebPCoder.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = SDWebImageWebPCoder.xcframework; sourceTree = "<group>"; };
|
|
71
|
+
A479BE709FC284B487B4B3EE /* MJRefresh.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = MJRefresh.xcframework; sourceTree = "<group>"; };
|
|
72
|
+
CA3E9DB48B8E7102AFB3F9D3 /* XElement.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = XElement.xcframework; sourceTree = "<group>"; };
|
|
61
73
|
D84EC48E5D673EC745AF0F7D /* LynxBundle */ = {isa = PBXFileReference; lastKnownFileType = folder; path = LynxBundle; sourceTree = "<group>"; };
|
|
62
74
|
/* End PBXFileReference section */
|
|
63
75
|
|
|
@@ -95,6 +107,9 @@
|
|
|
95
107
|
CF3E40650CC18BF7B0B1AB22 /* SDWebImage.xcframework in Frameworks */,
|
|
96
108
|
ED8182CEA14283E4D2AB28E9 /* SDWebImageWebPCoder.xcframework in Frameworks */,
|
|
97
109
|
C153F098EA734F20BDC0E7F1 /* libwebp.xcframework in Frameworks */,
|
|
110
|
+
F9F12589EAE493E887916AB0 /* XElement.xcframework in Frameworks */,
|
|
111
|
+
481B830F1B162EF6665B22C7 /* ServalSVG.xcframework in Frameworks */,
|
|
112
|
+
E0F04E5FF5F7548B94615806 /* MJRefresh.xcframework in Frameworks */,
|
|
98
113
|
);
|
|
99
114
|
runOnlyForDeploymentPostprocessing = 0;
|
|
100
115
|
};
|
|
@@ -131,6 +146,9 @@
|
|
|
131
146
|
0CDAE1A45DE3FB3C238352B1 /* SDWebImage.xcframework */,
|
|
132
147
|
938799E9025522AF0DB08F41 /* SDWebImageWebPCoder.xcframework */,
|
|
133
148
|
22AE3F7CF67B188787336BFA /* libwebp.xcframework */,
|
|
149
|
+
CA3E9DB48B8E7102AFB3F9D3 /* XElement.xcframework */,
|
|
150
|
+
1555CF4C65B98EB4D3757A5B /* ServalSVG.xcframework */,
|
|
151
|
+
A479BE709FC284B487B4B3EE /* MJRefresh.xcframework */,
|
|
134
152
|
);
|
|
135
153
|
path = Frameworks;
|
|
136
154
|
sourceTree = "<group>";
|