@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.
@@ -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
+ }
@@ -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>;
@@ -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
- return {
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
- current = signal(1, /* @ts-ignore */
32
- ...(ngDevMode ? [{ debugName: "current" }] : /* istanbul ignore next */ []));
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
- void present(this.native)
37
- ?.get()
38
- .then((level) => this.current.set(level), () => { });
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 entry = { level: clamped };
51
- this.levels.push(entry);
52
- this.apply(clamped);
58
+ const drop = this.window.levels.claims.push(this.owner, clamped);
59
+ this.window.levels.apply();
53
60
  return () => {
54
- const at = this.levels.indexOf(entry);
55
- if (at === -1)
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
- // Everything set so far is put back, so a release still to come has nothing left to undo.
75
- this.levels.length = 0;
76
- void this.native
77
- ?.restore()
78
- .then(() => this.native?.get().then((level) => this.current.set(level)));
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
@@ -1 +1 @@
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 { InjectionToken, Service, inject, signal, type Signal } from '@angular/core';\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}\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 return {\n get: () => expo.getBrightnessAsync(),\n set: (level) => expo.setBrightnessAsync(level),\n restore: () => expo.restoreSystemBrightnessAsync(),\n };\n },\n },\n );\n\n private readonly native = inject(Brightness.SOURCE);\n private readonly current = signal(1);\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 void present(this.native)\n ?.get()\n .then(\n (level) => this.current.set(level),\n () => {},\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 entry = { level: clamped };\n this.levels.push(entry);\n this.apply(clamped);\n return () => {\n const at = this.levels.indexOf(entry);\n if (at === -1) return;\n this.levels.splice(at, 1);\n if (at < this.levels.length) return;\n const below = this.levels.at(-1);\n if (below) this.apply(below.level);\n else this.restore();\n };\n }\n\n /** Every level set and not yet put back, the one in force last. */\n private readonly levels: { readonly level: number }[] = [];\n\n private apply(level: number): void {\n this.current.set(level);\n void this.native?.set(level);\n }\n\n restore(): void {\n // Everything set so far is put back, so a release still to come has nothing left to undo.\n this.levels.length = 0;\n void this.native\n ?.restore()\n .then(() => this.native?.get().then((level) => this.current.set(level)));\n }\n}\n"]}
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.host ?? inject(HostEngine);
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.host ?? inject(HostEngine));
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 */ []));