rn-system-bar 3.1.3 → 3.1.5

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.
@@ -5,13 +5,17 @@
5
5
  package com.systembar
6
6
 
7
7
  import android.app.Activity
8
+ import android.content.BroadcastReceiver
8
9
  import android.content.Context
9
10
  import android.content.Intent
11
+ import android.content.IntentFilter
10
12
  import android.content.pm.ActivityInfo
11
13
  import android.graphics.Color
12
14
  import android.hardware.display.DisplayManager
13
15
  import android.media.AudioManager
14
16
  import android.os.Build
17
+ import android.os.Handler
18
+ import android.os.Looper
15
19
  import android.provider.Settings
16
20
  import android.view.View
17
21
  import android.view.WindowInsets
@@ -29,12 +33,10 @@ class SystemBarModule(
29
33
  override fun getName(): String = "SystemBar"
30
34
 
31
35
  private fun activity(): Activity? = reactContext.currentActivity
36
+ private val mainHandler = Handler(Looper.getMainLooper())
32
37
 
33
- private fun audioManager() =
34
- reactContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
35
-
36
- private fun displayManager() =
37
- reactContext.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
38
+ private fun audioManager() = reactContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
39
+ private fun displayManager() = reactContext.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
38
40
 
39
41
  private fun streamType(stream: String): Int = when (stream) {
40
42
  "ring" -> AudioManager.STREAM_RING
@@ -51,26 +53,114 @@ class SystemBarModule(
51
53
  }
52
54
 
53
55
  // ═══════════════════════════════════════════════
54
- // NAVIGATION BAR COLOR
55
- // WindowInsetsController API 30+, fallback for older.
56
- // navigationBarColor deprecated on API 35+ but still
57
- // works — suppressed at class level.
56
+ // NAVIGATION BAR — 100% native, no expo dep
58
57
  // ═══════════════════════════════════════════════
59
58
 
60
59
  @ReactMethod
61
60
  fun setNavigationBarColor(color: String) {
62
61
  activity()?.runOnUiThread {
63
- try {
64
- val parsed = Color.parseColor(color)
65
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
66
- // API 35+ — use WindowInsetsController appearance for tinting
67
- val controller = activity()!!.window.insetsController
68
- // Nav bar color via window attribute still works as tint on 35+
69
- activity()!!.window.navigationBarColor = parsed
62
+ try { activity()!!.window.navigationBarColor = Color.parseColor(color) }
63
+ catch (_: Exception) {}
64
+ }
65
+ }
66
+
67
+ @ReactMethod
68
+ fun setNavigationBarVisibility(mode: String) {
69
+ activity()?.runOnUiThread {
70
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
71
+ val c = activity()!!.window.insetsController ?: return@runOnUiThread
72
+ if (mode == "hidden") c.hide(WindowInsets.Type.navigationBars())
73
+ else c.show(WindowInsets.Type.navigationBars())
74
+ } else {
75
+ val dv = activity()!!.window.decorView
76
+ dv.systemUiVisibility = if (mode == "hidden") {
77
+ dv.systemUiVisibility or
78
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
79
+ View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
70
80
  } else {
71
- activity()!!.window.navigationBarColor = parsed
81
+ dv.systemUiVisibility and View.SYSTEM_UI_FLAG_HIDE_NAVIGATION.inv()
72
82
  }
73
- } catch (_: Exception) {}
83
+ }
84
+ }
85
+ }
86
+
87
+ @ReactMethod
88
+ fun setNavigationBarButtonStyle(style: String) {
89
+ activity()?.runOnUiThread {
90
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
91
+ val c = activity()!!.window.insetsController ?: return@runOnUiThread
92
+ c.setSystemBarsAppearance(
93
+ if (style == "dark") WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS else 0,
94
+ WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS
95
+ )
96
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
97
+ val dv = activity()!!.window.decorView
98
+ dv.systemUiVisibility = if (style == "dark")
99
+ dv.systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
100
+ else
101
+ dv.systemUiVisibility and View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv()
102
+ }
103
+ }
104
+ }
105
+
106
+ @ReactMethod
107
+ fun setNavigationBarStyle(style: String) {
108
+ // Maps to button style — "dark"/"auto" → dark icons, "light"/"inverted" → light icons
109
+ setNavigationBarButtonStyle(if (style == "dark" || style == "auto") "dark" else "light")
110
+ }
111
+
112
+ @ReactMethod
113
+ fun setNavigationBarBehavior(behavior: String) {
114
+ activity()?.runOnUiThread {
115
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
116
+ val c = activity()!!.window.insetsController ?: return@runOnUiThread
117
+ c.systemBarsBehavior = when (behavior) {
118
+ "overlay-swipe" -> WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
119
+ // inset modes — default persistent behavior
120
+ else -> WindowInsetsController.BEHAVIOR_DEFAULT
121
+ }
122
+ }
123
+ // API < 30: IMMERSIVE_STICKY is closest — no-op otherwise
124
+ }
125
+ }
126
+
127
+ // ═══════════════════════════════════════════════
128
+ // STATUS BAR — 100% native
129
+ // ═══════════════════════════════════════════════
130
+
131
+ @ReactMethod
132
+ fun setStatusBarStyle(style: String) {
133
+ activity()?.runOnUiThread {
134
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
135
+ val c = activity()!!.window.insetsController ?: return@runOnUiThread
136
+ c.setSystemBarsAppearance(
137
+ if (style == "dark") WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS else 0,
138
+ WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
139
+ )
140
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
141
+ val dv = activity()!!.window.decorView
142
+ dv.systemUiVisibility = if (style == "dark")
143
+ dv.systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
144
+ else
145
+ dv.systemUiVisibility and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
146
+ }
147
+ }
148
+ }
149
+
150
+ @ReactMethod
151
+ fun setStatusBarVisibility(visible: Boolean) {
152
+ activity()?.runOnUiThread {
153
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
154
+ val c = activity()!!.window.insetsController ?: return@runOnUiThread
155
+ if (visible) c.show(WindowInsets.Type.statusBars())
156
+ else c.hide(WindowInsets.Type.statusBars())
157
+ } else {
158
+ val dv = activity()!!.window.decorView
159
+ dv.systemUiVisibility = if (!visible)
160
+ dv.systemUiVisibility or View.SYSTEM_UI_FLAG_FULLSCREEN
161
+ else
162
+ dv.systemUiVisibility and View.SYSTEM_UI_FLAG_FULLSCREEN.inv()
163
+ }
74
164
  }
75
165
  }
76
166
 
@@ -81,47 +171,64 @@ class SystemBarModule(
81
171
  @ReactMethod
82
172
  fun setBrightness(level: Float) {
83
173
  val clamped = level.coerceIn(0.01f, 1f)
84
-
85
- // Window brightness — instant, no permission needed
86
174
  activity()?.runOnUiThread {
87
175
  val lp = activity()!!.window.attributes
88
176
  lp.screenBrightness = clamped
89
177
  activity()!!.window.attributes = lp
90
178
  }
91
-
92
- // System brightness — persisted, needs WRITE_SETTINGS
93
179
  val sysValue = (clamped * 255).toInt().coerceIn(1, 255)
94
180
  if (Settings.System.canWrite(reactContext)) {
95
- Settings.System.putInt(
96
- reactContext.contentResolver,
97
- Settings.System.SCREEN_BRIGHTNESS,
98
- sysValue
99
- )
100
- Settings.System.putInt(
101
- reactContext.contentResolver,
102
- Settings.System.SCREEN_BRIGHTNESS_MODE,
103
- Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL
104
- )
181
+ Settings.System.putInt(reactContext.contentResolver, Settings.System.SCREEN_BRIGHTNESS, sysValue)
182
+ Settings.System.putInt(reactContext.contentResolver, Settings.System.SCREEN_BRIGHTNESS_MODE, Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL)
105
183
  } else {
106
- val intent = Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS).apply {
107
- flags = Intent.FLAG_ACTIVITY_NEW_TASK
108
- }
109
- try { reactContext.startActivity(intent) } catch (_: Exception) {}
184
+ try {
185
+ reactContext.startActivity(Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS).apply {
186
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
187
+ })
188
+ } catch (_: Exception) {}
110
189
  }
111
190
  }
112
191
 
113
192
  @ReactMethod
114
193
  fun getBrightness(promise: Promise) {
115
194
  try {
116
- val sys = Settings.System.getInt(
117
- reactContext.contentResolver,
118
- Settings.System.SCREEN_BRIGHTNESS,
119
- 128
120
- )
195
+ val sys = Settings.System.getInt(reactContext.contentResolver, Settings.System.SCREEN_BRIGHTNESS, 128)
121
196
  promise.resolve(sys / 255.0)
122
- } catch (e: Exception) {
123
- promise.reject("BRIGHTNESS_ERROR", e.message, e)
197
+ } catch (e: Exception) { promise.reject("BRIGHTNESS_ERROR", e.message, e) }
198
+ }
199
+
200
+ // ── Realtime brightness listener ─────────────
201
+ private var brightnessReceiver: BroadcastReceiver? = null
202
+ private var brightnessPollingRunnable: Runnable? = null
203
+ private var lastBrightness = -1
204
+
205
+ @ReactMethod
206
+ fun startBrightnessListener() {
207
+ if (brightnessPollingRunnable != null) return
208
+ // Settings.System doesn't broadcast changes — poll every 500ms
209
+ val runnable = object : Runnable {
210
+ override fun run() {
211
+ try {
212
+ val cur = Settings.System.getInt(reactContext.contentResolver, Settings.System.SCREEN_BRIGHTNESS, 128)
213
+ if (cur != lastBrightness) {
214
+ lastBrightness = cur
215
+ val map = Arguments.createMap()
216
+ map.putDouble("brightness", cur / 255.0)
217
+ emit("SystemBar_BrightnessChange", map)
218
+ }
219
+ } catch (_: Exception) {}
220
+ brightnessPollingRunnable?.let { mainHandler.postDelayed(it, 500) }
221
+ }
124
222
  }
223
+ brightnessPollingRunnable = runnable
224
+ mainHandler.post(runnable)
225
+ }
226
+
227
+ @ReactMethod
228
+ fun stopBrightnessListener() {
229
+ brightnessPollingRunnable?.let { mainHandler.removeCallbacks(it) }
230
+ brightnessPollingRunnable = null
231
+ lastBrightness = -1
125
232
  }
126
233
 
127
234
  // ═══════════════════════════════════════════════
@@ -130,16 +237,13 @@ class SystemBarModule(
130
237
 
131
238
  private var suppressVolumeHUD = false
132
239
 
133
- @ReactMethod
134
- fun setVolumeHUDVisible(visible: Boolean) { suppressVolumeHUD = !visible }
240
+ @ReactMethod fun setVolumeHUDVisible(visible: Boolean) { suppressVolumeHUD = !visible }
135
241
 
136
242
  @ReactMethod
137
243
  fun setVolume(level: Float, stream: String) {
138
244
  try {
139
- val am = audioManager()
140
- val type = streamType(stream)
141
- val max = am.getStreamMaxVolume(type)
142
- val vol = (level.coerceIn(0f, 1f) * max).toInt()
245
+ val am = audioManager(); val type = streamType(stream)
246
+ val vol = (level.coerceIn(0f, 1f) * am.getStreamMaxVolume(type)).toInt()
143
247
  am.setStreamVolume(type, vol, if (suppressVolumeHUD) 0 else AudioManager.FLAG_SHOW_UI)
144
248
  } catch (_: Exception) {}
145
249
  }
@@ -147,13 +251,46 @@ class SystemBarModule(
147
251
  @ReactMethod
148
252
  fun getVolume(stream: String, promise: Promise) {
149
253
  try {
150
- val am = audioManager()
151
- val type = streamType(stream)
152
- val max = am.getStreamMaxVolume(type)
254
+ val am = audioManager(); val type = streamType(stream)
255
+ val max = am.getStreamMaxVolume(type)
153
256
  promise.resolve(if (max > 0) am.getStreamVolume(type).toDouble() / max else 0.0)
154
- } catch (e: Exception) {
155
- promise.reject("VOLUME_ERROR", e.message, e)
257
+ } catch (e: Exception) { promise.reject("VOLUME_ERROR", e.message, e) }
258
+ }
259
+
260
+ // ── Realtime volume listener ─────────────────
261
+ private var volumeReceiver: BroadcastReceiver? = null
262
+
263
+ @ReactMethod
264
+ fun startVolumeListener() {
265
+ if (volumeReceiver != null) return
266
+ val receiver = object : BroadcastReceiver() {
267
+ override fun onReceive(ctx: Context?, intent: Intent?) {
268
+ if (intent?.action != "android.media.VOLUME_CHANGED_ACTION") return
269
+ val streamType = intent.getIntExtra("android.media.EXTRA_VOLUME_STREAM_TYPE", AudioManager.STREAM_MUSIC)
270
+ val am = audioManager()
271
+ val max = am.getStreamMaxVolume(streamType)
272
+ val cur = am.getStreamVolume(streamType)
273
+ val streamName = when (streamType) {
274
+ AudioManager.STREAM_RING -> "ring"
275
+ AudioManager.STREAM_NOTIFICATION -> "notification"
276
+ AudioManager.STREAM_ALARM -> "alarm"
277
+ AudioManager.STREAM_SYSTEM -> "system"
278
+ else -> "music"
279
+ }
280
+ val map = Arguments.createMap()
281
+ map.putDouble("volume", if (max > 0) cur.toDouble() / max else 0.0)
282
+ map.putString("stream", streamName)
283
+ emit("SystemBar_VolumeChange", map)
284
+ }
156
285
  }
286
+ volumeReceiver = receiver
287
+ reactContext.registerReceiver(receiver, IntentFilter("android.media.VOLUME_CHANGED_ACTION"))
288
+ }
289
+
290
+ @ReactMethod
291
+ fun stopVolumeListener() {
292
+ volumeReceiver?.let { try { reactContext.unregisterReceiver(it) } catch (_: Exception) {} }
293
+ volumeReceiver = null
157
294
  }
158
295
 
159
296
  // ═══════════════════════════════════════════════
@@ -181,14 +318,10 @@ class SystemBarModule(
181
318
  }
182
319
  } else {
183
320
  activity()!!.window.decorView.systemUiVisibility = if (enable) {
184
- View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
185
- View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
186
- View.SYSTEM_UI_FLAG_FULLSCREEN or
187
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
321
+ View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
322
+ View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
188
323
  View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
189
- } else {
190
- View.SYSTEM_UI_FLAG_VISIBLE
191
- }
324
+ } else { View.SYSTEM_UI_FLAG_VISIBLE }
192
325
  }
193
326
  }
194
327
  }
@@ -203,33 +336,59 @@ class SystemBarModule(
203
336
 
204
337
  // ═══════════════════════════════════════════════
205
338
  // ORIENTATION
206
- // "auto" = FULL_SENSOR (follows system auto-rotate)
207
339
  // ═══════════════════════════════════════════════
208
340
 
209
341
  @ReactMethod
210
342
  fun setOrientation(mode: String) {
343
+ // "auto" → always unspecified (follow system auto-rotate toggle unconditionally)
344
+ if (mode == "auto") {
345
+ activity()?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
346
+ return
347
+ }
348
+
349
+ // For a specific orientation: check if the device's auto-rotate toggle is ON.
350
+ // auto-rotate ON → use sensor-based variant so the device can still rotate
351
+ // within the requested axis (portrait ↔ reverse-portrait, etc.)
352
+ // auto-rotate OFF → hard-lock to the exact orientation requested
353
+ val autoRotateOn = Settings.System.getInt(
354
+ reactContext.contentResolver,
355
+ Settings.System.ACCELEROMETER_ROTATION,
356
+ 0 // default = locked (off)
357
+ ) == 1
358
+
211
359
  activity()?.requestedOrientation = when (mode) {
212
- "portrait" -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
213
- "landscape" -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
214
- "landscape-left" -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
215
- "landscape-right" -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
216
- // "auto" = respect system auto-rotate toggle
217
- else -> ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR
360
+ "portrait" ->
361
+ if (autoRotateOn) ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
362
+ else ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
363
+
364
+ "landscape" ->
365
+ if (autoRotateOn) ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
366
+ else ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
367
+
368
+ "landscape-left" ->
369
+ // Reverse landscape — no sensor variant; honour toggle by using full sensor landscape
370
+ if (autoRotateOn) ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
371
+ else ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
372
+
373
+ "landscape-right" ->
374
+ if (autoRotateOn) ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
375
+ else ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
376
+
377
+ else -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
218
378
  }
219
379
  }
220
380
 
221
381
  // ═══════════════════════════════════════════════
222
- // SCREENCAST — list all presentation displays
382
+ // SCREENCAST
223
383
  // ═══════════════════════════════════════════════
224
384
 
225
385
  private var displayListener: DisplayManager.DisplayListener? = null
226
386
 
227
387
  private fun buildDisplayListMap(): WritableMap {
228
- val dm = displayManager()
229
- val map = Arguments.createMap()
388
+ val dm = displayManager()
389
+ val map = Arguments.createMap()
230
390
  val displays = dm.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION)
231
- val arr = Arguments.createArray()
232
-
391
+ val arr = Arguments.createArray()
233
392
  for (d in displays) {
234
393
  val item = Arguments.createMap()
235
394
  item.putInt("id", d.displayId)
@@ -237,7 +396,6 @@ class SystemBarModule(
237
396
  item.putBoolean("isValid", d.isValid)
238
397
  arr.pushMap(item)
239
398
  }
240
-
241
399
  map.putBoolean("isCasting", displays.isNotEmpty())
242
400
  map.putArray("displays", arr)
243
401
  if (displays.isNotEmpty()) map.putString("displayName", displays[0].name)
@@ -271,6 +429,8 @@ class SystemBarModule(
271
429
 
272
430
  // ── Cleanup ──────────────────────────────────
273
431
  override fun onCatalystInstanceDestroy() {
432
+ stopBrightnessListener()
433
+ stopVolumeListener()
274
434
  stopScreencastListener()
275
435
  }
276
436
  }
@@ -1,17 +1,26 @@
1
1
  import type { NavigationBarBehavior, NavigationBarButtonStyle, NavigationBarStyle, NavigationBarVisibility, Orientation, ScreencastInfo, StatusBarStyle, VolumeStream } from "./types";
2
2
  export declare const setNavigationBarColor: (color: string) => void;
3
- export declare const setNavigationBarVisibility: (mode: NavigationBarVisibility) => Promise<void>;
4
- export declare const setNavigationBarButtonStyle: (style: NavigationBarButtonStyle) => Promise<void>;
5
- export declare const setNavigationBarStyle: (style: NavigationBarStyle) => Promise<void>;
6
- export declare const setNavigationBarBehavior: (behavior: NavigationBarBehavior) => Promise<void>;
7
- export declare const setStatusBarColor: (color: string, animated?: boolean) => void;
8
- export declare const setStatusBarStyle: (style: StatusBarStyle, animated?: boolean) => void;
9
- export declare const setStatusBarVisibility: (visible: boolean, animated?: boolean) => void;
3
+ export declare const setNavigationBarVisibility: (mode: NavigationBarVisibility) => void;
4
+ export declare const setNavigationBarButtonStyle: (style: NavigationBarButtonStyle) => void;
5
+ export declare const setNavigationBarStyle: (style: NavigationBarStyle) => void;
6
+ export declare const setNavigationBarBehavior: (behavior: NavigationBarBehavior) => void;
7
+ export declare const setStatusBarStyle: (style: StatusBarStyle) => void;
8
+ export declare const setStatusBarVisibility: (visible: boolean) => void;
10
9
  export declare const setBrightness: (level: number) => void;
11
10
  export declare const getBrightness: () => Promise<number>;
11
+ /**
12
+ * Subscribe to system brightness changes (polls every 500ms on Android).
13
+ * @returns unsubscribe function
14
+ */
15
+ export declare const onBrightnessChange: (callback: (brightness: number) => void) => (() => void);
12
16
  export declare const setVolume: (level: number, stream?: VolumeStream) => void;
13
17
  export declare const getVolume: (stream?: VolumeStream) => Promise<number>;
14
18
  export declare const setVolumeHUDVisible: (visible: boolean) => void;
19
+ /**
20
+ * Subscribe to system volume changes (hardware buttons, other apps).
21
+ * @returns unsubscribe function
22
+ */
23
+ export declare const onVolumeChange: (callback: (volume: number, stream: VolumeStream) => void) => (() => void);
15
24
  export declare const keepScreenOn: (enable: boolean) => void;
16
25
  export declare const immersiveMode: (enable: boolean) => void;
17
26
  export declare const setSecureScreen: (enable: boolean) => void;
@@ -1,18 +1,12 @@
1
1
  "use strict";
2
2
  // ─────────────────────────────────────────────
3
3
  // rn-system-bar · SystemBar.ts
4
+ // Zero expo-navigation-bar dependency.
5
+ // All APIs → native Kotlin / iOS Swift.
4
6
  // ─────────────────────────────────────────────
5
7
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.onScreencastChange = exports.getScreencastInfo = exports.setOrientation = exports.setSecureScreen = exports.immersiveMode = exports.keepScreenOn = exports.setVolumeHUDVisible = exports.getVolume = exports.setVolume = exports.getBrightness = exports.setBrightness = exports.setStatusBarVisibility = exports.setStatusBarStyle = exports.setStatusBarColor = exports.setNavigationBarBehavior = exports.setNavigationBarStyle = exports.setNavigationBarButtonStyle = exports.setNavigationBarVisibility = exports.setNavigationBarColor = void 0;
8
+ exports.onScreencastChange = exports.getScreencastInfo = exports.setOrientation = exports.setSecureScreen = exports.immersiveMode = exports.keepScreenOn = exports.onVolumeChange = exports.setVolumeHUDVisible = exports.getVolume = exports.setVolume = exports.onBrightnessChange = exports.getBrightness = exports.setBrightness = exports.setStatusBarVisibility = exports.setStatusBarStyle = exports.setNavigationBarBehavior = exports.setNavigationBarStyle = exports.setNavigationBarButtonStyle = exports.setNavigationBarVisibility = exports.setNavigationBarColor = void 0;
7
9
  const react_native_1 = require("react-native");
8
- const NavBar = (() => {
9
- try {
10
- return require("expo-navigation-bar");
11
- }
12
- catch (_a) {
13
- return null;
14
- }
15
- })();
16
10
  const { SystemBar: Native } = react_native_1.NativeModules;
17
11
  const isAndroid = react_native_1.Platform.OS === "android";
18
12
  const androidOnly = (name) => {
@@ -28,9 +22,7 @@ const checkNative = () => {
28
22
  throw new Error("[rn-system-bar] Native module not found. Rebuild your project.");
29
23
  };
30
24
  // ═══════════════════════════════════════════════
31
- // NAVIGATION BAR (Android-only)
32
- // Color → native Window API (edge-to-edge safe)
33
- // Others → expo-navigation-bar (work in edge-to-edge)
25
+ // NAVIGATION BAR (Android — 100% native)
34
26
  // ═══════════════════════════════════════════════
35
27
  const setNavigationBarColor = (color) => {
36
28
  if (!androidOnly("setNavigationBarColor"))
@@ -40,60 +32,48 @@ const setNavigationBarColor = (color) => {
40
32
  };
41
33
  exports.setNavigationBarColor = setNavigationBarColor;
42
34
  const setNavigationBarVisibility = (mode) => {
43
- var _a;
44
35
  if (!androidOnly("setNavigationBarVisibility"))
45
- return Promise.resolve();
46
- return (_a = NavBar === null || NavBar === void 0 ? void 0 : NavBar.setVisibilityAsync(mode)) !== null && _a !== void 0 ? _a : Promise.resolve();
36
+ return;
37
+ checkNative();
38
+ Native.setNavigationBarVisibility(mode);
47
39
  };
48
40
  exports.setNavigationBarVisibility = setNavigationBarVisibility;
49
41
  const setNavigationBarButtonStyle = (style) => {
50
- var _a;
51
42
  if (!androidOnly("setNavigationBarButtonStyle"))
52
- return Promise.resolve();
53
- return (_a = NavBar === null || NavBar === void 0 ? void 0 : NavBar.setButtonStyleAsync(style)) !== null && _a !== void 0 ? _a : Promise.resolve();
43
+ return;
44
+ checkNative();
45
+ Native.setNavigationBarButtonStyle(style);
54
46
  };
55
47
  exports.setNavigationBarButtonStyle = setNavigationBarButtonStyle;
56
48
  const setNavigationBarStyle = (style) => {
57
- var _a;
58
49
  if (!androidOnly("setNavigationBarStyle"))
59
- return Promise.resolve();
60
- const btn = style === "dark" || style === "auto" ? "dark" : "light";
61
- return (_a = NavBar === null || NavBar === void 0 ? void 0 : NavBar.setButtonStyleAsync(btn)) !== null && _a !== void 0 ? _a : Promise.resolve();
50
+ return;
51
+ checkNative();
52
+ Native.setNavigationBarStyle(style);
62
53
  };
63
54
  exports.setNavigationBarStyle = setNavigationBarStyle;
64
55
  const setNavigationBarBehavior = (behavior) => {
65
- var _a;
66
56
  if (!androidOnly("setNavigationBarBehavior"))
67
- return Promise.resolve();
68
- return (_a = NavBar === null || NavBar === void 0 ? void 0 : NavBar.setBehaviorAsync(behavior)) !== null && _a !== void 0 ? _a : Promise.resolve();
57
+ return;
58
+ checkNative();
59
+ Native.setNavigationBarBehavior(behavior);
69
60
  };
70
61
  exports.setNavigationBarBehavior = setNavigationBarBehavior;
71
62
  // ═══════════════════════════════════════════════
72
- // STATUS BAR
63
+ // STATUS BAR (native — no RN StatusBar)
73
64
  // ═══════════════════════════════════════════════
74
- const setStatusBarColor = (color, animated = false) => {
75
- if (!androidOnly("setStatusBarColor"))
76
- return;
77
- react_native_1.StatusBar.setBackgroundColor(color, animated);
78
- };
79
- exports.setStatusBarColor = setStatusBarColor;
80
- const setStatusBarStyle = (style, animated = false) => {
81
- react_native_1.StatusBar.setBarStyle(style === "light" ? "light-content" : "dark-content", animated);
65
+ const setStatusBarStyle = (style) => {
66
+ checkNative();
67
+ Native.setStatusBarStyle(style);
82
68
  };
83
69
  exports.setStatusBarStyle = setStatusBarStyle;
84
- const setStatusBarVisibility = (visible, animated = false) => {
85
- react_native_1.StatusBar.setHidden(!visible, animated ? "slide" : "none");
70
+ const setStatusBarVisibility = (visible) => {
71
+ checkNative();
72
+ Native.setStatusBarVisibility(visible);
86
73
  };
87
74
  exports.setStatusBarVisibility = setStatusBarVisibility;
88
75
  // ═══════════════════════════════════════════════
89
76
  // BRIGHTNESS
90
- //
91
- // setBrightness → updates BOTH window brightness (instant)
92
- // AND system brightness (persisted).
93
- // On first call: opens WRITE_SETTINGS if not granted.
94
- //
95
- // getBrightness → reads SYSTEM brightness so slider always
96
- // matches the device brightness bar.
97
77
  // ═══════════════════════════════════════════════
98
78
  const setBrightness = (level) => {
99
79
  checkNative();
@@ -105,6 +85,21 @@ const getBrightness = () => {
105
85
  return Native.getBrightness();
106
86
  };
107
87
  exports.getBrightness = getBrightness;
88
+ /**
89
+ * Subscribe to system brightness changes (polls every 500ms on Android).
90
+ * @returns unsubscribe function
91
+ */
92
+ const onBrightnessChange = (callback) => {
93
+ checkNative();
94
+ const { DeviceEventEmitter } = require("react-native");
95
+ Native.startBrightnessListener();
96
+ const sub = DeviceEventEmitter.addListener("SystemBar_BrightnessChange", (e) => callback(e.brightness));
97
+ return () => {
98
+ sub.remove();
99
+ Native.stopBrightnessListener();
100
+ };
101
+ };
102
+ exports.onBrightnessChange = onBrightnessChange;
108
103
  // ═══════════════════════════════════════════════
109
104
  // VOLUME
110
105
  // ═══════════════════════════════════════════════
@@ -125,6 +120,21 @@ const setVolumeHUDVisible = (visible) => {
125
120
  Native.setVolumeHUDVisible(visible);
126
121
  };
127
122
  exports.setVolumeHUDVisible = setVolumeHUDVisible;
123
+ /**
124
+ * Subscribe to system volume changes (hardware buttons, other apps).
125
+ * @returns unsubscribe function
126
+ */
127
+ const onVolumeChange = (callback) => {
128
+ checkNative();
129
+ const { DeviceEventEmitter } = require("react-native");
130
+ Native.startVolumeListener();
131
+ const sub = DeviceEventEmitter.addListener("SystemBar_VolumeChange", (e) => callback(e.volume, e.stream));
132
+ return () => {
133
+ sub.remove();
134
+ Native.stopVolumeListener();
135
+ };
136
+ };
137
+ exports.onVolumeChange = onVolumeChange;
128
138
  // ═══════════════════════════════════════════════
129
139
  // SCREEN
130
140
  // ═══════════════════════════════════════════════
@@ -47,24 +47,21 @@ const useSystemBar = (config) => {
47
47
  return;
48
48
  configRef.current = configStr;
49
49
  const apply = async () => {
50
- const p = [];
50
+ // All nav bar calls are now sync (no expo-navigation-bar)
51
51
  if (config.navigationBarColor !== undefined)
52
52
  SystemBar.setNavigationBarColor(config.navigationBarColor);
53
53
  if (config.navigationBarVisibility !== undefined)
54
- p.push(SystemBar.setNavigationBarVisibility(config.navigationBarVisibility));
54
+ SystemBar.setNavigationBarVisibility(config.navigationBarVisibility);
55
55
  if (config.navigationBarButtonStyle !== undefined)
56
- p.push(SystemBar.setNavigationBarButtonStyle(config.navigationBarButtonStyle));
56
+ SystemBar.setNavigationBarButtonStyle(config.navigationBarButtonStyle);
57
57
  if (config.navigationBarStyle !== undefined)
58
- p.push(SystemBar.setNavigationBarStyle(config.navigationBarStyle));
58
+ SystemBar.setNavigationBarStyle(config.navigationBarStyle);
59
59
  if (config.navigationBarBehavior !== undefined)
60
- p.push(SystemBar.setNavigationBarBehavior(config.navigationBarBehavior));
61
- await Promise.allSettled(p);
62
- if (config.statusBarColor !== undefined)
63
- SystemBar.setStatusBarColor(config.statusBarColor, config.statusBarAnimated);
60
+ SystemBar.setNavigationBarBehavior(config.navigationBarBehavior);
64
61
  if (config.statusBarStyle !== undefined)
65
- SystemBar.setStatusBarStyle(config.statusBarStyle, config.statusBarAnimated);
62
+ SystemBar.setStatusBarStyle(config.statusBarStyle);
66
63
  if (config.statusBarVisible !== undefined)
67
- SystemBar.setStatusBarVisibility(config.statusBarVisible, config.statusBarAnimated);
64
+ SystemBar.setStatusBarVisibility(config.statusBarVisible);
68
65
  if (config.keepScreenOn !== undefined)
69
66
  SystemBar.keepScreenOn(config.keepScreenOn);
70
67
  if (config.immersiveMode !== undefined)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rn-system-bar",
3
- "version": "3.1.3",
3
+ "version": "3.1.5",
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
@@ -1,8 +1,10 @@
1
1
  // ─────────────────────────────────────────────
2
2
  // rn-system-bar · SystemBar.ts
3
+ // Zero expo-navigation-bar dependency.
4
+ // All APIs → native Kotlin / iOS Swift.
3
5
  // ─────────────────────────────────────────────
4
6
 
5
- import { NativeModules, Platform, StatusBar } from "react-native";
7
+ import { NativeModules, Platform } from "react-native";
6
8
 
7
9
  import type {
8
10
  NavigationBarBehavior,
@@ -15,11 +17,6 @@ import type {
15
17
  VolumeStream,
16
18
  } from "./types";
17
19
 
18
- const NavBar = (() => {
19
- try { return require("expo-navigation-bar"); }
20
- catch { return null; }
21
- })();
22
-
23
20
  const { SystemBar: Native } = NativeModules;
24
21
 
25
22
  const isAndroid = Platform.OS === "android";
@@ -37,9 +34,7 @@ const checkNative = () => {
37
34
  };
38
35
 
39
36
  // ═══════════════════════════════════════════════
40
- // NAVIGATION BAR (Android-only)
41
- // Color → native Window API (edge-to-edge safe)
42
- // Others → expo-navigation-bar (work in edge-to-edge)
37
+ // NAVIGATION BAR (Android — 100% native)
43
38
  // ═══════════════════════════════════════════════
44
39
 
45
40
  export const setNavigationBarColor = (color: string): void => {
@@ -48,53 +43,46 @@ export const setNavigationBarColor = (color: string): void => {
48
43
  Native.setNavigationBarColor(color);
49
44
  };
50
45
 
51
- export const setNavigationBarVisibility = (mode: NavigationBarVisibility): Promise<void> => {
52
- if (!androidOnly("setNavigationBarVisibility")) return Promise.resolve();
53
- return NavBar?.setVisibilityAsync(mode) ?? Promise.resolve();
46
+ export const setNavigationBarVisibility = (mode: NavigationBarVisibility): void => {
47
+ if (!androidOnly("setNavigationBarVisibility")) return;
48
+ checkNative();
49
+ Native.setNavigationBarVisibility(mode);
54
50
  };
55
51
 
56
- export const setNavigationBarButtonStyle = (style: NavigationBarButtonStyle): Promise<void> => {
57
- if (!androidOnly("setNavigationBarButtonStyle")) return Promise.resolve();
58
- return NavBar?.setButtonStyleAsync(style) ?? Promise.resolve();
52
+ export const setNavigationBarButtonStyle = (style: NavigationBarButtonStyle): void => {
53
+ if (!androidOnly("setNavigationBarButtonStyle")) return;
54
+ checkNative();
55
+ Native.setNavigationBarButtonStyle(style);
59
56
  };
60
57
 
61
- export const setNavigationBarStyle = (style: NavigationBarStyle): Promise<void> => {
62
- if (!androidOnly("setNavigationBarStyle")) return Promise.resolve();
63
- const btn: NavigationBarButtonStyle = style === "dark" || style === "auto" ? "dark" : "light";
64
- return NavBar?.setButtonStyleAsync(btn) ?? Promise.resolve();
58
+ export const setNavigationBarStyle = (style: NavigationBarStyle): void => {
59
+ if (!androidOnly("setNavigationBarStyle")) return;
60
+ checkNative();
61
+ Native.setNavigationBarStyle(style);
65
62
  };
66
63
 
67
- export const setNavigationBarBehavior = (behavior: NavigationBarBehavior): Promise<void> => {
68
- if (!androidOnly("setNavigationBarBehavior")) return Promise.resolve();
69
- return NavBar?.setBehaviorAsync(behavior) ?? Promise.resolve();
64
+ export const setNavigationBarBehavior = (behavior: NavigationBarBehavior): void => {
65
+ if (!androidOnly("setNavigationBarBehavior")) return;
66
+ checkNative();
67
+ Native.setNavigationBarBehavior(behavior);
70
68
  };
71
69
 
72
70
  // ═══════════════════════════════════════════════
73
- // STATUS BAR
71
+ // STATUS BAR (native — no RN StatusBar)
74
72
  // ═══════════════════════════════════════════════
75
73
 
76
- export const setStatusBarColor = (color: string, animated = false): void => {
77
- if (!androidOnly("setStatusBarColor")) return;
78
- StatusBar.setBackgroundColor(color, animated);
79
- };
80
-
81
- export const setStatusBarStyle = (style: StatusBarStyle, animated = false): void => {
82
- StatusBar.setBarStyle(style === "light" ? "light-content" : "dark-content", animated);
74
+ export const setStatusBarStyle = (style: StatusBarStyle): void => {
75
+ checkNative();
76
+ Native.setStatusBarStyle(style);
83
77
  };
84
78
 
85
- export const setStatusBarVisibility = (visible: boolean, animated = false): void => {
86
- StatusBar.setHidden(!visible, animated ? "slide" : "none");
79
+ export const setStatusBarVisibility = (visible: boolean): void => {
80
+ checkNative();
81
+ Native.setStatusBarVisibility(visible);
87
82
  };
88
83
 
89
84
  // ═══════════════════════════════════════════════
90
85
  // BRIGHTNESS
91
- //
92
- // setBrightness → updates BOTH window brightness (instant)
93
- // AND system brightness (persisted).
94
- // On first call: opens WRITE_SETTINGS if not granted.
95
- //
96
- // getBrightness → reads SYSTEM brightness so slider always
97
- // matches the device brightness bar.
98
86
  // ═══════════════════════════════════════════════
99
87
 
100
88
  export const setBrightness = (level: number): void => {
@@ -107,6 +95,26 @@ export const getBrightness = (): Promise<number> => {
107
95
  return Native.getBrightness();
108
96
  };
109
97
 
98
+ /**
99
+ * Subscribe to system brightness changes (polls every 500ms on Android).
100
+ * @returns unsubscribe function
101
+ */
102
+ export const onBrightnessChange = (
103
+ callback: (brightness: number) => void
104
+ ): (() => void) => {
105
+ checkNative();
106
+ const { DeviceEventEmitter } = require("react-native");
107
+ Native.startBrightnessListener();
108
+ const sub = DeviceEventEmitter.addListener(
109
+ "SystemBar_BrightnessChange",
110
+ (e: { brightness: number }) => callback(e.brightness)
111
+ );
112
+ return () => {
113
+ sub.remove();
114
+ Native.stopBrightnessListener();
115
+ };
116
+ };
117
+
110
118
  // ═══════════════════════════════════════════════
111
119
  // VOLUME
112
120
  // ═══════════════════════════════════════════════
@@ -127,6 +135,26 @@ export const setVolumeHUDVisible = (visible: boolean): void => {
127
135
  Native.setVolumeHUDVisible(visible);
128
136
  };
129
137
 
138
+ /**
139
+ * Subscribe to system volume changes (hardware buttons, other apps).
140
+ * @returns unsubscribe function
141
+ */
142
+ export const onVolumeChange = (
143
+ callback: (volume: number, stream: VolumeStream) => void
144
+ ): (() => void) => {
145
+ checkNative();
146
+ const { DeviceEventEmitter } = require("react-native");
147
+ Native.startVolumeListener();
148
+ const sub = DeviceEventEmitter.addListener(
149
+ "SystemBar_VolumeChange",
150
+ (e: { volume: number; stream: VolumeStream }) => callback(e.volume, e.stream)
151
+ );
152
+ return () => {
153
+ sub.remove();
154
+ Native.stopVolumeListener();
155
+ };
156
+ };
157
+
130
158
  // ═══════════════════════════════════════════════
131
159
  // SCREEN
132
160
  // ═══════════════════════════════════════════════
@@ -44,24 +44,19 @@ export const useSystemBar = (config: SystemBarConfig) => {
44
44
  configRef.current = configStr;
45
45
 
46
46
  const apply = async () => {
47
- const p: Promise<void>[] = [];
48
-
47
+ // All nav bar calls are now sync (no expo-navigation-bar)
49
48
  if (config.navigationBarColor !== undefined) SystemBar.setNavigationBarColor(config.navigationBarColor);
50
- if (config.navigationBarVisibility !== undefined) p.push(SystemBar.setNavigationBarVisibility(config.navigationBarVisibility));
51
- if (config.navigationBarButtonStyle !== undefined) p.push(SystemBar.setNavigationBarButtonStyle(config.navigationBarButtonStyle));
52
- if (config.navigationBarStyle !== undefined) p.push(SystemBar.setNavigationBarStyle(config.navigationBarStyle));
53
- if (config.navigationBarBehavior !== undefined) p.push(SystemBar.setNavigationBarBehavior(config.navigationBarBehavior));
54
-
55
- await Promise.allSettled(p);
56
-
57
- if (config.statusBarColor !== undefined) SystemBar.setStatusBarColor(config.statusBarColor, config.statusBarAnimated);
58
- if (config.statusBarStyle !== undefined) SystemBar.setStatusBarStyle(config.statusBarStyle, config.statusBarAnimated);
59
- if (config.statusBarVisible !== undefined) SystemBar.setStatusBarVisibility(config.statusBarVisible, config.statusBarAnimated);
60
- if (config.keepScreenOn !== undefined) SystemBar.keepScreenOn(config.keepScreenOn);
61
- if (config.immersiveMode !== undefined) SystemBar.immersiveMode(config.immersiveMode);
62
- if (config.brightness !== undefined) SystemBar.setBrightness(config.brightness);
63
- if (config.orientation !== undefined) SystemBar.setOrientation(config.orientation);
64
- if (config.secureScreen !== undefined) SystemBar.setSecureScreen(config.secureScreen);
49
+ if (config.navigationBarVisibility !== undefined) SystemBar.setNavigationBarVisibility(config.navigationBarVisibility);
50
+ if (config.navigationBarButtonStyle !== undefined) SystemBar.setNavigationBarButtonStyle(config.navigationBarButtonStyle);
51
+ if (config.navigationBarStyle !== undefined) SystemBar.setNavigationBarStyle(config.navigationBarStyle);
52
+ if (config.navigationBarBehavior !== undefined) SystemBar.setNavigationBarBehavior(config.navigationBarBehavior);
53
+ if (config.statusBarStyle !== undefined) SystemBar.setStatusBarStyle(config.statusBarStyle);
54
+ if (config.statusBarVisible !== undefined) SystemBar.setStatusBarVisibility(config.statusBarVisible);
55
+ if (config.keepScreenOn !== undefined) SystemBar.keepScreenOn(config.keepScreenOn);
56
+ if (config.immersiveMode !== undefined) SystemBar.immersiveMode(config.immersiveMode);
57
+ if (config.brightness !== undefined) SystemBar.setBrightness(config.brightness);
58
+ if (config.orientation !== undefined) SystemBar.setOrientation(config.orientation);
59
+ if (config.secureScreen !== undefined) SystemBar.setSecureScreen(config.secureScreen);
65
60
  };
66
61
 
67
62
  apply().catch(() => {