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
|
@@ -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
|
|
@@ -226,36 +226,70 @@ class SystemBarModule: RCTEventEmitter {
|
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
// ═══════════════════════════════════════════════
|
|
229
|
-
// 🆕 SCREENCAST
|
|
229
|
+
// 🆕 SYSTEM SCREENCAST (external display / AirPlay mirror)
|
|
230
|
+
// Detects OS-level external screens via UIScreen.
|
|
230
231
|
// ═══════════════════════════════════════════════
|
|
231
232
|
|
|
232
|
-
private func
|
|
233
|
+
private func currentSystemScreencastMap() -> [String: Any] {
|
|
233
234
|
let isCasting = UIScreen.screens.count > 1
|
|
234
235
|
return [
|
|
235
236
|
"isCasting": isCasting,
|
|
236
237
|
"displayName": isCasting ? "External Display" : NSNull(),
|
|
238
|
+
"displays": UIScreen.screens.dropFirst().enumerated().map { idx, _ in
|
|
239
|
+
["id": idx + 1, "name": "External Display \(idx + 1)", "isValid": true]
|
|
240
|
+
},
|
|
237
241
|
]
|
|
238
242
|
}
|
|
239
243
|
|
|
240
244
|
@objc
|
|
241
|
-
func
|
|
242
|
-
|
|
243
|
-
resolve(
|
|
245
|
+
func getSystemScreencastInfo(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
246
|
+
rejecter reject: RCTPromiseRejectBlock) {
|
|
247
|
+
resolve(currentSystemScreencastMap())
|
|
244
248
|
}
|
|
245
249
|
|
|
246
|
-
@objc func
|
|
250
|
+
@objc func startSystemScreencastListener() {
|
|
247
251
|
screencastObserver = NotificationCenter.default.addObserver(
|
|
248
252
|
forName: UIScreen.didConnectNotification, object: nil, queue: .main
|
|
249
253
|
) { [weak self] _ in
|
|
250
|
-
self?.emit("
|
|
254
|
+
self?.emit("SystemBar_SystemScreencastChange", body: self?.currentSystemScreencastMap() ?? [:])
|
|
251
255
|
}
|
|
252
256
|
}
|
|
253
257
|
|
|
254
|
-
@objc func
|
|
258
|
+
@objc func stopSystemScreencastListener() {
|
|
255
259
|
if let obs = screencastObserver { NotificationCenter.default.removeObserver(obs) }
|
|
256
260
|
screencastObserver = nil
|
|
257
261
|
}
|
|
258
262
|
|
|
263
|
+
// ═══════════════════════════════════════════════
|
|
264
|
+
// APP-ONLY CAST (Android MediaRouter — iOS stubs)
|
|
265
|
+
// AirPlay is system-managed on iOS; no public API for app-only casting.
|
|
266
|
+
// ═══════════════════════════════════════════════
|
|
267
|
+
|
|
268
|
+
@objc func startAppCastScan() {
|
|
269
|
+
// iOS: AirPlay is system-managed — no-op. Emit idle state.
|
|
270
|
+
emit("SystemBar_AppCastChange", body: [
|
|
271
|
+
"state": "idle", "devices": [], "connectedDevice": NSNull(), "error": NSNull()
|
|
272
|
+
])
|
|
273
|
+
}
|
|
274
|
+
@objc func stopAppCastScan() {}
|
|
275
|
+
|
|
276
|
+
@objc func connectAppCast(_ deviceId: String, pairingPin: String?) {
|
|
277
|
+
emit("SystemBar_AppCastChange", body: [
|
|
278
|
+
"state": "idle", "devices": [], "connectedDevice": NSNull(),
|
|
279
|
+
"error": "App-only cast not supported on iOS"
|
|
280
|
+
])
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
@objc func disconnectAppCast() {}
|
|
284
|
+
|
|
285
|
+
@objc
|
|
286
|
+
func getAppCastInfo(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
287
|
+
rejecter reject: RCTPromiseRejectBlock) {
|
|
288
|
+
resolve([
|
|
289
|
+
"state": "idle", "devices": [], "connectedDevice": NSNull(), "error": NSNull()
|
|
290
|
+
])
|
|
291
|
+
}
|
|
292
|
+
|
|
259
293
|
// setSecureScreen — iOS doesn't support this via public API (no-op)
|
|
260
294
|
@objc func setSecureScreen(_ enable: Bool) {}
|
|
261
295
|
|
|
@@ -295,9 +329,10 @@ class SystemBarModule: RCTEventEmitter {
|
|
|
295
329
|
@objc func setNavigationBarButtonStyle(_ style: String) {}
|
|
296
330
|
@objc func setNavigationBarStyle(_ style: String) {}
|
|
297
331
|
@objc func setNavigationBarBehavior(_ behavior: String) {}
|
|
332
|
+
// color: hex | "transparent" | "translucent" — Android only
|
|
298
333
|
@objc func setStatusBarColor(_ color: String) {}
|
|
299
334
|
@objc func setVolumeHUDVisible(_ visible: Bool) {}
|
|
300
335
|
@objc func immersiveMode(_ enable: Bool) {}
|
|
301
336
|
@objc func setStatusBarStyle(_ style: String) {}
|
|
302
337
|
@objc func setStatusBarVisibility(_ visible: Bool) {}
|
|
303
|
-
}
|
|
338
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export * from "./src/SystemBar";
|
|
2
2
|
export * from "./src/types";
|
|
3
|
-
export {
|
|
4
|
-
export
|
|
3
|
+
export { setGlobalThemeMode, useTheme } from "./src/useTheme";
|
|
4
|
+
export { useAppCast, useScreencast, useSystemBar, useSystemScreencast, useTheme as useThemeHook, useThemeSystemBar, } from "./src/useSystemBar";
|
|
5
|
+
export type { SystemBarConfig, ThemedSystemBarConfig, UseAppCastReturn, } from "./src/useSystemBar";
|
package/lib/index.js
CHANGED
|
@@ -17,9 +17,20 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
17
17
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
18
18
|
};
|
|
19
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
-
exports.
|
|
20
|
+
exports.useThemeSystemBar = exports.useThemeHook = exports.useSystemScreencast = exports.useSystemBar = exports.useScreencast = exports.useAppCast = exports.useTheme = exports.setGlobalThemeMode = void 0;
|
|
21
|
+
// All imperative JS/TS APIs
|
|
21
22
|
__exportStar(require("./src/SystemBar"), exports);
|
|
23
|
+
// All TypeScript types
|
|
22
24
|
__exportStar(require("./src/types"), exports);
|
|
25
|
+
// Theme hook (standalone access)
|
|
26
|
+
var useTheme_1 = require("./src/useTheme");
|
|
27
|
+
Object.defineProperty(exports, "setGlobalThemeMode", { enumerable: true, get: function () { return useTheme_1.setGlobalThemeMode; } });
|
|
28
|
+
Object.defineProperty(exports, "useTheme", { enumerable: true, get: function () { return useTheme_1.useTheme; } });
|
|
29
|
+
// React hooks
|
|
23
30
|
var useSystemBar_1 = require("./src/useSystemBar");
|
|
24
|
-
Object.defineProperty(exports, "
|
|
31
|
+
Object.defineProperty(exports, "useAppCast", { enumerable: true, get: function () { return useSystemBar_1.useAppCast; } });
|
|
25
32
|
Object.defineProperty(exports, "useScreencast", { enumerable: true, get: function () { return useSystemBar_1.useScreencast; } });
|
|
33
|
+
Object.defineProperty(exports, "useSystemBar", { enumerable: true, get: function () { return useSystemBar_1.useSystemBar; } });
|
|
34
|
+
Object.defineProperty(exports, "useSystemScreencast", { enumerable: true, get: function () { return useSystemBar_1.useSystemScreencast; } });
|
|
35
|
+
Object.defineProperty(exports, "useThemeHook", { enumerable: true, get: function () { return useSystemBar_1.useTheme; } });
|
|
36
|
+
Object.defineProperty(exports, "useThemeSystemBar", { enumerable: true, get: function () { return useSystemBar_1.useThemeSystemBar; } });
|
|
@@ -15,7 +15,16 @@ export interface Spec extends TurboModule {
|
|
|
15
15
|
setVolumeHUDVisible(visible: boolean): void;
|
|
16
16
|
keepScreenOn(enable: boolean): void;
|
|
17
17
|
immersiveMode(enable: boolean): void;
|
|
18
|
+
setSecureScreen(enable: boolean): void;
|
|
18
19
|
setOrientation(mode: string): void;
|
|
20
|
+
getSystemScreencastInfo(): Promise<Object>;
|
|
21
|
+
startSystemScreencastListener(): void;
|
|
22
|
+
stopSystemScreencastListener(): void;
|
|
23
|
+
startAppCastScan(): void;
|
|
24
|
+
stopAppCastScan(): void;
|
|
25
|
+
connectAppCast(deviceId: string, pairingPin: string | null): void;
|
|
26
|
+
disconnectAppCast(): void;
|
|
27
|
+
getAppCastInfo(): Promise<Object>;
|
|
19
28
|
}
|
|
20
29
|
declare const _default: Spec;
|
|
21
30
|
export default _default;
|
package/lib/src/SystemBar.d.ts
CHANGED
|
@@ -1,15 +1,42 @@
|
|
|
1
|
-
import type { NavigationBarBehavior, NavigationBarButtonStyle, NavigationBarStyle, NavigationBarVisibility, Orientation,
|
|
2
|
-
|
|
1
|
+
import type { AppCastInfo, NavigationBarBehavior, NavigationBarButtonStyle, NavigationBarColorValue, NavigationBarStyle, NavigationBarVisibility, Orientation, StatusBarColorValue, StatusBarStyle, SystemScreencastInfo, VolumeStream } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Set the navigation bar background colour.
|
|
4
|
+
*
|
|
5
|
+
* @param color
|
|
6
|
+
* - Any hex string → solid colour e.g. `"#1a1a2e"`
|
|
7
|
+
* - `"transparent"` → fully transparent (content draws behind bar)
|
|
8
|
+
* - `"translucent"` → semi-transparent (system scrim over content)
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* setNavigationBarColor("#000000"); // solid black
|
|
12
|
+
* setNavigationBarColor("transparent"); // glass / edge-to-edge
|
|
13
|
+
* setNavigationBarColor("translucent"); // frosted glass
|
|
14
|
+
*/
|
|
15
|
+
export declare const setNavigationBarColor: (color: NavigationBarColorValue) => void;
|
|
16
|
+
/**
|
|
17
|
+
* Hide or show the navigation bar.
|
|
18
|
+
*
|
|
19
|
+
* @param mode `"visible"` | `"hidden"`
|
|
20
|
+
*/
|
|
3
21
|
export declare const setNavigationBarVisibility: (mode: NavigationBarVisibility) => void;
|
|
4
22
|
export declare const setNavigationBarButtonStyle: (style: NavigationBarButtonStyle) => void;
|
|
5
23
|
export declare const setNavigationBarStyle: (style: NavigationBarStyle) => void;
|
|
6
24
|
export declare const setNavigationBarBehavior: (behavior: NavigationBarBehavior) => void;
|
|
25
|
+
/**
|
|
26
|
+
* Set the status bar background colour (Android only).
|
|
27
|
+
*
|
|
28
|
+
* @param color
|
|
29
|
+
* - Any hex string → solid colour
|
|
30
|
+
* - `"transparent"` → fully transparent
|
|
31
|
+
* - `"translucent"` → semi-transparent
|
|
32
|
+
*/
|
|
33
|
+
export declare const setStatusBarColor: (color: StatusBarColorValue) => void;
|
|
7
34
|
export declare const setStatusBarStyle: (style: StatusBarStyle) => void;
|
|
8
35
|
export declare const setStatusBarVisibility: (visible: boolean) => void;
|
|
9
36
|
export declare const setBrightness: (level: number) => void;
|
|
10
37
|
export declare const getBrightness: () => Promise<number>;
|
|
11
38
|
/**
|
|
12
|
-
* Subscribe to system brightness changes (polls every
|
|
39
|
+
* Subscribe to system brightness changes (polls every 500 ms on Android).
|
|
13
40
|
* @returns unsubscribe function
|
|
14
41
|
*/
|
|
15
42
|
export declare const onBrightnessChange: (callback: (brightness: number) => void) => (() => void);
|
|
@@ -25,5 +52,56 @@ export declare const keepScreenOn: (enable: boolean) => void;
|
|
|
25
52
|
export declare const immersiveMode: (enable: boolean) => void;
|
|
26
53
|
export declare const setSecureScreen: (enable: boolean) => void;
|
|
27
54
|
export declare const setOrientation: (mode: Orientation) => void;
|
|
28
|
-
|
|
29
|
-
|
|
55
|
+
/**
|
|
56
|
+
* One-shot snapshot of system-level external display state.
|
|
57
|
+
* Works on both Android (DisplayManager) and iOS (UIScreen.screens).
|
|
58
|
+
*/
|
|
59
|
+
export declare const getSystemScreencastInfo: () => Promise<SystemScreencastInfo>;
|
|
60
|
+
/**
|
|
61
|
+
* Subscribe to system external-display changes.
|
|
62
|
+
* Fires when an HDMI / Miracast / AirPlay display connects or disconnects.
|
|
63
|
+
* @returns unsubscribe function
|
|
64
|
+
*/
|
|
65
|
+
export declare const onSystemScreencastChange: (callback: (info: SystemScreencastInfo) => void) => (() => void);
|
|
66
|
+
/** @deprecated Use getSystemScreencastInfo() */
|
|
67
|
+
export declare const getScreencastInfo: () => Promise<SystemScreencastInfo>;
|
|
68
|
+
/** @deprecated Use onSystemScreencastChange() */
|
|
69
|
+
export declare const onScreencastChange: (callback: (info: SystemScreencastInfo) => void) => (() => void);
|
|
70
|
+
/**
|
|
71
|
+
* Start scanning for nearby castable devices (Chromecast, TV, etc.).
|
|
72
|
+
* Listen for results via `onAppCastChange`.
|
|
73
|
+
* Android: uses MediaRouter. iOS: no-op (AirPlay is system-only).
|
|
74
|
+
*/
|
|
75
|
+
export declare const startAppCastScan: () => void;
|
|
76
|
+
/**
|
|
77
|
+
* Stop the device discovery scan.
|
|
78
|
+
*/
|
|
79
|
+
export declare const stopAppCastScan: () => void;
|
|
80
|
+
/**
|
|
81
|
+
* Connect to a discovered device and begin casting this app's screen.
|
|
82
|
+
* @param deviceId The `id` field from `AppCastDevice` (MediaRouter route ID).
|
|
83
|
+
* @param pairingPin Optional PIN string if `requiresPairing` is true.
|
|
84
|
+
*/
|
|
85
|
+
export declare const connectAppCast: (deviceId: string, pairingPin?: string) => void;
|
|
86
|
+
/**
|
|
87
|
+
* Disconnect the active in-app cast session.
|
|
88
|
+
*/
|
|
89
|
+
export declare const disconnectAppCast: () => void;
|
|
90
|
+
/**
|
|
91
|
+
* Get the current in-app cast snapshot (state + device list).
|
|
92
|
+
*/
|
|
93
|
+
export declare const getAppCastInfo: () => Promise<AppCastInfo>;
|
|
94
|
+
/**
|
|
95
|
+
* Subscribe to in-app cast state changes.
|
|
96
|
+
* Fires on: device discovered/lost, state changes (scanning → connecting → connected),
|
|
97
|
+
* pairing requests, errors.
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* const unsub = onAppCastChange((info) => {
|
|
101
|
+
* if (info.state === "connected") console.log("Casting to", info.connectedDevice?.name);
|
|
102
|
+
* if (info.error) console.warn("Cast error:", info.error);
|
|
103
|
+
* });
|
|
104
|
+
*
|
|
105
|
+
* @returns unsubscribe function
|
|
106
|
+
*/
|
|
107
|
+
export declare const onAppCastChange: (callback: (info: AppCastInfo) => void) => (() => void);
|