@blotch/angular-lynx-native 22.2.3 → 22.2.4

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
@@ -16,7 +16,7 @@ An ejected project (`ios/` or `android/` with a `.angular-lynx-ejected` marker,
16
16
 
17
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
- 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.
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`, the safe-area insets `safeAreaTop`, `safeAreaBottom`, `safeAreaLeft` and `safeAreaRight`, `osVersion` (which Lynx leaves out of the main thread's `SystemInfo`), and the accessibility settings `reduceMotion`, `reduceTransparency` (iOS only) and `increaseContrast`, again whenever the user changes one. Lynx Explorer sends `theme` and `isNotchScreen` too, but no `appLocale`, `osVersion` or accessibility settings. See [`templates/android/README.md`](templates/android/README.md) for the Android shell's details.
20
20
 
21
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
22
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blotch/angular-lynx-native",
3
- "version": "22.2.3",
3
+ "version": "22.2.4",
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": {
@@ -1,8 +1,15 @@
1
1
  package dev.angularlynx.shell
2
2
 
3
+ import android.app.UiModeManager
3
4
  import android.content.res.Configuration
5
+ import android.database.ContentObserver
6
+ import android.os.Build
4
7
  import android.os.Bundle
8
+ import android.os.Handler
9
+ import android.os.Looper
10
+ import android.provider.Settings
5
11
  import android.util.Log
12
+ import android.view.accessibility.AccessibilityManager
6
13
  import android.widget.FrameLayout
7
14
  import androidx.activity.ComponentActivity
8
15
  import androidx.activity.OnBackPressedCallback
@@ -31,6 +38,16 @@ class MainActivity : ComponentActivity(), ShellModule.Host {
31
38
  private var safeArea = Insets.NONE
32
39
  private var hasCutout = false
33
40
 
41
+ // Sends the accessibility settings again when the user changes one while
42
+ // the app is open: Remove animations (the animator duration scale), the
43
+ // contrast level (Android 14) and high contrast text (Android 16). The
44
+ // contrast listeners are kept as Any so the class loads on older Androids.
45
+ private val animationScaleObserver = object : ContentObserver(Handler(Looper.getMainLooper())) {
46
+ override fun onChange(selfChange: Boolean) = sendGlobalProps()
47
+ }
48
+ private var contrastListener: Any? = null
49
+ private var highContrastTextListener: Any? = null
50
+
34
51
  // Disabled until the Angular app enables it (see ShellModule), so by
35
52
  // default back leaves the app like any other root screen.
36
53
  private val backHandler = object : OnBackPressedCallback(false) {
@@ -90,6 +107,23 @@ class MainActivity : ComponentActivity(), ShellModule.Host {
90
107
  }
91
108
  setContentView(container)
92
109
 
110
+ contentResolver.registerContentObserver(
111
+ Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE),
112
+ false,
113
+ animationScaleObserver,
114
+ )
115
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
116
+ val listener = UiModeManager.ContrastChangeListener { sendGlobalProps() }
117
+ getSystemService(UiModeManager::class.java).addContrastChangeListener(mainExecutor, listener)
118
+ contrastListener = listener
119
+ }
120
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) {
121
+ val listener = AccessibilityManager.HighContrastTextStateChangeListener { sendGlobalProps() }
122
+ getSystemService(AccessibilityManager::class.java)
123
+ .addHighContrastTextStateChangeListener(mainExecutor, listener)
124
+ highContrastTextListener = listener
125
+ }
126
+
93
127
  onBackPressedDispatcher.addCallback(this, backHandler)
94
128
  // Sent before the bundle loads, so the app starts with the theme and
95
129
  // the language. The insets follow once the window reports them.
@@ -110,11 +144,16 @@ class MainActivity : ComponentActivity(), ShellModule.Host {
110
144
  lynxView.updateGlobalProps(globalProps(newConfig))
111
145
  }
112
146
 
147
+ private fun sendGlobalProps() {
148
+ lynxView.updateGlobalProps(globalProps(resources.configuration))
149
+ }
150
+
113
151
  /**
114
152
  * 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.
153
+ * (LynxSafeArea), `appLocale` (LynxLocale), the accessibility settings
154
+ * (LynxAccessibilitySettings) and `osVersion` (LynxSystemInfo), plus the
155
+ * safe-area insets in dp. The iOS shell sends the same keys, and Lynx
156
+ * Explorer sends theme and isNotchScreen too.
118
157
  */
119
158
  private fun globalProps(config: Configuration): Map<String, Any> {
120
159
  val density = resources.displayMetrics.density
@@ -127,12 +166,32 @@ class MainActivity : ComponentActivity(), ShellModule.Host {
127
166
  "safeAreaBottom" to safeArea.bottom / density,
128
167
  "safeAreaLeft" to safeArea.left / density,
129
168
  "safeAreaRight" to safeArea.right / density,
169
+ // Lynx builds the main thread's SystemInfo, where Angular runs,
170
+ // without the OS version, so LynxSystemInfo reads it from here.
171
+ "osVersion" to Build.VERSION.RELEASE,
172
+ // Lynx has no API for these. Android has no Reduce Transparency.
173
+ "reduceMotion" to reduceMotion(),
174
+ "increaseContrast" to increaseContrast(),
130
175
  )
131
176
  // The user's language as a BCP 47 tag, such as "fr-FR".
132
177
  ConfigurationCompat.getLocales(config)[0]?.let { props["appLocale"] = it.toLanguageTag() }
133
178
  return props
134
179
  }
135
180
 
181
+ // Settings > Accessibility > Remove animations sets the animator
182
+ // duration scale to 0, as does turning animations off in the developer
183
+ // options.
184
+ private fun reduceMotion(): Boolean =
185
+ Settings.Global.getFloat(contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f) == 0f
186
+
187
+ // Medium or high contrast (Android 14), or high contrast text (Android 16):
188
+ // the closest Android has to iOS's Increase Contrast.
189
+ private fun increaseContrast(): Boolean =
190
+ (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE &&
191
+ getSystemService(UiModeManager::class.java).contrast > 0f) ||
192
+ (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA &&
193
+ getSystemService(AccessibilityManager::class.java).isHighContrastTextEnabled)
194
+
136
195
  // Lynx pauses timers and animations in the background and fires the
137
196
  // page's visibility events from these.
138
197
  override fun onResume() {
@@ -146,6 +205,17 @@ class MainActivity : ComponentActivity(), ShellModule.Host {
146
205
  }
147
206
 
148
207
  override fun onDestroy() {
208
+ contentResolver.unregisterContentObserver(animationScaleObserver)
209
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
210
+ (contrastListener as UiModeManager.ContrastChangeListener?)?.let {
211
+ getSystemService(UiModeManager::class.java).removeContrastChangeListener(it)
212
+ }
213
+ }
214
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) {
215
+ (highContrastTextListener as AccessibilityManager.HighContrastTextStateChangeListener?)?.let {
216
+ getSystemService(AccessibilityManager::class.java).removeHighContrastTextStateChangeListener(it)
217
+ }
218
+ }
149
219
  lynxView.destroy()
150
220
  super.onDestroy()
151
221
  }
@@ -39,6 +39,21 @@ final class LynxViewController: UIViewController {
39
39
  controller.lynxView?.updateGlobalProps(with: controller.globalProps())
40
40
  }
41
41
  }
42
+
43
+ // Send the accessibility settings again when the user changes one while
44
+ // the app is open, so the components adapt at once.
45
+ for name in [
46
+ UIAccessibility.reduceMotionStatusDidChangeNotification,
47
+ UIAccessibility.reduceTransparencyStatusDidChangeNotification,
48
+ UIAccessibility.darkerSystemColorsStatusDidChangeNotification,
49
+ ] {
50
+ NotificationCenter.default.addObserver(
51
+ self, selector: #selector(accessibilitySettingsDidChange), name: name, object: nil)
52
+ }
53
+ }
54
+
55
+ @objc private func accessibilitySettingsDidChange() {
56
+ lynxView?.updateGlobalProps(with: globalProps())
42
57
  }
43
58
 
44
59
  @available(iOS, deprecated: 17.0, message: "registerForTraitChanges in viewDidLoad replaces it")
@@ -80,9 +95,10 @@ final class LynxViewController: UIViewController {
80
95
  }
81
96
 
82
97
  /// 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.
98
+ /// (LynxSafeArea), `appLocale` (LynxLocale), the accessibility settings
99
+ /// (LynxAccessibilitySettings) and `osVersion` (LynxSystemInfo), plus the
100
+ /// safe-area insets. The Android shell sends the same keys, and Lynx
101
+ /// Explorer sends theme and isNotchScreen too.
86
102
  private func globalProps() -> LynxTemplateData {
87
103
  let insets = view.safeAreaInsets
88
104
  var props: [String: Any] = [
@@ -96,6 +112,16 @@ final class LynxViewController: UIViewController {
96
112
  "isNotchScreen": insets.top > 20 || insets.bottom > 0 || insets.left > 0
97
113
  || insets.right > 0,
98
114
  "theme": traitCollection.userInterfaceStyle == .dark ? "Dark" : "Light",
115
+ // Lynx has no API for these. The glass components adapt what they draw
116
+ // to them, and follow Reduce Motion in their own animations.
117
+ "reduceMotion": UIAccessibility.isReduceMotionEnabled,
118
+ "reduceTransparency": UIAccessibility.isReduceTransparencyEnabled,
119
+ // Increase Contrast.
120
+ "increaseContrast": UIAccessibility.isDarkerSystemColorsEnabled,
121
+ // Lynx builds the main thread's SystemInfo, where Angular runs, without
122
+ // the OS version, so LynxSystemInfo reads it from here. The glass
123
+ // components need it: blur-view draws Liquid Glass only from iOS 26.
124
+ "osVersion": UIDevice.current.systemVersion,
99
125
  ]
100
126
  // The user's first language as a BCP 47 tag, such as "fr-FR". iOS
101
127
  // restarts the app when the language changes, so there's no update to send.