@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`,
|
|
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,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)
|
|
116
|
-
*
|
|
117
|
-
*
|
|
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)
|
|
84
|
-
///
|
|
85
|
-
///
|
|
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.
|