rn-system-bar 3.1.8 → 3.2.1

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
@@ -1,6 +1,6 @@
1
1
  # rn-system-bar v3
2
2
 
3
- > Control Android & iOS system bars, brightness, volume, orientation and screen flags from React Native.
3
+ > Control Android & iOS system bars, brightness, volume, orientation, theme, and screen casting from React Native.
4
4
 
5
5
  Supports **React Native CLI · Expo Dev Client · TypeScript · New Architecture (TurboModules)**
6
6
 
@@ -14,13 +14,13 @@ npm install rn-system-bar
14
14
  yarn add rn-system-bar
15
15
  ```
16
16
 
17
- **iOS** (after install):
17
+ **iOS** — run after install:
18
18
 
19
19
  ```bash
20
20
  cd ios && pod install
21
21
  ```
22
22
 
23
- **Android**: Auto-linked. No manual steps required.
23
+ **Android** — auto-linked, no manual steps required.
24
24
 
25
25
  ---
26
26
 
@@ -32,12 +32,17 @@ React Native App
32
32
 
33
33
  rn-system-bar (TypeScript API + Platform guard)
34
34
 
35
- ├─ Android ──► SystemBarModule.kt ──► Android SDK APIs
36
- ├ WindowInsetsController (API 30+)
37
- ├ AudioManager
38
- ActivityInfo
35
+ ├─ Android ──► SystemBarModule.kt
36
+ ├ WindowInsetsController (API 30+)
37
+ ├ AudioManager
38
+ ActivityInfo
39
+ │ ├ DisplayManager ← system screencast
40
+ │ └ MediaRouter ← app-only cast
39
41
 
40
- └─ iOS ──────► SystemBarModule.swift ──► UIKit / AVFoundation
42
+ └─ iOS ──────► SystemBarModule.swift
43
+ ├ UIKit / AVFoundation
44
+ ├ UIScreen.screens ← system screencast
45
+ └ (AirPlay = system-managed, no public API)
41
46
  ```
42
47
 
43
48
  ---
@@ -46,263 +51,747 @@ rn-system-bar (TypeScript API + Platform guard)
46
51
 
47
52
  ```
48
53
  rn-system-bar/
49
- ├ package.json
50
54
  ├ index.ts
51
55
  ├ rn-system-bar.podspec
52
56
 
53
57
  ├ src/
54
- │ ├ SystemBar.ts ← JS/TS API (Platform-guarded)
55
- types.ts All TypeScript types
58
+ │ ├ SystemBar.ts all imperative JS/TS APIs
59
+ types.ts all TypeScript types
60
+ │ ├ useSystemBar.ts ← React hooks
61
+ │ └ useTheme.ts ← theme singleton + useTheme hook
56
62
 
57
63
  ├ specs/
58
- │ └ NativeSystemBar.ts ← TurboModule spec (New Architecture)
64
+ │ └ NativeSystemBar.ts ← TurboModule spec (New Architecture)
59
65
 
60
66
  ├ android/
61
- │ ├ build.gradle
62
67
  │ └ src/main/java/com/systembar/
63
- │ ├ SystemBarModule.kt ← Full Android native module
68
+ │ ├ SystemBarModule.kt
64
69
  │ └ SystemBarPackage.kt
65
70
 
66
71
  └ ios/
67
- ├ SystemBarModule.swift ← iOS native module
68
- └ SystemBarModule.m ← ObjC bridge header
72
+ ├ SystemBarModule.swift
73
+ └ SystemBarModule.m
69
74
  ```
70
75
 
71
76
  ---
72
77
 
73
- ## 🚀 API Reference
78
+ ## 🚀 Quick Start
74
79
 
75
- ### Navigation Bar — `Android only`
80
+ ```tsx
81
+ import React, { useEffect } from "react";
82
+ import { View, Button } from "react-native";
83
+ import * as SystemBar from "rn-system-bar";
84
+ import { useSystemBar, useTheme } from "rn-system-bar";
85
+
86
+ export default function App() {
87
+ const { isDark } = useTheme();
88
+
89
+ useSystemBar({
90
+ navigationBarColor: isDark ? "#000000" : "#ffffff",
91
+ navigationBarButtonStyle: isDark ? "light" : "dark",
92
+ statusBarStyle: isDark ? "light" : "dark",
93
+ keepScreenOn: true,
94
+ });
76
95
 
77
- > All `NavigationBar` APIs are automatically guarded with `Platform.OS === "android"`.
78
- > Calling them on iOS prints a dev warning and is a no-op — no crash.
96
+ return (
97
+ <View>
98
+ <Button title="Immersive" onPress={() => SystemBar.immersiveMode(true)} />
99
+ <Button
100
+ title="Landscape"
101
+ onPress={() => SystemBar.setOrientation("landscape")}
102
+ />
103
+ </View>
104
+ );
105
+ }
106
+ ```
107
+
108
+ ---
109
+
110
+ ## 🎨 Theme
111
+
112
+ ### `useTheme()`
113
+
114
+ Reads the OS colour scheme and lets you override it manually. All `useTheme()` consumers across the app share one singleton — changing the mode in one place updates every subscriber instantly.
115
+
116
+ ```tsx
117
+ import { useTheme } from "rn-system-bar";
118
+
119
+ const { isDark, mode, setMode } = useTheme();
120
+
121
+ // isDark → true | false (resolved, always concrete)
122
+ // mode → "system" | "dark" | "light"
123
+ // setMode → (mode: ThemeMode) => void
124
+
125
+ // Follow OS (default)
126
+ setMode("system");
127
+
128
+ // Force dark
129
+ setMode("dark");
130
+
131
+ // Force light
132
+ setMode("light");
133
+ ```
134
+
135
+ ### `setGlobalThemeMode(mode)` — imperative, outside components
79
136
 
80
137
  ```ts
81
- import {
82
- setNavigationBarColor,
83
- setNavigationBarVisibility,
84
- setNavigationBarButtonStyle,
85
- setNavigationBarStyle,
86
- setNavigationBarBehavior,
87
- } from "rn-system-bar";
138
+ import { setGlobalThemeMode } from "rn-system-bar";
88
139
 
89
- setNavigationBarColor("#1a1a2e"); // hex color
90
- setNavigationBarVisibility("hidden"); // "visible" | "hidden"
91
- setNavigationBarButtonStyle("light"); // "light" | "dark"
92
- setNavigationBarStyle("dark"); // "auto" | "inverted" | "light" | "dark"
93
- setNavigationBarBehavior("overlay-swipe"); // "overlay-swipe" | "inset-swipe" | "inset-touch"
140
+ // Call from anywhere — navigation handlers, async callbacks, etc.
141
+ setGlobalThemeMode("dark");
142
+ ```
143
+
144
+ ### `useThemeSystemBar(config)` theme + system bar in one hook
145
+
146
+ Automatically re-applies the correct config whenever the theme changes (OS switch or manual `setMode`). Returns the full `ThemeState` so you can also read `isDark` / `mode` / `setMode`.
147
+
148
+ ```tsx
149
+ import { useThemeSystemBar } from "rn-system-bar";
150
+
151
+ const { isDark, mode, setMode } = useThemeSystemBar({
152
+ // Applied when dark
153
+ dark: {
154
+ navigationBarColor: "#000000",
155
+ navigationBarButtonStyle: "light",
156
+ statusBarColor: "#000000",
157
+ statusBarStyle: "light",
158
+ },
159
+ // Applied when light
160
+ light: {
161
+ navigationBarColor: "#ffffff",
162
+ navigationBarButtonStyle: "dark",
163
+ statusBarColor: "#ffffff",
164
+ statusBarStyle: "dark",
165
+ },
166
+ // Always applied on both themes (theme-specific values win)
167
+ base: {
168
+ keepScreenOn: true,
169
+ orientation: "portrait",
170
+ },
171
+ });
172
+
173
+ // Manual toggle buttons
174
+ <Button title="Dark" onPress={() => setMode("dark")} />
175
+ <Button title="Light" onPress={() => setMode("light")} />
176
+ <Button title="System" onPress={() => setMode("system")} />
94
177
  ```
95
178
 
96
179
  ---
97
180
 
98
- ### Status Bar
181
+ ## 🔵 Navigation Bar — `Android only`
182
+
183
+ > All navigation bar APIs are automatically guarded with `Platform.OS === "android"`.
184
+ > Calling them on iOS prints a dev-only warning and is a no-op — no crash.
185
+
186
+ ### `setNavigationBarColor(color)`
187
+
188
+ | Value | Result |
189
+ | --------------- | ------------------------------------------------------------- |
190
+ | `"#rrggbb"` | Solid colour |
191
+ | `"transparent"` | Fully transparent — content draws edge-to-edge behind the bar |
192
+ | `"translucent"` | Semi-transparent system scrim — frosted-glass effect |
99
193
 
100
194
  ```ts
101
- import {
102
- setStatusBarColor,
103
- setStatusBarStyle,
104
- setStatusBarVisibility,
105
- } from "rn-system-bar";
195
+ import { setNavigationBarColor } from "rn-system-bar";
196
+
197
+ setNavigationBarColor("#1a1a2e"); // solid hex
198
+ setNavigationBarColor("transparent"); // edge-to-edge / glass
199
+ setNavigationBarColor("translucent"); // frosted glass
200
+ ```
201
+
202
+ ### `setNavigationBarVisibility(mode)`
203
+
204
+ ```ts
205
+ import { setNavigationBarVisibility } from "rn-system-bar";
206
+
207
+ setNavigationBarVisibility("hidden"); // hide nav bar
208
+ setNavigationBarVisibility("visible"); // show nav bar
209
+ ```
210
+
211
+ ### `setNavigationBarButtonStyle(style)`
212
+
213
+ Controls the icon colour of the navigation bar buttons (back, home, recents).
214
+
215
+ ```ts
216
+ import { setNavigationBarButtonStyle } from "rn-system-bar";
217
+
218
+ setNavigationBarButtonStyle("light"); // white icons — use on dark backgrounds
219
+ setNavigationBarButtonStyle("dark"); // dark icons — use on light backgrounds
220
+ ```
221
+
222
+ ### `setNavigationBarStyle(style)`
223
+
224
+ Higher-level wrapper that maps to button style — `"dark"` / `"auto"` → dark icons; `"light"` / `"inverted"` → light icons.
106
225
 
107
- setStatusBarColor("#000000"); // Android only (iOS ignores bg color)
108
- setStatusBarStyle("light"); // "light" | "dark" — works on both platforms
109
- setStatusBarVisibility(false); // hide status bar
226
+ ```ts
227
+ import { setNavigationBarStyle } from "rn-system-bar";
228
+
229
+ setNavigationBarStyle("dark"); // dark icons
230
+ setNavigationBarStyle("light"); // light icons
231
+ setNavigationBarStyle("auto"); // follows system
232
+ setNavigationBarStyle("inverted"); // opposite of system
233
+ ```
234
+
235
+ ### `setNavigationBarBehavior(behavior)`
236
+
237
+ Controls how hidden bars re-appear after a swipe gesture.
238
+
239
+ ```ts
240
+ import { setNavigationBarBehavior } from "rn-system-bar";
241
+
242
+ setNavigationBarBehavior("overlay-swipe"); // bar appears temporarily on swipe (API 30+)
243
+ setNavigationBarBehavior("inset-swipe"); // bar pushes content on swipe
244
+ setNavigationBarBehavior("inset-touch"); // bar pushes content on touch
110
245
  ```
111
246
 
112
247
  ---
113
248
 
114
- ### Brightness
249
+ ## 🔴 Status Bar
250
+
251
+ ### `setStatusBarColor(color)` — Android only
252
+
253
+ | Value | Result |
254
+ | --------------- | -------------------------------------------------- |
255
+ | `"#rrggbb"` | Solid colour |
256
+ | `"transparent"` | Fully transparent — content draws under status bar |
257
+ | `"translucent"` | Semi-transparent system scrim |
115
258
 
116
259
  ```ts
117
- import { setBrightness, getBrightness } from "rn-system-bar";
260
+ import { setStatusBarColor } from "rn-system-bar";
261
+
262
+ setStatusBarColor("#000000"); // solid black
263
+ setStatusBarColor("transparent"); // content visible behind status bar
264
+ setStatusBarColor("translucent"); // frosted glass
265
+ ```
118
266
 
119
- setBrightness(0.8); // 0.0 1.0
267
+ ### `setStatusBarStyle(style)` Android + iOS
120
268
 
121
- const level = await getBrightness(); // returns 0.0 – 1.0
269
+ ```ts
270
+ import { setStatusBarStyle } from "rn-system-bar";
271
+
272
+ setStatusBarStyle("light"); // white text/icons — use on dark backgrounds
273
+ setStatusBarStyle("dark"); // dark text/icons — use on light backgrounds
274
+ ```
275
+
276
+ ### `setStatusBarVisibility(visible)` — Android + iOS
277
+
278
+ ```ts
279
+ import { setStatusBarVisibility } from "rn-system-bar";
280
+
281
+ setStatusBarVisibility(false); // hide status bar (fullscreen)
282
+ setStatusBarVisibility(true); // show status bar
122
283
  ```
123
284
 
124
285
  ---
125
286
 
126
- ### Volume
287
+ ## ☀️ Brightness
288
+
289
+ ### `setBrightness(level)`
127
290
 
128
291
  ```ts
129
- import { setVolume, getVolume, setVolumeHUDVisible } from "rn-system-bar";
292
+ import { setBrightness } from "rn-system-bar";
130
293
 
131
- setVolumeHUDVisible(false); // hide the system volume popup (Android)
132
- setVolume(0.5, "music"); // 0.0 – 1.0, stream: "music" | "ring" | "notification" | "alarm" | "system"
294
+ setBrightness(1.0); // maximum brightness
295
+ setBrightness(0.5); // 50%
296
+ setBrightness(0.01); // minimum (0.0 is clamped to 0.01)
297
+ ```
298
+
299
+ ### `getBrightness()` → `Promise<number>`
300
+
301
+ ```ts
302
+ import { getBrightness } from "rn-system-bar";
133
303
 
134
- const vol = await getVolume("music"); // returns 0.0 – 1.0
304
+ const level = await getBrightness(); // 0.0 – 1.0
305
+ console.log("Brightness:", level);
135
306
  ```
136
307
 
137
- > **iOS note**: `setVolume()` is a no-op on iOS (Apple restriction). `getVolume()` works.
308
+ ### `onBrightnessChange(callback)` unsubscribe
309
+
310
+ Fires on every brightness change. On Android, polls every 500 ms since `Settings.System` doesn't broadcast changes.
311
+
312
+ ```ts
313
+ import { onBrightnessChange } from "rn-system-bar";
314
+
315
+ const unsub = onBrightnessChange((brightness) => {
316
+ console.log("New brightness:", brightness); // 0.0 – 1.0
317
+ });
318
+
319
+ // Stop listening
320
+ unsub();
321
+ ```
138
322
 
139
323
  ---
140
324
 
141
- ### Screen
325
+ ## 🔊 Volume
326
+
327
+ ### `setVolume(level, stream?)`
328
+
329
+ > **iOS note:** `setVolume()` is a no-op on iOS due to Apple restrictions. `getVolume()` works normally.
330
+
331
+ ```ts
332
+ import { setVolume } from "rn-system-bar";
333
+
334
+ setVolume(0.8); // 80% — defaults to "music" stream
335
+ setVolume(0.5, "ring"); // 50% ring volume
336
+ setVolume(1.0, "alarm"); // max alarm volume
337
+ setVolume(0.3, "notification");
338
+ setVolume(0.6, "system");
339
+ ```
340
+
341
+ | Stream | Description |
342
+ | ---------------- | ----------------------- |
343
+ | `"music"` | Media / music (default) |
344
+ | `"ring"` | Ringtone |
345
+ | `"notification"` | Notifications |
346
+ | `"alarm"` | Alarms |
347
+ | `"system"` | UI sounds |
348
+
349
+ ### `getVolume(stream?)` → `Promise<number>`
350
+
351
+ ```ts
352
+ import { getVolume } from "rn-system-bar";
353
+
354
+ const vol = await getVolume("music"); // 0.0 – 1.0
355
+ ```
356
+
357
+ ### `setVolumeHUDVisible(visible)` — Android only
358
+
359
+ Show or hide the system volume popup when `setVolume` is called.
360
+
361
+ ```ts
362
+ import { setVolumeHUDVisible } from "rn-system-bar";
363
+
364
+ setVolumeHUDVisible(false); // silent volume change — no popup
365
+ setVolumeHUDVisible(true); // show the popup (default behaviour)
366
+ ```
367
+
368
+ ### `onVolumeChange(callback)` → unsubscribe
369
+
370
+ Fires when the volume changes via hardware buttons or another app.
142
371
 
143
372
  ```ts
144
- import { keepScreenOn, immersiveMode } from "rn-system-bar";
373
+ import { onVolumeChange } from "rn-system-bar";
374
+
375
+ const unsub = onVolumeChange((volume, stream) => {
376
+ console.log(`${stream} volume: ${volume}`); // e.g. "music volume: 0.7"
377
+ });
378
+
379
+ // Stop listening
380
+ unsub();
381
+ ```
382
+
383
+ ---
384
+
385
+ ## 📺 Screen
386
+
387
+ ### `keepScreenOn(enable)` — Android + iOS
388
+
389
+ Prevents the screen from sleeping while the app is in the foreground.
390
+
391
+ ```ts
392
+ import { keepScreenOn } from "rn-system-bar";
145
393
 
146
394
  keepScreenOn(true); // prevent sleep
147
- immersiveMode(true); // hide status + nav bar (Android only)
395
+ keepScreenOn(false); // allow sleep (default)
396
+ ```
397
+
398
+ ### `immersiveMode(enable)` — Android only
399
+
400
+ Hides both status bar and navigation bar. Bars temporarily reappear on swipe.
401
+
402
+ ```ts
403
+ import { immersiveMode } from "rn-system-bar";
404
+
405
+ immersiveMode(true); // full immersive — both bars hidden
406
+ immersiveMode(false); // restore both bars
407
+ ```
408
+
409
+ ### `setSecureScreen(enable)` — Android only
410
+
411
+ Prevents the screen from being captured via screenshots or screen recording. Applies `FLAG_SECURE` to the window.
412
+
413
+ ```ts
414
+ import { setSecureScreen } from "rn-system-bar";
415
+
416
+ setSecureScreen(true); // block screenshots and screen recording
417
+ setSecureScreen(false); // allow captures (default)
148
418
  ```
149
419
 
150
420
  ---
151
421
 
152
- ### Orientation
422
+ ## 🔄 Orientation
423
+
424
+ ### `setOrientation(mode)` — Android + iOS
153
425
 
154
426
  ```ts
155
427
  import { setOrientation } from "rn-system-bar";
156
428
 
157
429
  setOrientation("portrait"); // lock portrait
158
- setOrientation("landscape"); // lock landscape
159
- setOrientation("landscape-left"); // specific side
160
- setOrientation("landscape-right");
161
- setOrientation("auto"); // unlock — sensor-driven
430
+ setOrientation("landscape"); // lock landscape (either side)
431
+ setOrientation("landscape-left"); // lock landscape — specific side
432
+ setOrientation("landscape-right"); // lock landscape — specific side
433
+ setOrientation("auto"); // unlock — follows device sensor
162
434
  ```
163
435
 
436
+ > **Android note:** When the device's auto-rotate toggle is **ON**, `"portrait"` / `"landscape"` use sensor-based variants so the device can still rotate within the axis. When auto-rotate is **OFF**, orientation is hard-locked.
437
+
164
438
  ---
165
439
 
166
- ## 📱 Full Example
440
+ ## 📡 System Screencast
441
+
442
+ Detects OS-level external display state — HDMI, Miracast (Android), AirPlay mirror (iOS). This is the **system** casting state, not your app specifically.
443
+
444
+ ### `getSystemScreencastInfo()` → `Promise<SystemScreencastInfo>`
445
+
446
+ ```ts
447
+ import { getSystemScreencastInfo } from "rn-system-bar";
448
+
449
+ const info = await getSystemScreencastInfo();
450
+ // {
451
+ // isCasting: true,
452
+ // displayName: "Living Room TV",
453
+ // displays: [{ id: 1, name: "Living Room TV", isValid: true }]
454
+ // }
455
+ ```
456
+
457
+ ### `onSystemScreencastChange(callback)` → unsubscribe
458
+
459
+ ```ts
460
+ import { onSystemScreencastChange } from "rn-system-bar";
461
+
462
+ const unsub = onSystemScreencastChange((info) => {
463
+ if (info.isCasting) {
464
+ console.log("Now casting to:", info.displayName);
465
+ } else {
466
+ console.log("Casting stopped");
467
+ }
468
+ });
469
+
470
+ // Stop listening
471
+ unsub();
472
+ ```
473
+
474
+ ### `useSystemScreencast()` hook
167
475
 
168
476
  ```tsx
169
- import React, { useEffect } from "react";
170
- import { View, Button, Platform } from "react-native";
171
- import * as SystemBar from "rn-system-bar";
477
+ import { useSystemScreencast } from "rn-system-bar";
172
478
 
173
- export default function App() {
174
- useEffect(() => {
175
- // Works on both platforms
176
- SystemBar.setStatusBarStyle("light");
177
- SystemBar.setBrightness(0.9);
178
- SystemBar.keepScreenOn(true);
179
-
180
- // Android-only (auto-guarded safe to call without Platform check)
181
- SystemBar.setNavigationBarColor("#000000");
182
- SystemBar.setNavigationBarButtonStyle("light");
183
- SystemBar.setNavigationBarBehavior("overlay-swipe");
184
- }, []);
479
+ const { isCasting, displayName, displays } = useSystemScreencast();
480
+
481
+ return <Text>{isCasting ? `Casting to ${displayName}` : "Not casting"}</Text>;
482
+ ```
483
+
484
+ ---
485
+
486
+ ## 📲 App-Only Cast (Android)
487
+
488
+ Cast **only this app's screen** to a nearby TV or Chromecast — the whole system is not mirrored. Uses Android `MediaRouter` internally, no Google Play Services required.
489
+
490
+ > **iOS:** AirPlay is fully system-managed on iOS. Apple provides no public API for app-only casting. All `AppCast` functions are safe to call on iOS — they are no-ops that return idle state immediately.
491
+
492
+ ### State machine
493
+
494
+ ```
495
+ idle → scanning → connecting → connected → disconnecting → idle
496
+ ↕ error at any point
497
+ ```
498
+
499
+ ### `useAppCast()` hook — recommended
500
+
501
+ The easiest way to manage casting. Handles initial state fetch, event subscription, and auto-stops the scan on unmount (battery-safe).
502
+
503
+ ```tsx
504
+ import { useAppCast } from "rn-system-bar";
505
+
506
+ export default function CastScreen() {
507
+ const {
508
+ state, // "idle" | "scanning" | "connecting" | "connected" | "disconnecting"
509
+ devices, // AppCastDevice[] — found nearby devices
510
+ connectedDevice, // AppCastDevice | null — currently active
511
+ error, // string | null — last error message
512
+ scan, // () => void — start discovery
513
+ stopScan, // () => void — stop discovery
514
+ connect, // (deviceId: string, pin?: string) => void
515
+ disconnect, // () => void
516
+ } = useAppCast();
185
517
 
186
518
  return (
187
519
  <View>
188
- <Button
189
- title="Immersive Mode"
190
- onPress={() => SystemBar.immersiveMode(true)}
191
- />
192
- <Button
193
- title="Landscape"
194
- onPress={() => SystemBar.setOrientation("landscape")}
195
- />
196
- <Button
197
- title="Get Brightness"
198
- onPress={async () => {
199
- const b = await SystemBar.getBrightness();
200
- console.log("Brightness:", b);
201
- }}
202
- />
520
+ {state === "idle" && <Button title="Find TVs" onPress={scan} />}
521
+
522
+ {state === "scanning" && (
523
+ <>
524
+ <Text>Scanning…</Text>
525
+ <Button title="Stop" onPress={stopScan} />
526
+ </>
527
+ )}
528
+
529
+ {devices.map((device) => (
530
+ <View key={device.id}>
531
+ <Text>{device.name}</Text>
532
+ {device.description && <Text>{device.description}</Text>}
533
+ <Button
534
+ title={`Cast to ${device.name}`}
535
+ onPress={() => connect(device.id)}
536
+ />
537
+ </View>
538
+ ))}
539
+
540
+ {state === "connecting" && (
541
+ <Text>Connecting to {connectedDevice?.name}…</Text>
542
+ )}
543
+
544
+ {state === "connected" && (
545
+ <>
546
+ <Text>Casting to {connectedDevice?.name}</Text>
547
+ <Button title="Stop Casting" onPress={disconnect} />
548
+ </>
549
+ )}
550
+
551
+ {error && <Text style={{ color: "red" }}>Error: {error}</Text>}
203
552
  </View>
204
553
  );
205
554
  }
206
555
  ```
207
556
 
557
+ ### With pairing PIN
558
+
559
+ ```tsx
560
+ const { devices, connect } = useAppCast();
561
+
562
+ // If requiresPairing is true, pass the PIN as the second argument
563
+ connect(device.id, "1234");
564
+ ```
565
+
566
+ ### Imperative API (without hook)
567
+
568
+ ```ts
569
+ import {
570
+ startAppCastScan,
571
+ stopAppCastScan,
572
+ connectAppCast,
573
+ disconnectAppCast,
574
+ getAppCastInfo,
575
+ onAppCastChange,
576
+ } from "rn-system-bar";
577
+
578
+ // Subscribe first
579
+ const unsub = onAppCastChange((info) => {
580
+ console.log("Cast state:", info.state);
581
+ console.log("Devices:", info.devices);
582
+ if (info.state === "connected") {
583
+ console.log("Casting to:", info.connectedDevice?.name);
584
+ }
585
+ if (info.error) console.warn("Error:", info.error);
586
+ });
587
+
588
+ // Start scan
589
+ startAppCastScan();
590
+
591
+ // Connect when a device appears
592
+ connectAppCast("device_route_id_123");
593
+ // With pairing PIN
594
+ connectAppCast("device_route_id_123", "9876");
595
+
596
+ // Stop casting
597
+ disconnectAppCast();
598
+
599
+ // Stop scanning
600
+ stopAppCastScan();
601
+
602
+ // Get current state without subscribing
603
+ const info = await getAppCastInfo();
604
+
605
+ // Clean up
606
+ unsub();
607
+ ```
608
+
208
609
  ---
209
610
 
210
- ## 🆕 New Architecture (TurboModules)
611
+ ## 🪝 `useSystemBar()` hook
211
612
 
212
- Add to your `react-native.config.js`:
613
+ Apply multiple system bar settings declaratively in one call. Re-applies only when the config object actually changes (deep comparison via JSON).
213
614
 
214
- ```js
215
- module.exports = {
216
- dependencies: {
217
- "rn-system-bar": {
218
- platforms: {
219
- android: {
220
- packageImportPath: "import com.systembar.SystemBarPackage;",
221
- },
222
- },
223
- },
224
- },
225
- };
615
+ ```tsx
616
+ import { useSystemBar } from "rn-system-bar";
617
+
618
+ useSystemBar({
619
+ // Navigation bar (Android-only — safe to include on iOS)
620
+ navigationBarColor: "transparent",
621
+ navigationBarVisibility: "visible",
622
+ navigationBarButtonStyle: "light",
623
+ navigationBarStyle: "dark",
624
+ navigationBarBehavior: "overlay-swipe",
625
+
626
+ // Status bar
627
+ statusBarColor: "transparent", // Android only
628
+ statusBarStyle: "light",
629
+ statusBarVisible: true,
630
+
631
+ // Screen
632
+ keepScreenOn: true,
633
+ immersiveMode: false, // Android only
634
+ secureScreen: false, // Android only
635
+ brightness: 0.8,
636
+ orientation: "portrait",
637
+ });
226
638
  ```
227
639
 
228
- The `specs/NativeSystemBar.ts` file is already configured for codegen via `codegenConfig` in `package.json`.
640
+ All fields are optional. Only the ones you pass will be applied.
229
641
 
230
642
  ---
231
643
 
232
- ## 🔖 Type Reference
644
+ ## 📱 Platform Support Matrix
645
+
646
+ | Feature | Android | iOS |
647
+ | ----------------------------- | :-----: | :---: |
648
+ | `setNavigationBarColor` | ✅ | ❌ |
649
+ | `setNavigationBarVisibility` | ✅ | ❌ |
650
+ | `setNavigationBarButtonStyle` | ✅ | ❌ |
651
+ | `setNavigationBarStyle` | ✅ | ❌ |
652
+ | `setNavigationBarBehavior` | ✅ | ❌ |
653
+ | `setStatusBarColor` | ✅ | ❌ |
654
+ | `setStatusBarStyle` | ✅ | ✅ |
655
+ | `setStatusBarVisibility` | ✅ | ✅ |
656
+ | `setBrightness` | ✅ | ✅ |
657
+ | `getBrightness` | ✅ | ✅ |
658
+ | `onBrightnessChange` | ✅ | ✅ |
659
+ | `setVolume` | ✅ | ❌ \* |
660
+ | `getVolume` | ✅ | ✅ |
661
+ | `setVolumeHUDVisible` | ✅ | ❌ |
662
+ | `onVolumeChange` | ✅ | ❌ |
663
+ | `keepScreenOn` | ✅ | ✅ |
664
+ | `immersiveMode` | ✅ | ❌ |
665
+ | `setSecureScreen` | ✅ | ❌ |
666
+ | `setOrientation` | ✅ | ✅ |
667
+ | `getSystemScreencastInfo` | ✅ | ✅ |
668
+ | `onSystemScreencastChange` | ✅ | ✅ |
669
+ | `useSystemScreencast` | ✅ | ✅ |
670
+ | `startAppCastScan` | ✅ | ❌ † |
671
+ | `connectAppCast` | ✅ | ❌ † |
672
+ | `disconnectAppCast` | ✅ | ❌ † |
673
+ | `useAppCast` | ✅ | ❌ † |
674
+ | `useTheme` | ✅ | ✅ |
675
+ | `useThemeSystemBar` | ✅ | ✅ |
676
+ | `useSystemBar` | ✅ | ✅ |
677
+
678
+ > \* `setVolume` is a no-op on iOS — Apple does not allow apps to set system volume.
679
+ > † App-only cast APIs are no-ops on iOS — AirPlay is system-managed with no public API.
680
+
681
+ ---
682
+
683
+ ## 📐 Full Type Reference
233
684
 
234
685
  ```ts
686
+ // Theme
687
+ type ThemeMode = "system" | "dark" | "light";
688
+
689
+ interface ThemeState {
690
+ isDark: boolean;
691
+ mode: ThemeMode;
692
+ setMode: (mode: ThemeMode) => void;
693
+ }
694
+
695
+ // Navigation bar
696
+ type NavigationBarColorValue = string | "transparent" | "translucent";
235
697
  type NavigationBarBehavior = "overlay-swipe" | "inset-swipe" | "inset-touch";
236
698
  type NavigationBarButtonStyle = "light" | "dark";
237
699
  type NavigationBarStyle = "auto" | "inverted" | "light" | "dark";
238
700
  type NavigationBarVisibility = "visible" | "hidden";
701
+
702
+ // Status bar
703
+ type StatusBarColorValue = string | "transparent" | "translucent";
239
704
  type StatusBarStyle = "light" | "dark";
705
+
706
+ // Orientation
240
707
  type Orientation =
241
708
  | "portrait"
242
709
  | "landscape"
243
710
  | "landscape-left"
244
711
  | "landscape-right"
245
712
  | "auto";
713
+
714
+ // Volume
246
715
  type VolumeStream = "music" | "ring" | "notification" | "alarm" | "system";
716
+
717
+ // System screencast
718
+ interface ScreencastDisplay {
719
+ id: number;
720
+ name: string;
721
+ isValid: boolean;
722
+ }
723
+ interface SystemScreencastInfo {
724
+ isCasting: boolean;
725
+ displayName: string | null;
726
+ displays: ScreencastDisplay[];
727
+ }
728
+
729
+ // App-only cast
730
+ type AppCastState =
731
+ | "idle"
732
+ | "scanning"
733
+ | "connecting"
734
+ | "connected"
735
+ | "disconnecting";
736
+
737
+ interface AppCastDevice {
738
+ id: string;
739
+ name: string;
740
+ description: string | null;
741
+ signalStrength: number | null; // 0–100 or null
742
+ requiresPairing: boolean;
743
+ }
744
+
745
+ interface AppCastInfo {
746
+ state: AppCastState;
747
+ devices: AppCastDevice[];
748
+ connectedDevice: AppCastDevice | null;
749
+ error: string | null;
750
+ }
247
751
  ```
248
752
 
249
753
  ---
250
754
 
251
- ## 📋 Platform Support Matrix
252
-
253
- | Feature | Android | iOS |
254
- | --------------------------- | :-----: | :--: |
255
- | setNavigationBarColor | ✅ | ❌ |
256
- | setNavigationBarVisibility | ✅ | ❌ |
257
- | setNavigationBarButtonStyle | ✅ | ❌ |
258
- | setNavigationBarStyle | ✅ | ❌ |
259
- | setNavigationBarBehavior | ✅ | ❌ |
260
- | setStatusBarColor | ✅ | ❌ |
261
- | setStatusBarStyle | ✅ | ✅ |
262
- | setStatusBarVisibility | ✅ | ✅ |
263
- | setBrightness | ✅ | ✅ |
264
- | getBrightness | ✅ | ✅ |
265
- | setVolume | ✅ | ❌\* |
266
- | getVolume | ✅ | ✅ |
267
- | setVolumeHUDVisible | ✅ | ❌ |
268
- | keepScreenOn | ✅ | ✅ |
269
- | immersiveMode | ✅ | ❌ |
270
- | setOrientation | ✅ | ✅ |
271
-
272
- > \*iOS: `setVolume` is a no-op due to Apple restrictions.
273
-
274
- // useEffect(() => {
275
- // checkAuth();
276
- // }, []);
277
-
278
- // const checkAuth = async () => {
279
- // const token = await getAccessToken();
280
-
281
- // if (token) {
282
- // router.replace("/home");
283
- // } else {
284
- // router.replace("/login");
285
- // }
286
- // };
755
+ ## 🆕 New Architecture (TurboModules)
756
+
757
+ Add to your `react-native.config.js`:
758
+
759
+ ```js
760
+ module.exports = {
761
+ dependencies: {
762
+ "rn-system-bar": {
763
+ platforms: {
764
+ android: {
765
+ packageImportPath: "import com.systembar.SystemBarPackage;",
766
+ },
767
+ },
768
+ },
769
+ },
770
+ };
771
+ ```
772
+
773
+ The `specs/NativeSystemBar.ts` file is pre-configured for codegen.
774
+
775
+ ---
287
776
 
288
- # Use Sheet
777
+ ## 📋 Changelog
289
778
 
290
- <Button
291
- title="Open Sheet"
292
- variant="neutral"
293
- rounded={bw(80)}
294
- style={{ paddingVertical: hp(1) }}
295
- onPress={() => sheetRef.current?.open()}
296
- />
779
+ ### v3.2 (current)
297
780
 
298
- <BottomSheet
299
- ref={sheetRef}
300
- style={{
301
- snapPoints: [0.4, 0.7, 0.9],
302
- borderRadius: ["15"],
303
- }}
781
+ - **New:** `setNavigationBarColor("transparent" | "translucent")` — edge-to-edge and frosted-glass nav bar
782
+ - **New:** `setStatusBarColor("transparent" | "translucent")` — same for status bar
783
+ - **New:** `useTheme()` — reactive OS dark/light mode with manual override (`"system"` | `"dark"` | `"light"`)
784
+ - **New:** `setGlobalThemeMode()` — imperative theme setter, usable outside components
785
+ - **New:** `useThemeSystemBar()` — auto-applies themed bar config on every theme change
786
+ - **New:** `getSystemScreencastInfo()` / `onSystemScreencastChange()` / `useSystemScreencast()` — OS-level external display detection
787
+ - **New:** `startAppCastScan()` / `connectAppCast()` / `disconnectAppCast()` / `useAppCast()` — in-app MediaRouter casting to TV/Chromecast (Android)
788
+ - **Deprecated:** `getScreencastInfo()` → use `getSystemScreencastInfo()`
789
+ - **Deprecated:** `onScreencastChange()` → use `onSystemScreencastChange()`
790
+ - **Deprecated:** `useScreencast()` → use `useSystemScreencast()`
304
791
 
305
- >
792
+ ### v3.1
306
793
 
307
- <Text>BottomSheet Open</Text>
308
- </BottomSheet>
794
+ - Removed `expo-navigation-bar` dependency — all navigation bar APIs now go directly to native Kotlin/Swift
795
+ - Added `onBrightnessChange()` realtime listener
796
+ - Added `onVolumeChange()` realtime listener
797
+ - Added `setSecureScreen()` for screenshot blocking
@@ -590,49 +590,75 @@ class SystemBarModule(
590
590
  appCastState = "scanning"
591
591
  discoveredRoutes.clear()
592
592
 
593
- val cb = object : android.media.MediaRouter.Callback() {
594
- override fun onRouteAdded(router: android.media.MediaRouter, route: android.media.MediaRouter.RouteInfo) {
595
- // Skip the default phone speaker / earpiece
596
- if (route.isDefault) return
597
- val id = routeId(route)
598
- discoveredRoutes[id] = route
599
- emitAppCastChange()
600
- }
601
- override fun onRouteRemoved(router: android.media.MediaRouter, route: android.media.MediaRouter.RouteInfo) {
602
- discoveredRoutes.remove(routeId(route))
603
- if (connectedRoute?.let { routeId(it) } == routeId(route)) {
604
- connectedRoute = null
605
- appCastState = "idle"
606
- }
607
- emitAppCastChange()
608
- }
609
- override fun onRouteChanged(router: android.media.MediaRouter, route: android.media.MediaRouter.RouteInfo) {
610
- val id = routeId(route)
611
- if (!route.isDefault) discoveredRoutes[id] = route
612
- emitAppCastChange()
613
- }
614
- override fun onRouteSelected(router: android.media.MediaRouter, type: Int, route: android.media.MediaRouter.RouteInfo) {
615
- if (route.isDefault) return
616
- connectedRoute = route
617
- appCastState = "connected"
618
- emitAppCastChange()
619
- }
620
- override fun onRouteUnselected(router: android.media.MediaRouter, type: Int, route: android.media.MediaRouter.RouteInfo) {
621
- if (appCastState != "idle") {
622
- connectedRoute = null
623
- appCastState = "idle"
624
- emitAppCastChange()
625
- }
626
- }
593
+ val cb = object : android.media.MediaRouter.Callback() {
594
+
595
+ private fun android.media.MediaRouter.RouteInfo.isDefaultRoute(): Boolean =
596
+ (supportedTypes and android.media.MediaRouter.ROUTE_TYPE_LIVE_AUDIO) != 0 &&
597
+ name?.toString()?.lowercase()?.contains("phone") == true
598
+
599
+ override fun onRouteAdded(router: android.media.MediaRouter, route: android.media.MediaRouter.RouteInfo) {
600
+ if (route.isDefaultRoute()) return
601
+ val id = routeId(route)
602
+ discoveredRoutes[id] = route
603
+ emitAppCastChange()
604
+ }
605
+
606
+ override fun onRouteRemoved(router: android.media.MediaRouter, route: android.media.MediaRouter.RouteInfo) {
607
+ discoveredRoutes.remove(routeId(route))
608
+ if (connectedRoute?.let { routeId(it) } == routeId(route)) {
609
+ connectedRoute = null
610
+ appCastState = "idle"
611
+ }
612
+ emitAppCastChange()
613
+ }
614
+
615
+ override fun onRouteChanged(router: android.media.MediaRouter, route: android.media.MediaRouter.RouteInfo) {
616
+ val id = routeId(route)
617
+ if (!route.isDefaultRoute()) discoveredRoutes[id] = route
618
+ emitAppCastChange()
619
+ }
620
+
621
+ override fun onRouteSelected(router: android.media.MediaRouter, type: Int, route: android.media.MediaRouter.RouteInfo) {
622
+ if (route.isDefaultRoute()) return
623
+ connectedRoute = route
624
+ appCastState = "connected"
625
+ emitAppCastChange()
626
+ }
627
+
628
+ override fun onRouteUnselected(router: android.media.MediaRouter, type: Int, route: android.media.MediaRouter.RouteInfo) {
629
+ if (appCastState != "idle") {
630
+ connectedRoute = null
631
+ appCastState = "idle"
632
+ emitAppCastChange()
627
633
  }
634
+ }
628
635
 
636
+ // ── Required abstract stubs ──────────────────────────────────────────
637
+ override fun onRouteGrouped(
638
+ router: android.media.MediaRouter,
639
+ route: android.media.MediaRouter.RouteInfo,
640
+ group: android.media.MediaRouter.RouteGroup,
641
+ index: Int
642
+ ) {}
643
+
644
+ override fun onRouteUngrouped(
645
+ router: android.media.MediaRouter,
646
+ route: android.media.MediaRouter.RouteInfo,
647
+ group: android.media.MediaRouter.RouteGroup
648
+ ) {}
649
+
650
+ override fun onRouteVolumeChanged(
651
+ router: android.media.MediaRouter,
652
+ route: android.media.MediaRouter.RouteInfo
653
+ ) {}
654
+ }
629
655
  appCastCallback = cb
630
656
  mainHandler.post {
631
657
  mediaRouter.addCallback(
632
658
  android.media.MediaRouter.ROUTE_TYPE_LIVE_VIDEO,
633
659
  cb,
634
- android.media.MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY or
635
- android.media.MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN
660
+ android.media.MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY.toInt() or
661
+ android.media.MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN.toInt()
636
662
  )
637
663
  }
638
664
  emitAppCastChange()
@@ -17,9 +17,11 @@ const androidOnly = (name) => {
17
17
  }
18
18
  return true;
19
19
  };
20
- const checkNative = () => {
20
+ const checkNative = (method) => {
21
21
  if (!Native)
22
22
  throw new Error("[rn-system-bar] Native module not found. Rebuild your project.");
23
+ if (method && typeof Native[method] !== "function")
24
+ throw new Error(`[rn-system-bar] Native.${method} not found. Rebuild your project.`);
23
25
  };
24
26
  // ═══════════════════════════════════════════════
25
27
  // NAVIGATION BAR (Android — 100% native)
@@ -207,7 +209,15 @@ exports.setOrientation = setOrientation;
207
209
  * Works on both Android (DisplayManager) and iOS (UIScreen.screens).
208
210
  */
209
211
  const getSystemScreencastInfo = () => {
210
- checkNative();
212
+ if (!Native || typeof Native.getSystemScreencastInfo !== "function") {
213
+ if (__DEV__)
214
+ console.warn("[rn-system-bar] getSystemScreencastInfo not available on this build.");
215
+ return Promise.resolve({
216
+ isCasting: false,
217
+ displayName: null,
218
+ displays: [],
219
+ });
220
+ }
211
221
  return Native.getSystemScreencastInfo();
212
222
  };
213
223
  exports.getSystemScreencastInfo = getSystemScreencastInfo;
@@ -1,4 +1,4 @@
1
- import type { AppCastInfo, NavigationBarBehavior, NavigationBarButtonStyle, NavigationBarColorValue, NavigationBarStyle, NavigationBarVisibility, Orientation, StatusBarColorValue, StatusBarStyle, SystemScreencastInfo, ThemeMode, ThemeState } from "./types";
1
+ import type { AppCastInfo, NavigationBarBehavior, NavigationBarButtonStyle, NavigationBarColorValue, NavigationBarStyle, NavigationBarVisibility, Orientation, ScreencastInfo, StatusBarColorValue, StatusBarStyle, SystemScreencastInfo, ThemeMode, ThemeState } from "./types";
2
2
  export interface SystemBarConfig {
3
3
  navigationBarColor?: NavigationBarColorValue;
4
4
  navigationBarVisibility?: NavigationBarVisibility;
@@ -32,7 +32,7 @@ export { useTheme } from "./useTheme";
32
32
  export type { ThemeMode, ThemeState };
33
33
  export declare const useSystemScreencast: () => SystemScreencastInfo;
34
34
  /** @deprecated Renamed to useSystemScreencast() */
35
- export declare const useScreencast: () => SystemScreencastInfo;
35
+ export declare const useScreencast: () => ScreencastInfo;
36
36
  export interface UseAppCastReturn extends AppCastInfo {
37
37
  /** Start scanning for nearby castable devices (TV, Chromecast, etc.). */
38
38
  scan: () => void;
@@ -134,7 +134,33 @@ const useSystemScreencast = () => {
134
134
  };
135
135
  exports.useSystemScreencast = useSystemScreencast;
136
136
  /** @deprecated Renamed to useSystemScreencast() */
137
- exports.useScreencast = exports.useSystemScreencast;
137
+ // useSystemBar.ts useScreencast
138
+ const useScreencast = () => {
139
+ const [info, setInfo] = (0, react_1.useState)({
140
+ isCasting: false,
141
+ displayName: null,
142
+ displays: [],
143
+ });
144
+ (0, react_1.useEffect)(() => {
145
+ // Guard: if method doesn't exist on native module, bail silently
146
+ SystemBar.getScreencastInfo()
147
+ .then(setInfo)
148
+ .catch(() => {
149
+ if (__DEV__)
150
+ console.warn("[rn-system-bar] getSystemScreencastInfo not available. Rebuild native.");
151
+ });
152
+ let unsub;
153
+ try {
154
+ unsub = SystemBar.onScreencastChange(setInfo);
155
+ }
156
+ catch (_a) {
157
+ // native listener not available
158
+ }
159
+ return () => unsub === null || unsub === void 0 ? void 0 : unsub();
160
+ }, []);
161
+ return info;
162
+ };
163
+ exports.useScreencast = useScreencast;
138
164
  const useAppCast = () => {
139
165
  const [info, setInfo] = (0, react_1.useState)({
140
166
  state: "idle",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rn-system-bar",
3
- "version": "3.1.8",
3
+ "version": "3.2.1",
4
4
  "description": "Control Android & iOS system bars, brightness, volume, orientation and screen flags from React Native.",
5
5
  "main": "lib/index.js",
6
6
  "react-native": "lib/index.js",
package/src/SystemBar.ts CHANGED
@@ -15,7 +15,6 @@ import type {
15
15
  NavigationBarStyle,
16
16
  NavigationBarVisibility,
17
17
  Orientation,
18
- ScreencastInfo,
19
18
  StatusBarColorValue,
20
19
  StatusBarStyle,
21
20
  SystemScreencastInfo,
@@ -35,11 +34,15 @@ const androidOnly = (name: string): boolean => {
35
34
  return true;
36
35
  };
37
36
 
38
- const checkNative = () => {
37
+ const checkNative = (method?: string) => {
39
38
  if (!Native)
40
39
  throw new Error(
41
40
  "[rn-system-bar] Native module not found. Rebuild your project.",
42
41
  );
42
+ if (method && typeof Native[method] !== "function")
43
+ throw new Error(
44
+ `[rn-system-bar] Native.${method} not found. Rebuild your project.`,
45
+ );
43
46
  };
44
47
 
45
48
  // ═══════════════════════════════════════════════
@@ -246,7 +249,17 @@ export const setOrientation = (mode: Orientation): void => {
246
249
  * Works on both Android (DisplayManager) and iOS (UIScreen.screens).
247
250
  */
248
251
  export const getSystemScreencastInfo = (): Promise<SystemScreencastInfo> => {
249
- checkNative();
252
+ if (!Native || typeof Native.getSystemScreencastInfo !== "function") {
253
+ if (__DEV__)
254
+ console.warn(
255
+ "[rn-system-bar] getSystemScreencastInfo not available on this build.",
256
+ );
257
+ return Promise.resolve({
258
+ isCasting: false,
259
+ displayName: null,
260
+ displays: [],
261
+ });
262
+ }
250
263
  return Native.getSystemScreencastInfo();
251
264
  };
252
265
 
@@ -12,6 +12,7 @@ import type {
12
12
  NavigationBarStyle,
13
13
  NavigationBarVisibility,
14
14
  Orientation,
15
+ ScreencastInfo,
15
16
  StatusBarColorValue,
16
17
  StatusBarStyle,
17
18
  SystemScreencastInfo,
@@ -183,7 +184,36 @@ export const useSystemScreencast = (): SystemScreencastInfo => {
183
184
  };
184
185
 
185
186
  /** @deprecated Renamed to useSystemScreencast() */
186
- export const useScreencast = useSystemScreencast;
187
+ // useSystemBar.ts useScreencast
188
+ export const useScreencast = () => {
189
+ const [info, setInfo] = useState<ScreencastInfo>({
190
+ isCasting: false,
191
+ displayName: null,
192
+ displays: [],
193
+ });
194
+
195
+ useEffect(() => {
196
+ // Guard: if method doesn't exist on native module, bail silently
197
+ SystemBar.getScreencastInfo()
198
+ .then(setInfo)
199
+ .catch(() => {
200
+ if (__DEV__)
201
+ console.warn(
202
+ "[rn-system-bar] getSystemScreencastInfo not available. Rebuild native.",
203
+ );
204
+ });
205
+
206
+ let unsub: (() => void) | undefined;
207
+ try {
208
+ unsub = SystemBar.onScreencastChange(setInfo);
209
+ } catch {
210
+ // native listener not available
211
+ }
212
+ return () => unsub?.();
213
+ }, []);
214
+
215
+ return info;
216
+ };
187
217
 
188
218
  // ─────────────────────────────────────────────
189
219
  // UseAppCastReturn + useAppCast