@ng-native/expo 0.16.0 → 0.17.0
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/android/src/main/java/dev/ngnative/expo/KeyboardModule.kt +187 -0
- package/android/src/main/java/dev/ngnative/expo/KeyboardWatcher.kt +190 -0
- package/dist/brightness.d.ts +6 -3
- package/dist/brightness.js +55 -33
- package/dist/brightness.js.map +1 -1
- package/dist/expo-ui-components.js +2 -2
- package/dist/expo-ui-components.js.map +1 -1
- package/dist/keep-awake.d.ts +8 -4
- package/dist/keep-awake.js +63 -15
- package/dist/keep-awake.js.map +1 -1
- package/dist/keyboard.d.ts +61 -0
- package/dist/keyboard.js +102 -0
- package/dist/keyboard.js.map +1 -0
- package/dist/language-model.d.ts +1 -2
- package/dist/language-model.js +2 -2
- package/dist/language-model.js.map +1 -1
- package/dist/orientation.d.ts +7 -2
- package/dist/orientation.js +25 -17
- package/dist/orientation.js.map +1 -1
- package/dist/screen-capture.d.ts +16 -3
- package/dist/screen-capture.js +68 -7
- package/dist/screen-capture.js.map +1 -1
- package/dist/splash-screen.js +2 -10
- package/dist/splash-screen.js.map +1 -1
- package/expo-module.config.json +2 -1
- package/ios/NgNativeExpo.podspec +10 -2
- package/package.json +9 -4
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
package dev.ngnative.expo
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.os.Handler
|
|
5
|
+
import android.os.Looper
|
|
6
|
+
import android.view.View
|
|
7
|
+
import android.view.ViewTreeObserver
|
|
8
|
+
import androidx.core.view.ViewCompat
|
|
9
|
+
import androidx.core.view.WindowInsetsCompat
|
|
10
|
+
import expo.modules.kotlin.AppContext
|
|
11
|
+
import expo.modules.kotlin.modules.Module
|
|
12
|
+
import expo.modules.kotlin.modules.ModuleDefinition
|
|
13
|
+
import expo.modules.kotlin.viewevent.EventDispatcher
|
|
14
|
+
import expo.modules.kotlin.views.ExpoView
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The keyboard's motion on Android, which React Native reports only as a `keyboardDidShow` once
|
|
18
|
+
* the window's insets have changed, so a layout that waits for it jumps while the keyboard slides.
|
|
19
|
+
*
|
|
20
|
+
* `onKeyboardChange` is the motion starting: where the keyboard is going and how long it takes,
|
|
21
|
+
* what iOS's `keyboardWillShow` carries. `onKeyboardMove` is where the keyboard is at each frame:
|
|
22
|
+
* before the motion, through it, and `end` after it, for a layout to move with it, since Android
|
|
23
|
+
* animates no layout alongside the keyboard by default. The view,
|
|
24
|
+
* `<keyboard-motion-view>`, reports every frame of the motion as how far the keyboard covers the
|
|
25
|
+
* view's own bottom edge, for a native animation to follow it.
|
|
26
|
+
*/
|
|
27
|
+
class KeyboardModule : Module() {
|
|
28
|
+
private var watcher: KeyboardWatcher? = null
|
|
29
|
+
/** Whether JavaScript is listening: set on its thread, read on the main one. */
|
|
30
|
+
@Volatile private var observing = false
|
|
31
|
+
private val main = Handler(Looper.getMainLooper())
|
|
32
|
+
|
|
33
|
+
private val listener = object : KeyboardWatcher.Listener {
|
|
34
|
+
override fun onKeyboardStart(target: Int, duration: Long) {
|
|
35
|
+
val watcher = watcher ?: return
|
|
36
|
+
// Where it is starting from first, so a layout following the frames is not handed the
|
|
37
|
+
// destination before the motion has begun.
|
|
38
|
+
sendEvent("onKeyboardMove", position(watcher, watcher.inset))
|
|
39
|
+
sendEvent("onKeyboardChange", change(watcher, target, duration))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
override fun onKeyboardProgress(inset: Int) {
|
|
43
|
+
val watcher = watcher ?: return
|
|
44
|
+
sendEvent("onKeyboardMove", position(watcher, inset))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
override fun onKeyboardEnd(inset: Int) {
|
|
48
|
+
sendEvent("onKeyboardMove", mapOf("end" to true))
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
override fun definition() = ModuleDefinition {
|
|
53
|
+
Name("NgNativeKeyboard")
|
|
54
|
+
|
|
55
|
+
Events("onKeyboardChange", "onKeyboardMove")
|
|
56
|
+
|
|
57
|
+
OnStartObserving {
|
|
58
|
+
observing = true
|
|
59
|
+
watchActivity()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
OnStopObserving {
|
|
63
|
+
observing = false
|
|
64
|
+
main.post { unwatch() }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// An activity Android recreates, after the font size changes for one, is a new window while
|
|
68
|
+
// the app's listeners stay: the watcher moves to it rather than keep the old one alive.
|
|
69
|
+
OnActivityEntersForeground {
|
|
70
|
+
if (observing) watchActivity()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
OnActivityDestroys {
|
|
74
|
+
unwatch()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
View(KeyboardMotionView::class) {
|
|
78
|
+
Events("onKeyboardMoveStart", "onKeyboardMove", "onKeyboardMoveEnd")
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Watch the keyboard of the activity in front, once. On the main thread. */
|
|
83
|
+
private fun watchActivity() {
|
|
84
|
+
val activity = appContext.currentActivity ?: return
|
|
85
|
+
activity.runOnUiThread {
|
|
86
|
+
if (watcher == null && observing) {
|
|
87
|
+
watcher = KeyboardWatcher.watch(activity.window.decorView, listener)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private fun unwatch() {
|
|
93
|
+
KeyboardWatcher.unwatch(watcher, listener)
|
|
94
|
+
watcher = null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Where the motion is going and how long it takes. */
|
|
98
|
+
private fun change(watcher: KeyboardWatcher, target: Int, duration: Long): Map<String, Any> =
|
|
99
|
+
position(watcher, target) + mapOf(
|
|
100
|
+
"duration" to duration.toDouble(),
|
|
101
|
+
// Android's own curve decelerates, as `easeOut` does.
|
|
102
|
+
"easing" to "easeOut",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A keyboard `inset` pixels tall as React Native's keyboard events describe one, in points: its
|
|
107
|
+
* height above the system bars, and its top edge on the screen. Zero, and no edge, for none.
|
|
108
|
+
*/
|
|
109
|
+
private fun position(watcher: KeyboardWatcher, inset: Int): Map<String, Any> {
|
|
110
|
+
if (inset == 0) return mapOf("height" to 0.0)
|
|
111
|
+
val density = appContext.reactContext?.resources?.displayMetrics?.density ?: 1f
|
|
112
|
+
val bars = appContext.currentActivity?.window?.decorView?.let { decor ->
|
|
113
|
+
ViewCompat.getRootWindowInsets(decor)?.getInsets(WindowInsetsCompat.Type.systemBars())?.bottom
|
|
114
|
+
} ?: 0
|
|
115
|
+
return mapOf(
|
|
116
|
+
"height" to maxOf(0, inset - bars) / density.toDouble(),
|
|
117
|
+
"screenY" to (watcher.bottomOnScreen - inset) / density.toDouble(),
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* How far the keyboard covers this view's bottom edge, in points, at every frame of its motion.
|
|
124
|
+
*
|
|
125
|
+
* Laid out over the element it reports for, it moves with whatever moves that element: a sheet
|
|
126
|
+
* that react-native-screens raises with the keyboard covers less of it, so what follows the
|
|
127
|
+
* keyboard by this rises only as far as it still must.
|
|
128
|
+
*/
|
|
129
|
+
class KeyboardMotionView(context: Context, appContext: AppContext) :
|
|
130
|
+
ExpoView(context, appContext),
|
|
131
|
+
KeyboardWatcher.Listener {
|
|
132
|
+
private val onKeyboardMoveStart by EventDispatcher()
|
|
133
|
+
private val onKeyboardMove by EventDispatcher()
|
|
134
|
+
private val onKeyboardMoveEnd by EventDispatcher()
|
|
135
|
+
|
|
136
|
+
private var watcher: KeyboardWatcher? = null
|
|
137
|
+
/** The last height reported once the keyboard stopped, so a layout reports only a change. */
|
|
138
|
+
private var settled = -1.0
|
|
139
|
+
|
|
140
|
+
private val laidOut = ViewTreeObserver.OnGlobalLayoutListener {
|
|
141
|
+
val watcher = watcher ?: return@OnGlobalLayoutListener
|
|
142
|
+
if (watcher.moving) return@OnGlobalLayoutListener
|
|
143
|
+
val height = covered(watcher.inset)
|
|
144
|
+
if (height != settled) {
|
|
145
|
+
settled = height
|
|
146
|
+
onKeyboardMoveEnd(mapOf("height" to height))
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
override fun onAttachedToWindow() {
|
|
151
|
+
super.onAttachedToWindow()
|
|
152
|
+
watcher = KeyboardWatcher.watch(this, this)
|
|
153
|
+
viewTreeObserver.addOnGlobalLayoutListener(laidOut)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
override fun onDetachedFromWindow() {
|
|
157
|
+
viewTreeObserver.removeOnGlobalLayoutListener(laidOut)
|
|
158
|
+
KeyboardWatcher.unwatch(watcher, this)
|
|
159
|
+
watcher = null
|
|
160
|
+
settled = -1.0
|
|
161
|
+
super.onDetachedFromWindow()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
override fun onKeyboardStart(target: Int, duration: Long) {
|
|
165
|
+
onKeyboardMoveStart(mapOf("height" to covered(target)))
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
override fun onKeyboardProgress(inset: Int) {
|
|
169
|
+
onKeyboardMove(mapOf("height" to covered(inset)))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
override fun onKeyboardEnd(inset: Int) {
|
|
173
|
+
settled = covered(inset)
|
|
174
|
+
onKeyboardMoveEnd(mapOf("height" to settled))
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** How far a keyboard `inset` pixels tall covers this view's bottom edge, in points. */
|
|
178
|
+
private fun covered(inset: Int): Double {
|
|
179
|
+
val watcher = watcher ?: return 0.0
|
|
180
|
+
if (inset == 0) return 0.0
|
|
181
|
+
val location = IntArray(2)
|
|
182
|
+
getLocationOnScreen(location)
|
|
183
|
+
val bottom = location[1] + height
|
|
184
|
+
val top = watcher.bottomOnScreen - inset
|
|
185
|
+
return maxOf(0, bottom - top) / resources.displayMetrics.density.toDouble()
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
package dev.ngnative.expo
|
|
2
|
+
|
|
3
|
+
import android.os.Build
|
|
4
|
+
import android.view.View
|
|
5
|
+
import android.view.ViewGroup
|
|
6
|
+
import androidx.core.view.ViewCompat
|
|
7
|
+
import androidx.core.view.WindowInsetsAnimationCompat
|
|
8
|
+
import androidx.core.view.WindowInsetsCompat
|
|
9
|
+
import java.util.WeakHashMap
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The keyboard as one window sees it, frame by frame: the inset the keyboard takes from the bottom
|
|
13
|
+
* of the window, in pixels, as Android animates it.
|
|
14
|
+
*
|
|
15
|
+
* Android tells a view about the keyboard's motion through `WindowInsetsAnimationCompat`, but only
|
|
16
|
+
* a view the window dispatches to: react-native-screens stops the dispatch at a sheet's screen, to
|
|
17
|
+
* move the sheet itself. So the callback is on a view of its own, appended to the window's content
|
|
18
|
+
* after the app, which the dispatch reaches past every screen. A window of its own, as a modal is,
|
|
19
|
+
* has a watcher of its own.
|
|
20
|
+
*
|
|
21
|
+
* Nothing here applies or consumes an inset: the app lays itself out as it did without it.
|
|
22
|
+
*/
|
|
23
|
+
internal class KeyboardWatcher private constructor(private val root: ViewGroup) {
|
|
24
|
+
interface Listener {
|
|
25
|
+
/** The keyboard starts to move to `target`, over `duration` milliseconds. */
|
|
26
|
+
fun onKeyboardStart(target: Int, duration: Long)
|
|
27
|
+
/** One frame of the motion. */
|
|
28
|
+
fun onKeyboardProgress(inset: Int)
|
|
29
|
+
/** The keyboard has stopped at `inset`. */
|
|
30
|
+
fun onKeyboardEnd(inset: Int)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private val listeners = LinkedHashSet<Listener>()
|
|
34
|
+
private val probe = View(root.context)
|
|
35
|
+
|
|
36
|
+
/** Where the keyboard is now, as the last frame left it. */
|
|
37
|
+
var inset = keyboardInset(ViewCompat.getRootWindowInsets(root))
|
|
38
|
+
private set
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Set from the moment Android prepares a motion, which comes before the insets it ends at, until
|
|
42
|
+
* its end is reported.
|
|
43
|
+
*/
|
|
44
|
+
var moving = false
|
|
45
|
+
private set
|
|
46
|
+
|
|
47
|
+
/** How many motions Android has prepared, which tells an end whether another has begun. */
|
|
48
|
+
private var motions = 0
|
|
49
|
+
|
|
50
|
+
/** Whether the motion prepared last has started. */
|
|
51
|
+
private var started = false
|
|
52
|
+
|
|
53
|
+
/** The window's bottom edge on the screen, which the keyboard's top is measured up from. */
|
|
54
|
+
val bottomOnScreen: Int
|
|
55
|
+
get() {
|
|
56
|
+
val window = root.rootView
|
|
57
|
+
val location = IntArray(2)
|
|
58
|
+
window.getLocationOnScreen(location)
|
|
59
|
+
return location[1] + window.height
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
init {
|
|
63
|
+
root.addView(probe, ViewGroup.LayoutParams(0, 0))
|
|
64
|
+
ViewCompat.setWindowInsetsAnimationCallback(
|
|
65
|
+
probe,
|
|
66
|
+
object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
|
|
67
|
+
override fun onPrepare(animation: WindowInsetsAnimationCompat) {
|
|
68
|
+
if (!animation.isKeyboard) return
|
|
69
|
+
moving = true
|
|
70
|
+
started = false
|
|
71
|
+
val prepared = ++motions
|
|
72
|
+
// Android 11 sends no end for a motion cancelled before it starts, which would leave
|
|
73
|
+
// `moving` set and every later change ignored until another motion ends. One that has
|
|
74
|
+
// not started by now never will; the keyboard is then wherever the window says.
|
|
75
|
+
// ponytail: a fixed wait, so a first frame slower than it reads as a cancel.
|
|
76
|
+
probe.postDelayed({ if (motions == prepared && !started) settle() }, CANCELLED_MS)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
override fun onStart(
|
|
80
|
+
animation: WindowInsetsAnimationCompat,
|
|
81
|
+
bounds: WindowInsetsAnimationCompat.BoundsCompat,
|
|
82
|
+
): WindowInsetsAnimationCompat.BoundsCompat {
|
|
83
|
+
if (animation.isKeyboard) {
|
|
84
|
+
moving = true
|
|
85
|
+
started = true
|
|
86
|
+
// The window's insets are the ones the motion ends at by now.
|
|
87
|
+
val target = keyboardInset(ViewCompat.getRootWindowInsets(probe))
|
|
88
|
+
listeners.toList().forEach { it.onKeyboardStart(target, animation.durationMillis) }
|
|
89
|
+
}
|
|
90
|
+
return bounds
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
override fun onProgress(
|
|
94
|
+
insets: WindowInsetsCompat,
|
|
95
|
+
running: MutableList<WindowInsetsAnimationCompat>,
|
|
96
|
+
): WindowInsetsCompat {
|
|
97
|
+
if (running.any { it.isKeyboard }) {
|
|
98
|
+
inset = keyboardInset(insets)
|
|
99
|
+
listeners.toList().forEach { it.onKeyboardProgress(inset) }
|
|
100
|
+
}
|
|
101
|
+
return insets
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
override fun onEnd(animation: WindowInsetsAnimationCompat) {
|
|
105
|
+
if (!animation.isKeyboard) return
|
|
106
|
+
// A keyboard that turns back part way ends its motion before the one that replaces it
|
|
107
|
+
// is prepared, in the same call, and by then the window's insets are the cancelled
|
|
108
|
+
// motion's destination. So the end waits a turn, and is dropped when a motion has begun
|
|
109
|
+
// since: reported, it would land what follows the keyboard there for a frame.
|
|
110
|
+
val ending = motions
|
|
111
|
+
probe.post {
|
|
112
|
+
if (motions != ending) return@post
|
|
113
|
+
moving = false
|
|
114
|
+
inset = keyboardInset(ViewCompat.getRootWindowInsets(probe))
|
|
115
|
+
listeners.toList().forEach { it.onKeyboardEnd(inset) }
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
)
|
|
120
|
+
// A keyboard that changes without a motion: with animations turned off, or a height that
|
|
121
|
+
// changes in place. Below Android 11 the compat callback animates every change itself, and
|
|
122
|
+
// owns the view's insets listener to do it.
|
|
123
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
124
|
+
ViewCompat.setOnApplyWindowInsetsListener(probe) { _, insets ->
|
|
125
|
+
if (!moving) jump(keyboardInset(insets))
|
|
126
|
+
insets
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** A motion that will not start is over: report a keyboard it left elsewhere as one jump. */
|
|
132
|
+
private fun settle() {
|
|
133
|
+
moving = false
|
|
134
|
+
jump(keyboardInset(ViewCompat.getRootWindowInsets(probe)))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The keyboard at `now` with no motion, reported as one that starts and ends at once. */
|
|
138
|
+
private fun jump(now: Int) {
|
|
139
|
+
if (now == inset) return
|
|
140
|
+
inset = now
|
|
141
|
+
listeners.toList().forEach {
|
|
142
|
+
it.onKeyboardStart(now, 0)
|
|
143
|
+
it.onKeyboardEnd(now)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private fun add(listener: Listener) {
|
|
148
|
+
listeners.add(listener)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private fun remove(listener: Listener) {
|
|
152
|
+
listeners.remove(listener)
|
|
153
|
+
if (listeners.isNotEmpty()) return
|
|
154
|
+
ViewCompat.setWindowInsetsAnimationCallback(probe, null)
|
|
155
|
+
ViewCompat.setOnApplyWindowInsetsListener(probe, null)
|
|
156
|
+
root.removeView(probe)
|
|
157
|
+
watchers.remove(root)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
companion object {
|
|
161
|
+
/** How long a prepared motion has to start before it counts as cancelled, in ms. */
|
|
162
|
+
private const val CANCELLED_MS = 500L
|
|
163
|
+
|
|
164
|
+
private val watchers = WeakHashMap<ViewGroup, KeyboardWatcher>()
|
|
165
|
+
|
|
166
|
+
/** The window content `view` is in, which its watcher's view joins. */
|
|
167
|
+
private fun rootOf(view: View): ViewGroup? =
|
|
168
|
+
view.rootView.findViewById(android.R.id.content) ?: view.rootView as? ViewGroup
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Start telling `listener` about the keyboard of the window `view` is in. Answers the watcher,
|
|
172
|
+
* or null when the view is in no window. On the main thread.
|
|
173
|
+
*/
|
|
174
|
+
fun watch(view: View, listener: Listener): KeyboardWatcher? {
|
|
175
|
+
val root = rootOf(view) ?: return null
|
|
176
|
+
return watchers.getOrPut(root) { KeyboardWatcher(root) }.also { it.add(listener) }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Stop telling `listener`, and take the watcher's view away with the last one. */
|
|
180
|
+
fun unwatch(watcher: KeyboardWatcher?, listener: Listener) {
|
|
181
|
+
watcher?.remove(listener)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private val WindowInsetsAnimationCompat.isKeyboard: Boolean
|
|
185
|
+
get() = typeMask and WindowInsetsCompat.Type.ime() != 0
|
|
186
|
+
|
|
187
|
+
private fun keyboardInset(insets: WindowInsetsCompat?): Int =
|
|
188
|
+
insets?.getInsets(WindowInsetsCompat.Type.ime())?.bottom ?: 0
|
|
189
|
+
}
|
|
190
|
+
}
|
package/dist/brightness.d.ts
CHANGED
|
@@ -13,12 +13,18 @@ export interface NativeBrightness {
|
|
|
13
13
|
get(): Promise<number>;
|
|
14
14
|
set(level: number): Promise<void>;
|
|
15
15
|
restore(): Promise<void>;
|
|
16
|
+
/** Calls `listener` when a window comes to the front, to be given the level again. */
|
|
17
|
+
onWindowFront?(listener: () => void): () => void;
|
|
16
18
|
}
|
|
17
19
|
export declare class Brightness {
|
|
18
20
|
/** Overridden in a test to watch the level without changing a screen. */
|
|
19
21
|
static readonly SOURCE: InjectionToken<NativeBrightness | null>;
|
|
20
22
|
private readonly native;
|
|
23
|
+
/** Every app's levels: the screen is the runtime's, not an app's. */
|
|
24
|
+
private readonly window;
|
|
21
25
|
private readonly current;
|
|
26
|
+
/** This app, among the apps setting the level. */
|
|
27
|
+
private readonly owner;
|
|
22
28
|
/** Nought to one. One until the platform answers, since that is what a screen already shows. */
|
|
23
29
|
readonly level: Signal<number>;
|
|
24
30
|
constructor();
|
|
@@ -29,9 +35,6 @@ export declare class Brightness {
|
|
|
29
35
|
* after the boarding pass has been shown - which the user experiences as a battery fault.
|
|
30
36
|
*/
|
|
31
37
|
set(level: number): () => void;
|
|
32
|
-
/** Every level set and not yet put back, the one in force last. */
|
|
33
|
-
private readonly levels;
|
|
34
|
-
private apply;
|
|
35
38
|
restore(): void;
|
|
36
39
|
static ɵfac: i0.ɵɵFactoryDeclaration<Brightness, never>;
|
|
37
40
|
static ɵprov: i0.ɵɵInjectableDeclaration<any>;
|
package/dist/brightness.js
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* after it is gone, and needs a permission on Android - a decision to make deliberately against
|
|
8
8
|
* Expo's own API rather than to reach through a convenience here.
|
|
9
9
|
*/
|
|
10
|
-
import { InjectionToken, Service, inject, signal } from '@angular/core';
|
|
10
|
+
import { DestroyRef, InjectionToken, Service, inject, signal, untracked, } from '@angular/core';
|
|
11
|
+
import { TopClaim, onWindowFront, sharedBySource } from '@ng-native/device';
|
|
11
12
|
import { expoModule, present } from "./native.js";
|
|
12
13
|
import * as i0 from "@angular/core";
|
|
13
14
|
export class Brightness {
|
|
@@ -20,22 +21,29 @@ export class Brightness {
|
|
|
20
21
|
// Missing from the build: the stand-in, to say so when it is used.
|
|
21
22
|
if (!present(expo))
|
|
22
23
|
return expo;
|
|
23
|
-
|
|
24
|
+
// One per module, so every app the runtime mounts sets the same screen.
|
|
25
|
+
return sharedBySource(expo, () => ({
|
|
24
26
|
get: () => expo.getBrightnessAsync(),
|
|
25
27
|
set: (level) => expo.setBrightnessAsync(level),
|
|
26
28
|
restore: () => expo.restoreSystemBrightnessAsync(),
|
|
27
|
-
|
|
29
|
+
onWindowFront,
|
|
30
|
+
}));
|
|
28
31
|
},
|
|
29
32
|
});
|
|
30
33
|
native = inject(Brightness.SOURCE);
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
/** Every app's levels: the screen is the runtime's, not an app's. */
|
|
35
|
+
window = sharedBySource(this.native, () => new BrightnessWindow(this.native));
|
|
36
|
+
current = this.window.current;
|
|
37
|
+
/** This app, among the apps setting the level. */
|
|
38
|
+
owner = {};
|
|
33
39
|
/** Nought to one. One until the platform answers, since that is what a screen already shows. */
|
|
34
40
|
level = this.current.asReadonly();
|
|
35
41
|
constructor() {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
.
|
|
42
|
+
inject(DestroyRef).onDestroy(() => {
|
|
43
|
+
// An app that goes takes its levels with it, and leaves the screen to the apps still here.
|
|
44
|
+
this.window.levels.claims.drop(this.owner);
|
|
45
|
+
this.window.levels.apply();
|
|
46
|
+
});
|
|
39
47
|
}
|
|
40
48
|
/**
|
|
41
49
|
* Set it, and return the function that puts it back.
|
|
@@ -47,35 +55,23 @@ export class Brightness {
|
|
|
47
55
|
const clamped = Math.min(1, Math.max(0, level));
|
|
48
56
|
// A stack, as the status bar's is: a screen pushed over one that set a level of its own puts
|
|
49
57
|
// back the level beneath it, not the system's, when it goes.
|
|
50
|
-
const
|
|
51
|
-
this.levels.
|
|
52
|
-
this.apply(clamped);
|
|
58
|
+
const drop = this.window.levels.claims.push(this.owner, clamped);
|
|
59
|
+
this.window.levels.apply();
|
|
53
60
|
return () => {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return;
|
|
57
|
-
this.levels.splice(at, 1);
|
|
58
|
-
if (at < this.levels.length)
|
|
59
|
-
return;
|
|
60
|
-
const below = this.levels.at(-1);
|
|
61
|
-
if (below)
|
|
62
|
-
this.apply(below.level);
|
|
63
|
-
else
|
|
64
|
-
this.restore();
|
|
61
|
+
drop();
|
|
62
|
+
this.window.levels.apply();
|
|
65
63
|
};
|
|
66
64
|
}
|
|
67
|
-
/** Every level set and not yet put back, the one in force last. */
|
|
68
|
-
levels = [];
|
|
69
|
-
apply(level) {
|
|
70
|
-
this.current.set(level);
|
|
71
|
-
void this.native?.set(level);
|
|
72
|
-
}
|
|
73
65
|
restore() {
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
66
|
+
// This app's levels are put back, so a release still to come has nothing left to undo. Another
|
|
67
|
+
// app's stay: the screen is the runtime's.
|
|
68
|
+
this.window.levels.claims.drop(this.owner);
|
|
69
|
+
if (untracked(this.window.levels.claims.all).length > 0) {
|
|
70
|
+
this.window.levels.apply();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
this.window.levels.clear();
|
|
74
|
+
this.window.restore();
|
|
79
75
|
}
|
|
80
76
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: Brightness, deps: [], target: i0.ɵɵFactoryTarget.Service });
|
|
81
77
|
static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.2.0", ngImport: i0, type: Brightness });
|
|
@@ -83,4 +79,30 @@ export class Brightness {
|
|
|
83
79
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: Brightness, decorators: [{
|
|
84
80
|
type: Service
|
|
85
81
|
}], ctorParameters: () => [] });
|
|
82
|
+
/** The screen's level, as every app on one module sets it. */
|
|
83
|
+
class BrightnessWindow {
|
|
84
|
+
current = signal(1, /* @ts-ignore */
|
|
85
|
+
...(ngDevMode ? [{ debugName: "current" }] : /* istanbul ignore next */ []));
|
|
86
|
+
levels;
|
|
87
|
+
native;
|
|
88
|
+
constructor(native) {
|
|
89
|
+
this.native = native;
|
|
90
|
+
this.levels = new TopClaim((level) => {
|
|
91
|
+
this.current.set(level);
|
|
92
|
+
void this.native?.set(level);
|
|
93
|
+
}, () => this.restore(), present(native)?.onWindowFront);
|
|
94
|
+
// What the screen shows, unless a level was set before the platform answered.
|
|
95
|
+
void present(native)
|
|
96
|
+
?.get()
|
|
97
|
+
.then((level) => {
|
|
98
|
+
if (untracked(this.levels.claims.all).length === 0)
|
|
99
|
+
this.current.set(level);
|
|
100
|
+
}, () => { });
|
|
101
|
+
}
|
|
102
|
+
restore() {
|
|
103
|
+
void this.native
|
|
104
|
+
?.restore()
|
|
105
|
+
.then(() => this.native?.get().then((level) => this.current.set(level)));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
86
108
|
//# sourceMappingURL=brightness.js.map
|
package/dist/brightness.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"brightness.js","sourceRoot":"","sources":["../src/brightness.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,
|
|
1
|
+
{"version":3,"file":"brightness.js","sourceRoot":"","sources":["../src/brightness.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `Brightness`, bound to `expo-brightness`.\n *\n * For the screens that have a reason to change it: a boarding pass, a QR code.\n *\n * The app's own brightness only. The system one is a global setting an app can leave changed\n * after it is gone, and needs a permission on Android - a decision to make deliberately against\n * Expo's own API rather than to reach through a convenience here.\n */\nimport {\n DestroyRef,\n InjectionToken,\n Service,\n inject,\n signal,\n type Signal,\n untracked,\n type WritableSignal,\n} from '@angular/core';\nimport { TopClaim, onWindowFront, sharedBySource } from '@ng-native/device';\nimport { expoModule, present } from './native.ts';\n\nexport interface NativeBrightness {\n get(): Promise<number>;\n set(level: number): Promise<void>;\n restore(): Promise<void>;\n /** Calls `listener` when a window comes to the front, to be given the level again. */\n onWindowFront?(listener: () => void): () => void;\n}\n\n@Service()\nexport class Brightness {\n /** Overridden in a test to watch the level without changing a screen. */\n static readonly SOURCE = new InjectionToken<NativeBrightness | null>(\n 'angular-native.brightnessSource',\n {\n factory: () => {\n const expo = expoModule(\n 'expo-brightness',\n () => require('expo-brightness') as typeof import('expo-brightness'),\n );\n if (!expo) return null;\n // Missing from the build: the stand-in, to say so when it is used.\n if (!present(expo)) return expo as never;\n // One per module, so every app the runtime mounts sets the same screen.\n return sharedBySource(expo, () => ({\n get: () => expo.getBrightnessAsync(),\n set: (level) => expo.setBrightnessAsync(level),\n restore: () => expo.restoreSystemBrightnessAsync(),\n onWindowFront,\n }));\n },\n },\n );\n\n private readonly native = inject(Brightness.SOURCE);\n /** Every app's levels: the screen is the runtime's, not an app's. */\n private readonly window = sharedBySource(this.native, () => new BrightnessWindow(this.native));\n private readonly current = this.window.current;\n /** This app, among the apps setting the level. */\n private readonly owner = {};\n\n /** Nought to one. One until the platform answers, since that is what a screen already shows. */\n readonly level: Signal<number> = this.current.asReadonly();\n\n constructor() {\n inject(DestroyRef).onDestroy(() => {\n // An app that goes takes its levels with it, and leaves the screen to the apps still here.\n this.window.levels.claims.drop(this.owner);\n this.window.levels.apply();\n });\n }\n\n /**\n * Set it, and return the function that puts it back.\n *\n * Always paired, because the alternative is an app that leaves the screen at full brightness\n * after the boarding pass has been shown - which the user experiences as a battery fault.\n */\n set(level: number): () => void {\n const clamped = Math.min(1, Math.max(0, level));\n // A stack, as the status bar's is: a screen pushed over one that set a level of its own puts\n // back the level beneath it, not the system's, when it goes.\n const drop = this.window.levels.claims.push(this.owner, clamped);\n this.window.levels.apply();\n return () => {\n drop();\n this.window.levels.apply();\n };\n }\n\n restore(): void {\n // This app's levels are put back, so a release still to come has nothing left to undo. Another\n // app's stay: the screen is the runtime's.\n this.window.levels.claims.drop(this.owner);\n if (untracked(this.window.levels.claims.all).length > 0) {\n this.window.levels.apply();\n return;\n }\n this.window.levels.clear();\n this.window.restore();\n }\n}\n\n/** The screen's level, as every app on one module sets it. */\nclass BrightnessWindow {\n readonly current: WritableSignal<number> = signal(1);\n readonly levels: TopClaim<number>;\n private readonly native: NativeBrightness | null;\n\n constructor(native: NativeBrightness | null) {\n this.native = native;\n this.levels = new TopClaim(\n (level) => {\n this.current.set(level);\n void this.native?.set(level);\n },\n () => this.restore(),\n present(native)?.onWindowFront,\n );\n // What the screen shows, unless a level was set before the platform answered.\n void present(native)\n ?.get()\n .then(\n (level) => {\n if (untracked(this.levels.claims.all).length === 0) this.current.set(level);\n },\n () => {},\n );\n }\n\n restore(): void {\n void this.native\n ?.restore()\n .then(() => this.native?.get().then((level) => this.current.set(level)));\n }\n}\n"]}
|
|
@@ -503,7 +503,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImpor
|
|
|
503
503
|
*/
|
|
504
504
|
function listen(event, handle) {
|
|
505
505
|
const node = inject(ElementRef).nativeElement;
|
|
506
|
-
const engine = node.
|
|
506
|
+
const engine = node.engine ?? inject(HostEngine);
|
|
507
507
|
const stop = engine.setEventListener(node, event, (raw) => handle(raw?.nativeEvent ?? {}));
|
|
508
508
|
inject(DestroyRef).onDestroy(stop);
|
|
509
509
|
}
|
|
@@ -1686,7 +1686,7 @@ const HALF_AND_FULL = ['medium', 'large'];
|
|
|
1686
1686
|
/** The window's width as the engine knows it, or undefined before the platform has said. */
|
|
1687
1687
|
function windowWidth() {
|
|
1688
1688
|
const node = inject(ElementRef).nativeElement;
|
|
1689
|
-
const engine = (node.
|
|
1689
|
+
const engine = (node.engine ?? inject(HostEngine));
|
|
1690
1690
|
const read = () => engine.viewport?.width || undefined;
|
|
1691
1691
|
const width = signal(read(), /* @ts-ignore */
|
|
1692
1692
|
...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
|