rn-system-bar 3.1.7 → 3.2.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/README.md +655 -125
- package/android/src/main/java/com/systembar/SystemBarModule.kt +288 -13
- package/index.ts +23 -2
- package/ios/SystemBarModule.m +13 -6
- package/ios/SystemBarModule.swift +44 -9
- package/lib/index.d.ts +3 -2
- package/lib/index.js +13 -2
- package/lib/specs/NativeSystemBar.d.ts +9 -0
- package/lib/src/SystemBar.d.ts +83 -5
- package/lib/src/SystemBar.js +172 -13
- package/lib/src/types.d.ts +61 -1
- package/lib/src/useSystemBar.d.ts +34 -3
- package/lib/src/useSystemBar.js +96 -6
- package/lib/src/useTheme.d.ts +24 -0
- package/lib/src/useTheme.js +81 -0
- package/package.json +3 -2
- package/specs/NativeSystemBar.ts +21 -6
- package/src/SystemBar.ts +209 -23
- package/src/types.ts +85 -1
- package/src/useSystemBar.ts +234 -26
- package/src/useTheme.ts +95 -0
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
|
|
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,12 +14,13 @@ npm install rn-system-bar
|
|
|
14
14
|
yarn add rn-system-bar
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
**iOS**
|
|
17
|
+
**iOS** — run after install:
|
|
18
|
+
|
|
18
19
|
```bash
|
|
19
20
|
cd ios && pod install
|
|
20
21
|
```
|
|
21
22
|
|
|
22
|
-
**Android
|
|
23
|
+
**Android** — auto-linked, no manual steps required.
|
|
23
24
|
|
|
24
25
|
---
|
|
25
26
|
|
|
@@ -31,12 +32,17 @@ React Native App
|
|
|
31
32
|
▼
|
|
32
33
|
rn-system-bar (TypeScript API + Platform guard)
|
|
33
34
|
│
|
|
34
|
-
├─ Android ──► SystemBarModule.kt
|
|
35
|
-
│
|
|
36
|
-
│
|
|
37
|
-
│
|
|
35
|
+
├─ Android ──► SystemBarModule.kt
|
|
36
|
+
│ ├ WindowInsetsController (API 30+)
|
|
37
|
+
│ ├ AudioManager
|
|
38
|
+
│ ├ ActivityInfo
|
|
39
|
+
│ ├ DisplayManager ← system screencast
|
|
40
|
+
│ └ MediaRouter ← app-only cast
|
|
38
41
|
│
|
|
39
|
-
└─ iOS ──────► SystemBarModule.swift
|
|
42
|
+
└─ iOS ──────► SystemBarModule.swift
|
|
43
|
+
├ UIKit / AVFoundation
|
|
44
|
+
├ UIScreen.screens ← system screencast
|
|
45
|
+
└ (AirPlay = system-managed, no public API)
|
|
40
46
|
```
|
|
41
47
|
|
|
42
48
|
---
|
|
@@ -45,166 +51,705 @@ rn-system-bar (TypeScript API + Platform guard)
|
|
|
45
51
|
|
|
46
52
|
```
|
|
47
53
|
rn-system-bar/
|
|
48
|
-
├ package.json
|
|
49
54
|
├ index.ts
|
|
50
55
|
├ rn-system-bar.podspec
|
|
51
56
|
│
|
|
52
57
|
├ src/
|
|
53
|
-
│ ├ SystemBar.ts
|
|
54
|
-
│
|
|
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
|
|
55
62
|
│
|
|
56
63
|
├ specs/
|
|
57
|
-
│ └ NativeSystemBar.ts
|
|
64
|
+
│ └ NativeSystemBar.ts ← TurboModule spec (New Architecture)
|
|
58
65
|
│
|
|
59
66
|
├ android/
|
|
60
|
-
│ ├ build.gradle
|
|
61
67
|
│ └ src/main/java/com/systembar/
|
|
62
|
-
│ ├ SystemBarModule.kt
|
|
68
|
+
│ ├ SystemBarModule.kt
|
|
63
69
|
│ └ SystemBarPackage.kt
|
|
64
70
|
│
|
|
65
71
|
└ ios/
|
|
66
|
-
├ SystemBarModule.swift
|
|
67
|
-
└ SystemBarModule.m
|
|
72
|
+
├ SystemBarModule.swift
|
|
73
|
+
└ SystemBarModule.m
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 🚀 Quick Start
|
|
79
|
+
|
|
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
|
+
});
|
|
95
|
+
|
|
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
|
+
}
|
|
68
106
|
```
|
|
69
107
|
|
|
70
108
|
---
|
|
71
109
|
|
|
72
|
-
##
|
|
110
|
+
## 🎨 Theme
|
|
111
|
+
|
|
112
|
+
### `useTheme()`
|
|
73
113
|
|
|
74
|
-
|
|
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.
|
|
75
115
|
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
78
136
|
|
|
79
137
|
```ts
|
|
80
|
-
import {
|
|
81
|
-
setNavigationBarColor,
|
|
82
|
-
setNavigationBarVisibility,
|
|
83
|
-
setNavigationBarButtonStyle,
|
|
84
|
-
setNavigationBarStyle,
|
|
85
|
-
setNavigationBarBehavior,
|
|
86
|
-
} from "rn-system-bar";
|
|
138
|
+
import { setGlobalThemeMode } from "rn-system-bar";
|
|
87
139
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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")} />
|
|
93
177
|
```
|
|
94
178
|
|
|
95
179
|
---
|
|
96
180
|
|
|
97
|
-
|
|
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 |
|
|
98
193
|
|
|
99
194
|
```ts
|
|
100
|
-
import {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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";
|
|
105
206
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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.
|
|
225
|
+
|
|
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
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
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 |
|
|
258
|
+
|
|
259
|
+
```ts
|
|
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
|
+
```
|
|
266
|
+
|
|
267
|
+
### `setStatusBarStyle(style)` — Android + iOS
|
|
268
|
+
|
|
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
|
|
109
283
|
```
|
|
110
284
|
|
|
111
285
|
---
|
|
112
286
|
|
|
113
|
-
|
|
287
|
+
## ☀️ Brightness
|
|
288
|
+
|
|
289
|
+
### `setBrightness(level)`
|
|
114
290
|
|
|
115
291
|
```ts
|
|
116
|
-
import { setBrightness
|
|
292
|
+
import { setBrightness } from "rn-system-bar";
|
|
293
|
+
|
|
294
|
+
setBrightness(1.0); // maximum brightness
|
|
295
|
+
setBrightness(0.5); // 50%
|
|
296
|
+
setBrightness(0.01); // minimum (0.0 is clamped to 0.01)
|
|
297
|
+
```
|
|
117
298
|
|
|
118
|
-
|
|
299
|
+
### `getBrightness()` → `Promise<number>`
|
|
119
300
|
|
|
120
|
-
|
|
301
|
+
```ts
|
|
302
|
+
import { getBrightness } from "rn-system-bar";
|
|
303
|
+
|
|
304
|
+
const level = await getBrightness(); // 0.0 – 1.0
|
|
305
|
+
console.log("Brightness:", level);
|
|
306
|
+
```
|
|
307
|
+
|
|
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();
|
|
121
321
|
```
|
|
122
322
|
|
|
123
323
|
---
|
|
124
324
|
|
|
125
|
-
|
|
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.
|
|
126
330
|
|
|
127
331
|
```ts
|
|
128
|
-
import { setVolume
|
|
332
|
+
import { setVolume } from "rn-system-bar";
|
|
129
333
|
|
|
130
|
-
|
|
131
|
-
setVolume(0.5, "
|
|
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
|
+
```
|
|
132
356
|
|
|
133
|
-
|
|
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)
|
|
134
366
|
```
|
|
135
367
|
|
|
136
|
-
|
|
368
|
+
### `onVolumeChange(callback)` → unsubscribe
|
|
369
|
+
|
|
370
|
+
Fires when the volume changes via hardware buttons or another app.
|
|
371
|
+
|
|
372
|
+
```ts
|
|
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
|
+
```
|
|
137
382
|
|
|
138
383
|
---
|
|
139
384
|
|
|
140
|
-
|
|
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";
|
|
393
|
+
|
|
394
|
+
keepScreenOn(true); // prevent sleep
|
|
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.
|
|
141
412
|
|
|
142
413
|
```ts
|
|
143
|
-
import {
|
|
414
|
+
import { setSecureScreen } from "rn-system-bar";
|
|
144
415
|
|
|
145
|
-
|
|
146
|
-
|
|
416
|
+
setSecureScreen(true); // block screenshots and screen recording
|
|
417
|
+
setSecureScreen(false); // allow captures (default)
|
|
147
418
|
```
|
|
148
419
|
|
|
149
420
|
---
|
|
150
421
|
|
|
151
|
-
|
|
422
|
+
## 🔄 Orientation
|
|
423
|
+
|
|
424
|
+
### `setOrientation(mode)` — Android + iOS
|
|
152
425
|
|
|
153
426
|
```ts
|
|
154
427
|
import { setOrientation } from "rn-system-bar";
|
|
155
428
|
|
|
156
|
-
setOrientation("portrait");
|
|
157
|
-
setOrientation("landscape");
|
|
158
|
-
setOrientation("landscape-left");
|
|
159
|
-
setOrientation("landscape-right");
|
|
160
|
-
setOrientation("auto");
|
|
429
|
+
setOrientation("portrait"); // lock portrait
|
|
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
|
|
161
434
|
```
|
|
162
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
|
+
|
|
163
438
|
---
|
|
164
439
|
|
|
165
|
-
##
|
|
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
|
|
166
475
|
|
|
167
476
|
```tsx
|
|
168
|
-
import
|
|
169
|
-
import { View, Button, Platform } from "react-native";
|
|
170
|
-
import * as SystemBar from "rn-system-bar";
|
|
477
|
+
import { useSystemScreencast } from "rn-system-bar";
|
|
171
478
|
|
|
172
|
-
|
|
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.
|
|
173
489
|
|
|
174
|
-
|
|
175
|
-
// Works on both platforms
|
|
176
|
-
SystemBar.setStatusBarStyle("light");
|
|
177
|
-
SystemBar.setBrightness(0.9);
|
|
178
|
-
SystemBar.keepScreenOn(true);
|
|
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.
|
|
179
491
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
+
|
|
609
|
+
---
|
|
610
|
+
|
|
611
|
+
## 🪝 `useSystemBar()` hook
|
|
612
|
+
|
|
613
|
+
Apply multiple system bar settings declaratively in one call. Re-applies only when the config object actually changes (deep comparison via JSON).
|
|
614
|
+
|
|
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
|
+
});
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
All fields are optional. Only the ones you pass will be applied.
|
|
641
|
+
|
|
642
|
+
---
|
|
643
|
+
|
|
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
|
|
684
|
+
|
|
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";
|
|
697
|
+
type NavigationBarBehavior = "overlay-swipe" | "inset-swipe" | "inset-touch";
|
|
698
|
+
type NavigationBarButtonStyle = "light" | "dark";
|
|
699
|
+
type NavigationBarStyle = "auto" | "inverted" | "light" | "dark";
|
|
700
|
+
type NavigationBarVisibility = "visible" | "hidden";
|
|
701
|
+
|
|
702
|
+
// Status bar
|
|
703
|
+
type StatusBarColorValue = string | "transparent" | "translucent";
|
|
704
|
+
type StatusBarStyle = "light" | "dark";
|
|
705
|
+
|
|
706
|
+
// Orientation
|
|
707
|
+
type Orientation =
|
|
708
|
+
| "portrait"
|
|
709
|
+
| "landscape"
|
|
710
|
+
| "landscape-left"
|
|
711
|
+
| "landscape-right"
|
|
712
|
+
| "auto";
|
|
713
|
+
|
|
714
|
+
// Volume
|
|
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
|
+
}
|
|
751
|
+
```
|
|
752
|
+
|
|
208
753
|
---
|
|
209
754
|
|
|
210
755
|
## 🆕 New Architecture (TurboModules)
|
|
@@ -225,43 +770,28 @@ module.exports = {
|
|
|
225
770
|
};
|
|
226
771
|
```
|
|
227
772
|
|
|
228
|
-
The `specs/NativeSystemBar.ts` file is
|
|
773
|
+
The `specs/NativeSystemBar.ts` file is pre-configured for codegen.
|
|
229
774
|
|
|
230
775
|
---
|
|
231
776
|
|
|
232
|
-
##
|
|
777
|
+
## 📋 Changelog
|
|
233
778
|
|
|
234
|
-
|
|
235
|
-
type NavigationBarBehavior = "overlay-swipe" | "inset-swipe" | "inset-touch";
|
|
236
|
-
type NavigationBarButtonStyle = "light" | "dark";
|
|
237
|
-
type NavigationBarStyle = "auto" | "inverted" | "light" | "dark";
|
|
238
|
-
type NavigationBarVisibility = "visible" | "hidden";
|
|
239
|
-
type StatusBarStyle = "light" | "dark";
|
|
240
|
-
type Orientation = "portrait" | "landscape" | "landscape-left" | "landscape-right" | "auto";
|
|
241
|
-
type VolumeStream = "music" | "ring" | "notification" | "alarm" | "system";
|
|
242
|
-
```
|
|
779
|
+
### v3.2 (current)
|
|
243
780
|
|
|
244
|
-
|
|
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()`
|
|
791
|
+
|
|
792
|
+
### v3.1
|
|
245
793
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
| setNavigationBarColor | ✅ | ❌ |
|
|
251
|
-
| setNavigationBarVisibility | ✅ | ❌ |
|
|
252
|
-
| setNavigationBarButtonStyle| ✅ | ❌ |
|
|
253
|
-
| setNavigationBarStyle | ✅ | ❌ |
|
|
254
|
-
| setNavigationBarBehavior | ✅ | ❌ |
|
|
255
|
-
| setStatusBarColor | ✅ | ❌ |
|
|
256
|
-
| setStatusBarStyle | ✅ | ✅ |
|
|
257
|
-
| setStatusBarVisibility | ✅ | ✅ |
|
|
258
|
-
| setBrightness | ✅ | ✅ |
|
|
259
|
-
| getBrightness | ✅ | ✅ |
|
|
260
|
-
| setVolume | ✅ | ❌* |
|
|
261
|
-
| getVolume | ✅ | ✅ |
|
|
262
|
-
| setVolumeHUDVisible | ✅ | ❌ |
|
|
263
|
-
| keepScreenOn | ✅ | ✅ |
|
|
264
|
-
| immersiveMode | ✅ | ❌ |
|
|
265
|
-
| setOrientation | ✅ | ✅ |
|
|
266
|
-
|
|
267
|
-
> *iOS: `setVolume` is a no-op due to Apple restrictions.
|
|
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
|