@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 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. See [`templates/android/README.md`](templates/android/README.md) for the Android shell's details.
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
- | | iOS | Android |
20
- | ---------------- | ----------------------------------------------- | -------------------------------------------------------- |
21
- | Language | Swift, scene lifecycle | Kotlin |
22
- | Lynx | 4.1.0, prebuilt XCFrameworks (below) | 4.1.0 AARs from Maven Central |
23
- | Toolchain | Xcode 27, iOS 15+ | AGP 9.4, Gradle 9.6, JDK 17+, SDK 36, minSdk 24 |
24
- | Generated config | `Config/App.xcconfig`, `App/Info.plist` markers | `angular-lynx.properties`, `AndroidManifest.xml` markers |
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 24 MB zipped, with debug symbols in a separate zip). 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>`.
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
@@ -9,7 +9,14 @@
9
9
  */
10
10
  export declare const LYNX_IOS: {
11
11
  readonly version: "4.1.0";
12
- readonly url: "https://github.com/Blotch-Smart-Frames/AngularLynx/releases/download/lynx-ios-4.1.0/lynx-ios-4.1.0.zip";
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;;;qBAIf,MAAM,GACN,SAAS;CACL,CAAC;AAEX;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,0HAStB,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"}
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"}
@@ -14,8 +14,15 @@ import { run } from "../process.js";
14
14
  */
15
15
  export const LYNX_IOS = {
16
16
  version: '4.1.0',
17
- url: 'https://github.com/Blotch-Smart-Frames/AngularLynx/releases/download/lynx-ios-4.1.0/lynx-ios-4.1.0.zip',
18
- sha256: '8f00fbd9eddc885175ea526136197222b5a8bd892c6b4d97e131abf88b9a39f4',
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blotch/angular-lynx-native",
3
- "version": "22.2.2",
3
+ "version": "22.2.3",
4
4
  "description": "Builds AngularLynx apps into iOS and Android binaries",
5
5
  "homepage": "https://github.com/Blotch-Smart-Frames/AngularLynx#readme",
6
6
  "bugs": {
@@ -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)` (from the background thread, where `NativeModules` lives). While it's on, a back press sends the global event `angularLynxBack` instead, which the runtime turns into a Router back navigation. 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).
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
  }
@@ -10,6 +10,7 @@
10
10
  android:allowBackup="true"
11
11
  android:icon="@mipmap/ic_launcher"
12
12
  android:label="@string/app_name"
13
+ android:networkSecurityConfig="@xml/network_security_config"
13
14
  android:supportsRtl="true"
14
15
  android:theme="@style/Theme.AngularLynxShell">
15
16
  <!--
@@ -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) until the runtime exposes the insets to CSS.
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 draw edge to edge later.
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
- return LynxTemplateData(dictionary: [
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>";