rn-system-bar 3.1.7 → 3.1.8
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 +81 -40
- 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 +161 -12
- package/lib/src/types.d.ts +61 -1
- package/lib/src/useSystemBar.d.ts +35 -4
- package/lib/src/useSystemBar.js +71 -7
- 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 +194 -21
- package/src/types.ts +85 -1
- package/src/useSystemBar.ts +206 -28
- package/src/useTheme.ts +95 -0
package/README.md
CHANGED
|
@@ -15,6 +15,7 @@ yarn add rn-system-bar
|
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
**iOS** (after install):
|
|
18
|
+
|
|
18
19
|
```bash
|
|
19
20
|
cd ios && pod install
|
|
20
21
|
```
|
|
@@ -85,10 +86,10 @@ import {
|
|
|
85
86
|
setNavigationBarBehavior,
|
|
86
87
|
} from "rn-system-bar";
|
|
87
88
|
|
|
88
|
-
setNavigationBarColor("#1a1a2e");
|
|
89
|
-
setNavigationBarVisibility("hidden");
|
|
90
|
-
setNavigationBarButtonStyle("light");
|
|
91
|
-
setNavigationBarStyle("dark");
|
|
89
|
+
setNavigationBarColor("#1a1a2e"); // hex color
|
|
90
|
+
setNavigationBarVisibility("hidden"); // "visible" | "hidden"
|
|
91
|
+
setNavigationBarButtonStyle("light"); // "light" | "dark"
|
|
92
|
+
setNavigationBarStyle("dark"); // "auto" | "inverted" | "light" | "dark"
|
|
92
93
|
setNavigationBarBehavior("overlay-swipe"); // "overlay-swipe" | "inset-swipe" | "inset-touch"
|
|
93
94
|
```
|
|
94
95
|
|
|
@@ -103,9 +104,9 @@ import {
|
|
|
103
104
|
setStatusBarVisibility,
|
|
104
105
|
} from "rn-system-bar";
|
|
105
106
|
|
|
106
|
-
setStatusBarColor("#000000");
|
|
107
|
-
setStatusBarStyle("light");
|
|
108
|
-
setStatusBarVisibility(false);
|
|
107
|
+
setStatusBarColor("#000000"); // Android only (iOS ignores bg color)
|
|
108
|
+
setStatusBarStyle("light"); // "light" | "dark" — works on both platforms
|
|
109
|
+
setStatusBarVisibility(false); // hide status bar
|
|
109
110
|
```
|
|
110
111
|
|
|
111
112
|
---
|
|
@@ -115,9 +116,9 @@ setStatusBarVisibility(false); // hide status bar
|
|
|
115
116
|
```ts
|
|
116
117
|
import { setBrightness, getBrightness } from "rn-system-bar";
|
|
117
118
|
|
|
118
|
-
setBrightness(0.8);
|
|
119
|
+
setBrightness(0.8); // 0.0 – 1.0
|
|
119
120
|
|
|
120
|
-
const level = await getBrightness();
|
|
121
|
+
const level = await getBrightness(); // returns 0.0 – 1.0
|
|
121
122
|
```
|
|
122
123
|
|
|
123
124
|
---
|
|
@@ -127,10 +128,10 @@ const level = await getBrightness(); // returns 0.0 – 1.0
|
|
|
127
128
|
```ts
|
|
128
129
|
import { setVolume, getVolume, setVolumeHUDVisible } from "rn-system-bar";
|
|
129
130
|
|
|
130
|
-
setVolumeHUDVisible(false);
|
|
131
|
-
setVolume(0.5, "music");
|
|
131
|
+
setVolumeHUDVisible(false); // hide the system volume popup (Android)
|
|
132
|
+
setVolume(0.5, "music"); // 0.0 – 1.0, stream: "music" | "ring" | "notification" | "alarm" | "system"
|
|
132
133
|
|
|
133
|
-
const vol = await getVolume("music");
|
|
134
|
+
const vol = await getVolume("music"); // returns 0.0 – 1.0
|
|
134
135
|
```
|
|
135
136
|
|
|
136
137
|
> **iOS note**: `setVolume()` is a no-op on iOS (Apple restriction). `getVolume()` works.
|
|
@@ -142,8 +143,8 @@ const vol = await getVolume("music"); // returns 0.0 – 1.0
|
|
|
142
143
|
```ts
|
|
143
144
|
import { keepScreenOn, immersiveMode } from "rn-system-bar";
|
|
144
145
|
|
|
145
|
-
keepScreenOn(true);
|
|
146
|
-
immersiveMode(true);
|
|
146
|
+
keepScreenOn(true); // prevent sleep
|
|
147
|
+
immersiveMode(true); // hide status + nav bar (Android only)
|
|
147
148
|
```
|
|
148
149
|
|
|
149
150
|
---
|
|
@@ -153,11 +154,11 @@ immersiveMode(true); // hide status + nav bar (Android o
|
|
|
153
154
|
```ts
|
|
154
155
|
import { setOrientation } from "rn-system-bar";
|
|
155
156
|
|
|
156
|
-
setOrientation("portrait");
|
|
157
|
-
setOrientation("landscape");
|
|
158
|
-
setOrientation("landscape-left");
|
|
157
|
+
setOrientation("portrait"); // lock portrait
|
|
158
|
+
setOrientation("landscape"); // lock landscape
|
|
159
|
+
setOrientation("landscape-left"); // specific side
|
|
159
160
|
setOrientation("landscape-right");
|
|
160
|
-
setOrientation("auto");
|
|
161
|
+
setOrientation("auto"); // unlock — sensor-driven
|
|
161
162
|
```
|
|
162
163
|
|
|
163
164
|
---
|
|
@@ -170,7 +171,6 @@ import { View, Button, Platform } from "react-native";
|
|
|
170
171
|
import * as SystemBar from "rn-system-bar";
|
|
171
172
|
|
|
172
173
|
export default function App() {
|
|
173
|
-
|
|
174
174
|
useEffect(() => {
|
|
175
175
|
// Works on both platforms
|
|
176
176
|
SystemBar.setStatusBarStyle("light");
|
|
@@ -237,7 +237,12 @@ type NavigationBarButtonStyle = "light" | "dark";
|
|
|
237
237
|
type NavigationBarStyle = "auto" | "inverted" | "light" | "dark";
|
|
238
238
|
type NavigationBarVisibility = "visible" | "hidden";
|
|
239
239
|
type StatusBarStyle = "light" | "dark";
|
|
240
|
-
type Orientation =
|
|
240
|
+
type Orientation =
|
|
241
|
+
| "portrait"
|
|
242
|
+
| "landscape"
|
|
243
|
+
| "landscape-left"
|
|
244
|
+
| "landscape-right"
|
|
245
|
+
| "auto";
|
|
241
246
|
type VolumeStream = "music" | "ring" | "notification" | "alarm" | "system";
|
|
242
247
|
```
|
|
243
248
|
|
|
@@ -245,23 +250,59 @@ type VolumeStream = "music" | "ring" | "notification" | "alarm" | "system";
|
|
|
245
250
|
|
|
246
251
|
## 📋 Platform Support Matrix
|
|
247
252
|
|
|
248
|
-
| Feature
|
|
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
|
-
>
|
|
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
|
+
// };
|
|
287
|
+
|
|
288
|
+
# Use Sheet
|
|
289
|
+
|
|
290
|
+
<Button
|
|
291
|
+
title="Open Sheet"
|
|
292
|
+
variant="neutral"
|
|
293
|
+
rounded={bw(80)}
|
|
294
|
+
style={{ paddingVertical: hp(1) }}
|
|
295
|
+
onPress={() => sheetRef.current?.open()}
|
|
296
|
+
/>
|
|
297
|
+
|
|
298
|
+
<BottomSheet
|
|
299
|
+
ref={sheetRef}
|
|
300
|
+
style={{
|
|
301
|
+
snapPoints: [0.4, 0.7, 0.9],
|
|
302
|
+
borderRadius: ["15"],
|
|
303
|
+
}}
|
|
304
|
+
|
|
305
|
+
>
|
|
306
|
+
|
|
307
|
+
<Text>BottomSheet Open</Text>
|
|
308
|
+
</BottomSheet>
|
|
@@ -56,11 +56,60 @@ class SystemBarModule(
|
|
|
56
56
|
// NAVIGATION BAR — 100% native, no expo dep
|
|
57
57
|
// ═══════════════════════════════════════════════
|
|
58
58
|
|
|
59
|
+
// ── Helpers for transparent / translucent bars ──────────────────────────
|
|
60
|
+
|
|
61
|
+
private fun applyEdgeToEdgeFlags() {
|
|
62
|
+
// Required so our colour / transparency actually shows through
|
|
63
|
+
activity()!!.window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private fun setNavBarTransparent() {
|
|
67
|
+
val win = activity()!!.window
|
|
68
|
+
applyEdgeToEdgeFlags()
|
|
69
|
+
win.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
|
|
70
|
+
win.navigationBarColor = Color.TRANSPARENT
|
|
71
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
72
|
+
win.setDecorFitsSystemWindows(false)
|
|
73
|
+
} else {
|
|
74
|
+
@Suppress("DEPRECATION")
|
|
75
|
+
win.decorView.systemUiVisibility =
|
|
76
|
+
win.decorView.systemUiVisibility or
|
|
77
|
+
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
|
78
|
+
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private fun setNavBarTranslucent() {
|
|
83
|
+
val win = activity()!!.window
|
|
84
|
+
win.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
|
|
85
|
+
win.clearFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private fun setNavBarSolid(color: String) {
|
|
89
|
+
val win = activity()!!.window
|
|
90
|
+
applyEdgeToEdgeFlags()
|
|
91
|
+
win.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
|
|
92
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
93
|
+
win.setDecorFitsSystemWindows(true)
|
|
94
|
+
} else {
|
|
95
|
+
@Suppress("DEPRECATION")
|
|
96
|
+
win.decorView.systemUiVisibility =
|
|
97
|
+
win.decorView.systemUiVisibility and
|
|
98
|
+
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION.inv() and
|
|
99
|
+
View.SYSTEM_UI_FLAG_LAYOUT_STABLE.inv()
|
|
100
|
+
}
|
|
101
|
+
try { win.navigationBarColor = Color.parseColor(color) }
|
|
102
|
+
catch (_: Exception) {}
|
|
103
|
+
}
|
|
104
|
+
|
|
59
105
|
@ReactMethod
|
|
60
106
|
fun setNavigationBarColor(color: String) {
|
|
61
107
|
activity()?.runOnUiThread {
|
|
62
|
-
|
|
63
|
-
|
|
108
|
+
when (color.trim().lowercase()) {
|
|
109
|
+
"transparent" -> setNavBarTransparent()
|
|
110
|
+
"translucent" -> setNavBarTranslucent()
|
|
111
|
+
else -> setNavBarSolid(color)
|
|
112
|
+
}
|
|
64
113
|
}
|
|
65
114
|
}
|
|
66
115
|
|
|
@@ -147,6 +196,47 @@ class SystemBarModule(
|
|
|
147
196
|
}
|
|
148
197
|
}
|
|
149
198
|
|
|
199
|
+
@ReactMethod
|
|
200
|
+
fun setStatusBarColor(color: String) {
|
|
201
|
+
activity()?.runOnUiThread {
|
|
202
|
+
val win = activity()!!.window
|
|
203
|
+
when (color.trim().lowercase()) {
|
|
204
|
+
"transparent" -> {
|
|
205
|
+
applyEdgeToEdgeFlags()
|
|
206
|
+
win.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
|
|
207
|
+
win.statusBarColor = Color.TRANSPARENT
|
|
208
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
209
|
+
win.setDecorFitsSystemWindows(false)
|
|
210
|
+
} else {
|
|
211
|
+
@Suppress("DEPRECATION")
|
|
212
|
+
win.decorView.systemUiVisibility =
|
|
213
|
+
win.decorView.systemUiVisibility or
|
|
214
|
+
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
|
|
215
|
+
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
"translucent" -> {
|
|
219
|
+
win.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
|
|
220
|
+
win.clearFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
|
|
221
|
+
}
|
|
222
|
+
else -> {
|
|
223
|
+
applyEdgeToEdgeFlags()
|
|
224
|
+
win.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
|
|
225
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
226
|
+
win.setDecorFitsSystemWindows(true)
|
|
227
|
+
} else {
|
|
228
|
+
@Suppress("DEPRECATION")
|
|
229
|
+
win.decorView.systemUiVisibility =
|
|
230
|
+
win.decorView.systemUiVisibility and
|
|
231
|
+
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN.inv()
|
|
232
|
+
}
|
|
233
|
+
try { win.statusBarColor = Color.parseColor(color) }
|
|
234
|
+
catch (_: Exception) {}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
150
240
|
@ReactMethod
|
|
151
241
|
fun setStatusBarVisibility(visible: Boolean) {
|
|
152
242
|
activity()?.runOnUiThread {
|
|
@@ -379,12 +469,13 @@ class SystemBarModule(
|
|
|
379
469
|
}
|
|
380
470
|
|
|
381
471
|
// ═══════════════════════════════════════════════
|
|
382
|
-
// SCREENCAST
|
|
472
|
+
// SYSTEM SCREENCAST (external display / HDMI / Miracast)
|
|
473
|
+
// Detects OS-level external displays via DisplayManager.
|
|
383
474
|
// ═══════════════════════════════════════════════
|
|
384
475
|
|
|
385
476
|
private var displayListener: DisplayManager.DisplayListener? = null
|
|
386
477
|
|
|
387
|
-
private fun
|
|
478
|
+
private fun buildSystemDisplayMap(): WritableMap {
|
|
388
479
|
val dm = displayManager()
|
|
389
480
|
val map = Arguments.createMap()
|
|
390
481
|
val displays = dm.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION)
|
|
@@ -404,33 +495,217 @@ class SystemBarModule(
|
|
|
404
495
|
}
|
|
405
496
|
|
|
406
497
|
@ReactMethod
|
|
407
|
-
fun
|
|
408
|
-
try { promise.resolve(
|
|
498
|
+
fun getSystemScreencastInfo(promise: Promise) {
|
|
499
|
+
try { promise.resolve(buildSystemDisplayMap()) }
|
|
409
500
|
catch (e: Exception) { promise.reject("SCREENCAST_ERROR", e.message, e) }
|
|
410
501
|
}
|
|
411
502
|
|
|
412
503
|
@ReactMethod
|
|
413
|
-
fun
|
|
504
|
+
fun startSystemScreencastListener() {
|
|
414
505
|
if (displayListener != null) return
|
|
415
506
|
val listener = object : DisplayManager.DisplayListener {
|
|
416
|
-
override fun onDisplayAdded(id: Int) { emit("
|
|
417
|
-
override fun onDisplayRemoved(id: Int) { emit("
|
|
418
|
-
override fun onDisplayChanged(id: Int) { emit("
|
|
507
|
+
override fun onDisplayAdded(id: Int) { emit("SystemBar_SystemScreencastChange", buildSystemDisplayMap()) }
|
|
508
|
+
override fun onDisplayRemoved(id: Int) { emit("SystemBar_SystemScreencastChange", buildSystemDisplayMap()) }
|
|
509
|
+
override fun onDisplayChanged(id: Int) { emit("SystemBar_SystemScreencastChange", buildSystemDisplayMap()) }
|
|
419
510
|
}
|
|
420
511
|
displayListener = listener
|
|
421
512
|
displayManager().registerDisplayListener(listener, null)
|
|
422
513
|
}
|
|
423
514
|
|
|
424
515
|
@ReactMethod
|
|
425
|
-
fun
|
|
516
|
+
fun stopSystemScreencastListener() {
|
|
426
517
|
displayListener?.let { displayManager().unregisterDisplayListener(it) }
|
|
427
518
|
displayListener = null
|
|
428
519
|
}
|
|
429
520
|
|
|
521
|
+
// ═══════════════════════════════════════════════
|
|
522
|
+
// APP-ONLY CAST (MediaRouter — Chromecast / TV)
|
|
523
|
+
//
|
|
524
|
+
// Casts ONLY this app's screen — not the whole system.
|
|
525
|
+
// Uses android.media.MediaRouter (API 16+, no Play Services required).
|
|
526
|
+
//
|
|
527
|
+
// Flow:
|
|
528
|
+
// startAppCastScan() → devices arrive via SystemBar_AppCastChange
|
|
529
|
+
// connectAppCast(deviceId) → state = "connecting" then "connected"
|
|
530
|
+
// disconnectAppCast() → state = "disconnecting" then "idle"
|
|
531
|
+
// ═══════════════════════════════════════════════
|
|
532
|
+
|
|
533
|
+
private val mediaRouter: android.media.MediaRouter by lazy {
|
|
534
|
+
reactContext.getSystemService(Context.MEDIA_ROUTER_SERVICE) as android.media.MediaRouter
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Current state
|
|
538
|
+
private var appCastState = "idle"
|
|
539
|
+
private var connectedRoute: android.media.MediaRouter.RouteInfo? = null
|
|
540
|
+
private var appCastCallback: android.media.MediaRouter.Callback? = null
|
|
541
|
+
|
|
542
|
+
// All routes discovered during the current scan (id → route)
|
|
543
|
+
private val discoveredRoutes = mutableMapOf<String, android.media.MediaRouter.RouteInfo>()
|
|
544
|
+
|
|
545
|
+
// ── Serialisation helpers ──────────────────
|
|
546
|
+
|
|
547
|
+
private fun routeId(r: android.media.MediaRouter.RouteInfo): String =
|
|
548
|
+
r.name?.toString()?.replace(" ", "_")?.plus("_${r.hashCode()}") ?: r.hashCode().toString()
|
|
549
|
+
|
|
550
|
+
private fun routeToMap(r: android.media.MediaRouter.RouteInfo): WritableMap {
|
|
551
|
+
val m = Arguments.createMap()
|
|
552
|
+
m.putString("id", routeId(r))
|
|
553
|
+
m.putString("name", r.name?.toString() ?: "Unknown")
|
|
554
|
+
val desc = r.description?.toString()
|
|
555
|
+
if (desc != null) m.putString("description", desc) else m.putNull("description")
|
|
556
|
+
// MediaRouter doesn't expose signal strength — set null
|
|
557
|
+
m.putNull("signalStrength")
|
|
558
|
+
// Consider a device to require pairing when its status matches "connecting"
|
|
559
|
+
m.putBoolean("requiresPairing", false)
|
|
560
|
+
return m
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
private fun buildAppCastMap(): WritableMap {
|
|
564
|
+
val map = Arguments.createMap()
|
|
565
|
+
map.putString("state", appCastState)
|
|
566
|
+
map.putNull("error")
|
|
567
|
+
|
|
568
|
+
val devArr = Arguments.createArray()
|
|
569
|
+
for (r in discoveredRoutes.values) devArr.pushMap(routeToMap(r))
|
|
570
|
+
map.putArray("devices", devArr)
|
|
571
|
+
|
|
572
|
+
val connected = connectedRoute
|
|
573
|
+
if (connected != null) map.putMap("connectedDevice", routeToMap(connected))
|
|
574
|
+
else map.putNull("connectedDevice")
|
|
575
|
+
|
|
576
|
+
return map
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
private fun emitAppCastChange(error: String? = null) {
|
|
580
|
+
val map = buildAppCastMap()
|
|
581
|
+
if (error != null) map.putString("error", error)
|
|
582
|
+
emit("SystemBar_AppCastChange", map)
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// ── Scan ──────────────────────────────────
|
|
586
|
+
|
|
587
|
+
@ReactMethod
|
|
588
|
+
fun startAppCastScan() {
|
|
589
|
+
if (appCastCallback != null) return // already scanning
|
|
590
|
+
appCastState = "scanning"
|
|
591
|
+
discoveredRoutes.clear()
|
|
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
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
appCastCallback = cb
|
|
630
|
+
mainHandler.post {
|
|
631
|
+
mediaRouter.addCallback(
|
|
632
|
+
android.media.MediaRouter.ROUTE_TYPE_LIVE_VIDEO,
|
|
633
|
+
cb,
|
|
634
|
+
android.media.MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY or
|
|
635
|
+
android.media.MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN
|
|
636
|
+
)
|
|
637
|
+
}
|
|
638
|
+
emitAppCastChange()
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
@ReactMethod
|
|
642
|
+
fun stopAppCastScan() {
|
|
643
|
+
appCastCallback?.let { mainHandler.post { mediaRouter.removeCallback(it) } }
|
|
644
|
+
appCastCallback = null
|
|
645
|
+
if (appCastState == "scanning") {
|
|
646
|
+
appCastState = "idle"
|
|
647
|
+
emitAppCastChange()
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// ── Connect / Disconnect ──────────────────
|
|
652
|
+
|
|
653
|
+
@ReactMethod
|
|
654
|
+
fun connectAppCast(deviceId: String, pairingPin: String?) {
|
|
655
|
+
val route = discoveredRoutes[deviceId]
|
|
656
|
+
if (route == null) {
|
|
657
|
+
val err = Arguments.createMap()
|
|
658
|
+
err.putString("state", appCastState)
|
|
659
|
+
err.putString("error", "Device not found: $deviceId")
|
|
660
|
+
err.putNull("connectedDevice")
|
|
661
|
+
err.putArray("devices", Arguments.createArray())
|
|
662
|
+
emit("SystemBar_AppCastChange", err)
|
|
663
|
+
return
|
|
664
|
+
}
|
|
665
|
+
appCastState = "connecting"
|
|
666
|
+
emitAppCastChange()
|
|
667
|
+
mainHandler.post {
|
|
668
|
+
try {
|
|
669
|
+
mediaRouter.selectRoute(android.media.MediaRouter.ROUTE_TYPE_LIVE_VIDEO, route)
|
|
670
|
+
// onRouteSelected callback will set state = "connected" and re-emit
|
|
671
|
+
} catch (e: Exception) {
|
|
672
|
+
appCastState = "idle"
|
|
673
|
+
emitAppCastChange(error = e.message ?: "Connection failed")
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
@ReactMethod
|
|
679
|
+
fun disconnectAppCast() {
|
|
680
|
+
if (connectedRoute == null) return
|
|
681
|
+
appCastState = "disconnecting"
|
|
682
|
+
emitAppCastChange()
|
|
683
|
+
mainHandler.post {
|
|
684
|
+
try {
|
|
685
|
+
// Select the default (phone) route to stop remote casting
|
|
686
|
+
val defaultRoute = mediaRouter.getDefaultRoute()
|
|
687
|
+
mediaRouter.selectRoute(android.media.MediaRouter.ROUTE_TYPE_LIVE_VIDEO, defaultRoute)
|
|
688
|
+
connectedRoute = null
|
|
689
|
+
appCastState = "idle"
|
|
690
|
+
emitAppCastChange()
|
|
691
|
+
} catch (e: Exception) {
|
|
692
|
+
appCastState = "idle"
|
|
693
|
+
emitAppCastChange(error = e.message ?: "Disconnect failed")
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
@ReactMethod
|
|
699
|
+
fun getAppCastInfo(promise: Promise) {
|
|
700
|
+
try { promise.resolve(buildAppCastMap()) }
|
|
701
|
+
catch (e: Exception) { promise.reject("APP_CAST_ERROR", e.message, e) }
|
|
702
|
+
}
|
|
703
|
+
|
|
430
704
|
// ── Cleanup ──────────────────────────────────
|
|
431
705
|
override fun onCatalystInstanceDestroy() {
|
|
432
706
|
stopBrightnessListener()
|
|
433
707
|
stopVolumeListener()
|
|
434
|
-
|
|
708
|
+
stopSystemScreencastListener()
|
|
709
|
+
stopAppCastScan()
|
|
435
710
|
}
|
|
436
|
-
}
|
|
711
|
+
}
|
package/index.ts
CHANGED
|
@@ -2,7 +2,28 @@
|
|
|
2
2
|
// rn-system-bar · index.ts
|
|
3
3
|
// ─────────────────────────────────────────────
|
|
4
4
|
|
|
5
|
+
// All imperative JS/TS APIs
|
|
5
6
|
export * from "./src/SystemBar";
|
|
7
|
+
|
|
8
|
+
// All TypeScript types
|
|
6
9
|
export * from "./src/types";
|
|
7
|
-
|
|
8
|
-
|
|
10
|
+
|
|
11
|
+
// Theme hook (standalone access)
|
|
12
|
+
export { setGlobalThemeMode, useTheme } from "./src/useTheme";
|
|
13
|
+
|
|
14
|
+
// React hooks
|
|
15
|
+
export {
|
|
16
|
+
useAppCast,
|
|
17
|
+
useScreencast,
|
|
18
|
+
useSystemBar,
|
|
19
|
+
useSystemScreencast,
|
|
20
|
+
useTheme as useThemeHook,
|
|
21
|
+
useThemeSystemBar,
|
|
22
|
+
} from "./src/useSystemBar";
|
|
23
|
+
|
|
24
|
+
// Types from hooks
|
|
25
|
+
export type {
|
|
26
|
+
SystemBarConfig,
|
|
27
|
+
ThemedSystemBarConfig,
|
|
28
|
+
UseAppCastReturn,
|
|
29
|
+
} from "./src/useSystemBar";
|
package/ios/SystemBarModule.m
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ─────────────────────────────────────────────
|
|
2
|
-
// rn-system-bar · SystemBarModule.m
|
|
2
|
+
// rn-system-bar · SystemBarModule.m v6
|
|
3
3
|
// Objective-C bridge — exposes Swift to React Native
|
|
4
4
|
// ─────────────────────────────────────────────
|
|
5
5
|
|
|
@@ -36,10 +36,17 @@ RCT_EXTERN_METHOD(stopBatteryListener)
|
|
|
36
36
|
// Haptics
|
|
37
37
|
RCT_EXTERN_METHOD(haptic:(NSString *)pattern)
|
|
38
38
|
|
|
39
|
-
// Screencast
|
|
40
|
-
RCT_EXTERN_METHOD(
|
|
41
|
-
RCT_EXTERN_METHOD(
|
|
42
|
-
RCT_EXTERN_METHOD(
|
|
39
|
+
// System Screencast (external display / AirPlay mirror)
|
|
40
|
+
RCT_EXTERN_METHOD(getSystemScreencastInfo:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
41
|
+
RCT_EXTERN_METHOD(startSystemScreencastListener)
|
|
42
|
+
RCT_EXTERN_METHOD(stopSystemScreencastListener)
|
|
43
|
+
|
|
44
|
+
// App-only Cast (Android MediaRouter — stubs on iOS)
|
|
45
|
+
RCT_EXTERN_METHOD(startAppCastScan)
|
|
46
|
+
RCT_EXTERN_METHOD(stopAppCastScan)
|
|
47
|
+
RCT_EXTERN_METHOD(connectAppCast:(NSString *)deviceId pairingPin:(NSString *)pairingPin)
|
|
48
|
+
RCT_EXTERN_METHOD(disconnectAppCast)
|
|
49
|
+
RCT_EXTERN_METHOD(getAppCastInfo:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
43
50
|
|
|
44
51
|
// Font Scale
|
|
45
52
|
RCT_EXTERN_METHOD(getFontScaleInfo:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
@@ -58,4 +65,4 @@ RCT_EXTERN_METHOD(setStatusBarVisibility:(BOOL)visible)
|
|
|
58
65
|
RCT_EXTERN_METHOD(setVolumeHUDVisible:(BOOL)visible)
|
|
59
66
|
RCT_EXTERN_METHOD(immersiveMode:(BOOL)enable)
|
|
60
67
|
|
|
61
|
-
@end
|
|
68
|
+
@end
|