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.
@@ -2,32 +2,44 @@
2
2
  // rn-system-bar · useSystemBar.ts
3
3
  // ─────────────────────────────────────────────
4
4
 
5
- import { useEffect, useRef, useState } from "react";
5
+ import { useCallback, useEffect, useRef, useState } from "react";
6
+ import * as SystemBar from "./SystemBar";
6
7
  import type {
8
+ AppCastInfo,
7
9
  NavigationBarBehavior,
8
10
  NavigationBarButtonStyle,
11
+ NavigationBarColorValue,
9
12
  NavigationBarStyle,
10
13
  NavigationBarVisibility,
11
14
  Orientation,
12
15
  ScreencastInfo,
16
+ StatusBarColorValue,
13
17
  StatusBarStyle,
18
+ SystemScreencastInfo,
19
+ ThemeMode,
20
+ ThemeState,
14
21
  } from "./types";
15
- import * as SystemBar from "./SystemBar";
22
+ import { useTheme } from "./useTheme";
16
23
 
17
24
  // ─────────────────────────────────────────────
18
- // useSystemBar — apply system bar config safely
19
- // All async calls run inside useEffect — no Suspense crash
25
+ // SystemBarConfig
20
26
  // ─────────────────────────────────────────────
27
+
21
28
  export interface SystemBarConfig {
22
- navigationBarColor?: string;
29
+ // Navigation bar (Android-only)
30
+ navigationBarColor?: NavigationBarColorValue;
23
31
  navigationBarVisibility?: NavigationBarVisibility;
24
32
  navigationBarButtonStyle?: NavigationBarButtonStyle;
25
33
  navigationBarStyle?: NavigationBarStyle;
26
34
  navigationBarBehavior?: NavigationBarBehavior;
27
- statusBarColor?: string;
35
+
36
+ // Status bar
37
+ statusBarColor?: StatusBarColorValue;
28
38
  statusBarStyle?: StatusBarStyle;
29
39
  statusBarVisible?: boolean;
30
40
  statusBarAnimated?: boolean;
41
+
42
+ // Screen
31
43
  keepScreenOn?: boolean;
32
44
  immersiveMode?: boolean;
33
45
  brightness?: number;
@@ -35,7 +47,20 @@ export interface SystemBarConfig {
35
47
  secureScreen?: boolean;
36
48
  }
37
49
 
38
- export const useSystemBar = (config: SystemBarConfig) => {
50
+ // ─────────────────────────────────────────────
51
+ // useSystemBar
52
+ // Apply system bar settings declaratively.
53
+ //
54
+ // @example
55
+ // useSystemBar({
56
+ // navigationBarColor: "transparent",
57
+ // navigationBarButtonStyle: "light",
58
+ // statusBarStyle: "light",
59
+ // keepScreenOn: true,
60
+ // });
61
+ // ─────────────────────────────────────────────
62
+
63
+ export const useSystemBar = (config: SystemBarConfig): void => {
39
64
  const configRef = useRef("");
40
65
  const configStr = JSON.stringify(config);
41
66
 
@@ -44,31 +69,122 @@ export const useSystemBar = (config: SystemBarConfig) => {
44
69
  configRef.current = configStr;
45
70
 
46
71
  const apply = async () => {
47
- // All nav bar calls are now sync (no expo-navigation-bar)
48
- if (config.navigationBarColor !== undefined) SystemBar.setNavigationBarColor(config.navigationBarColor);
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);
72
+ if (config.navigationBarColor !== undefined)
73
+ SystemBar.setNavigationBarColor(config.navigationBarColor);
74
+ if (config.navigationBarVisibility !== undefined)
75
+ SystemBar.setNavigationBarVisibility(config.navigationBarVisibility);
76
+ if (config.navigationBarButtonStyle !== undefined)
77
+ SystemBar.setNavigationBarButtonStyle(config.navigationBarButtonStyle);
78
+ if (config.navigationBarStyle !== undefined)
79
+ SystemBar.setNavigationBarStyle(config.navigationBarStyle);
80
+ if (config.navigationBarBehavior !== undefined)
81
+ SystemBar.setNavigationBarBehavior(config.navigationBarBehavior);
82
+ if (config.statusBarColor !== undefined)
83
+ SystemBar.setStatusBarColor(config.statusBarColor);
84
+ if (config.statusBarStyle !== undefined)
85
+ SystemBar.setStatusBarStyle(config.statusBarStyle);
86
+ if (config.statusBarVisible !== undefined)
87
+ SystemBar.setStatusBarVisibility(config.statusBarVisible);
88
+ if (config.keepScreenOn !== undefined)
89
+ SystemBar.keepScreenOn(config.keepScreenOn);
90
+ if (config.immersiveMode !== undefined)
91
+ SystemBar.immersiveMode(config.immersiveMode);
92
+ if (config.brightness !== undefined)
93
+ SystemBar.setBrightness(config.brightness);
94
+ if (config.orientation !== undefined)
95
+ SystemBar.setOrientation(config.orientation);
96
+ if (config.secureScreen !== undefined)
97
+ SystemBar.setSecureScreen(config.secureScreen);
60
98
  };
61
99
 
62
100
  apply().catch(() => {
63
- if (__DEV__) console.warn("[rn-system-bar] useSystemBar: one or more settings failed.");
101
+ if (__DEV__)
102
+ console.warn(
103
+ "[rn-system-bar] useSystemBar: one or more settings failed.",
104
+ );
64
105
  });
65
- // eslint-disable-next-line react-hooks/exhaustive-deps
106
+ // eslint-disable-next-line react-hooks/exhaustive-deps
66
107
  }, [configStr]);
67
108
  };
68
109
 
69
110
  // ─────────────────────────────────────────────
70
- // useScreencast
111
+ // ThemedSystemBarConfig + useThemeSystemBar
112
+ //
113
+ // Combines useTheme() + useSystemBar() — nav/status bars
114
+ // automatically update whenever the theme changes (OS or manual).
115
+ //
116
+ // @example
117
+ // const { isDark, mode, setMode } = useThemeSystemBar({
118
+ // dark: { navigationBarColor: "#000000", statusBarStyle: "light" },
119
+ // light: { navigationBarColor: "#ffffff", statusBarStyle: "dark" },
120
+ // base: { keepScreenOn: true },
121
+ // });
71
122
  // ─────────────────────────────────────────────
123
+
124
+ export interface ThemedSystemBarConfig {
125
+ /** Config applied when the resolved theme is DARK. */
126
+ dark?: SystemBarConfig;
127
+ /** Config applied when the resolved theme is LIGHT. */
128
+ light?: SystemBarConfig;
129
+ /**
130
+ * Static config merged under both themes.
131
+ * Theme-specific (dark/light) values always win.
132
+ */
133
+ base?: SystemBarConfig;
134
+ }
135
+
136
+ export const useThemeSystemBar = (
137
+ config: ThemedSystemBarConfig,
138
+ ): ThemeState => {
139
+ const theme = useTheme();
140
+
141
+ const resolved: SystemBarConfig = {
142
+ ...(config.base ?? {}),
143
+ ...(theme.isDark ? (config.dark ?? {}) : (config.light ?? {})),
144
+ };
145
+
146
+ useSystemBar(resolved);
147
+
148
+ return theme;
149
+ };
150
+
151
+ // ─────────────────────────────────────────────
152
+ // Re-export useTheme + theme types
153
+ // ─────────────────────────────────────────────
154
+
155
+ export { useTheme } from "./useTheme";
156
+ export type { ThemeMode, ThemeState };
157
+
158
+ // ─────────────────────────────────────────────
159
+ // useSystemScreencast
160
+ // Tracks OS-level external display state.
161
+ // Android: DisplayManager (HDMI / Miracast / Presentation display).
162
+ // iOS: UIScreen.screens (AirPlay mirror).
163
+ //
164
+ // @example
165
+ // const { isCasting, displayName, displays } = useSystemScreencast();
166
+ // ─────────────────────────────────────────────
167
+
168
+ export const useSystemScreencast = (): SystemScreencastInfo => {
169
+ const [info, setInfo] = useState<SystemScreencastInfo>({
170
+ isCasting: false,
171
+ displayName: null,
172
+ displays: [],
173
+ });
174
+
175
+ useEffect(() => {
176
+ SystemBar.getSystemScreencastInfo()
177
+ .then(setInfo)
178
+ .catch(() => {});
179
+ const unsub = SystemBar.onSystemScreencastChange(setInfo);
180
+ return () => unsub();
181
+ }, []);
182
+
183
+ return info;
184
+ };
185
+
186
+ /** @deprecated Renamed to useSystemScreencast() */
187
+ // useSystemBar.ts — useScreencast
72
188
  export const useScreencast = () => {
73
189
  const [info, setInfo] = useState<ScreencastInfo>({
74
190
  isCasting: false,
@@ -77,10 +193,102 @@ export const useScreencast = () => {
77
193
  });
78
194
 
79
195
  useEffect(() => {
80
- SystemBar.getScreencastInfo().then(setInfo).catch(() => {});
81
- const unsub = SystemBar.onScreencastChange(setInfo);
82
- return () => unsub();
196
+ // Guard: if method doesn't exist on native module, bail silently
197
+ SystemBar.getScreencastInfo()
198
+ .then(setInfo)
199
+ .catch(() => {
200
+ if (__DEV__)
201
+ console.warn(
202
+ "[rn-system-bar] getSystemScreencastInfo not available. Rebuild native.",
203
+ );
204
+ });
205
+
206
+ let unsub: (() => void) | undefined;
207
+ try {
208
+ unsub = SystemBar.onScreencastChange(setInfo);
209
+ } catch {
210
+ // native listener not available
211
+ }
212
+ return () => unsub?.();
83
213
  }, []);
84
214
 
85
215
  return info;
86
216
  };
217
+
218
+ // ─────────────────────────────────────────────
219
+ // UseAppCastReturn + useAppCast
220
+ //
221
+ // In-app device discovery + casting via Android MediaRouter.
222
+ // Mirrors ONLY this app's screen — NOT the whole system.
223
+ // iOS always returns idle (AirPlay is system-managed, no public API).
224
+ //
225
+ // Flow:
226
+ // 1. Mount the component — calls getAppCastInfo() for initial state
227
+ // 2. call scan() → state = "scanning", devices populate
228
+ // 3. call connect(device.id) → state = "connecting" → "connected"
229
+ // 4. call disconnect() → state = "disconnecting" → "idle"
230
+ // 5. Unmount → scan auto-stopped (battery safe)
231
+ //
232
+ // @example
233
+ // const { state, devices, connectedDevice, error,
234
+ // scan, stopScan, connect, disconnect } = useAppCast();
235
+ //
236
+ // <Button title="Find TVs" onPress={scan} />
237
+ // {devices.map(d => (
238
+ // <Button key={d.id} title={`Cast to ${d.name}`}
239
+ // onPress={() => connect(d.id)} />
240
+ // ))}
241
+ // {connectedDevice && (
242
+ // <Button title="Stop casting" onPress={disconnect} />
243
+ // )}
244
+ // ─────────────────────────────────────────────
245
+
246
+ export interface UseAppCastReturn extends AppCastInfo {
247
+ /** Start scanning for nearby castable devices (TV, Chromecast, etc.). */
248
+ scan: () => void;
249
+ /** Stop the device discovery scan. */
250
+ stopScan: () => void;
251
+ /**
252
+ * Connect to a discovered device and begin casting this app's screen.
253
+ * @param deviceId The `id` field from `AppCastDevice`.
254
+ * @param pairingPin Optional PIN string if `requiresPairing` is true.
255
+ */
256
+ connect: (deviceId: string, pairingPin?: string) => void;
257
+ /** End the active cast session and return to idle. */
258
+ disconnect: () => void;
259
+ }
260
+
261
+ export const useAppCast = (): UseAppCastReturn => {
262
+ const [info, setInfo] = useState<AppCastInfo>({
263
+ state: "idle",
264
+ devices: [],
265
+ connectedDevice: null,
266
+ error: null,
267
+ });
268
+
269
+ useEffect(() => {
270
+ // Sync initial state
271
+ SystemBar.getAppCastInfo()
272
+ .then(setInfo)
273
+ .catch(() => {});
274
+
275
+ // Subscribe to all cast events (device found/lost, state changes, errors)
276
+ const unsub = SystemBar.onAppCastChange(setInfo);
277
+
278
+ return () => {
279
+ unsub();
280
+ // Auto-stop scan on unmount — avoids background battery drain
281
+ SystemBar.stopAppCastScan();
282
+ };
283
+ }, []);
284
+
285
+ const scan = useCallback(() => SystemBar.startAppCastScan(), []);
286
+ const stopScan = useCallback(() => SystemBar.stopAppCastScan(), []);
287
+ const connect = useCallback(
288
+ (id: string, pin?: string) => SystemBar.connectAppCast(id, pin),
289
+ [],
290
+ );
291
+ const disconnect = useCallback(() => SystemBar.disconnectAppCast(), []);
292
+
293
+ return { ...info, scan, stopScan, connect, disconnect };
294
+ };
@@ -0,0 +1,95 @@
1
+ // ─────────────────────────────────────────────
2
+ // rn-system-bar · useTheme.ts
3
+ //
4
+ // Reads the OS colour scheme via React Native's
5
+ // `Appearance` API and lets the user override it
6
+ // with "dark" | "light" | "system".
7
+ //
8
+ // Usage:
9
+ // const { isDark, mode, setMode } = useTheme();
10
+ // const { isDark } = useTheme(); // read-only
11
+ // ─────────────────────────────────────────────
12
+
13
+ import { useCallback, useEffect, useState } from "react";
14
+ import { Appearance, ColorSchemeName } from "react-native";
15
+ import type { ThemeMode, ThemeState } from "./types";
16
+
17
+ // ── Module-level singleton so all consumers share state ──────────────────────
18
+
19
+ let _mode: ThemeMode = "system";
20
+ let _systemIsDark: boolean = Appearance.getColorScheme() === "dark";
21
+
22
+ type Listener = (state: { isDark: boolean; mode: ThemeMode }) => void;
23
+ const _listeners = new Set<Listener>();
24
+
25
+ function _notify() {
26
+ const isDark = resolveIsDark(_mode, _systemIsDark);
27
+ _listeners.forEach((fn) => fn({ isDark, mode: _mode }));
28
+ }
29
+
30
+ function resolveIsDark(mode: ThemeMode, systemIsDark: boolean): boolean {
31
+ if (mode === "dark") return true;
32
+ if (mode === "light") return false;
33
+ return systemIsDark; // "system"
34
+ }
35
+
36
+ // Keep in sync with OS changes
37
+ Appearance.addChangeListener(
38
+ ({ colorScheme }: { colorScheme: ColorSchemeName }) => {
39
+ _systemIsDark = colorScheme === "dark";
40
+ _notify();
41
+ },
42
+ );
43
+
44
+ // ── Public API ───────────────────────────────────────────────────────────────
45
+
46
+ /**
47
+ * Set the global theme mode from anywhere (outside a component).
48
+ * Triggers all `useTheme()` consumers.
49
+ */
50
+ export function setGlobalThemeMode(mode: ThemeMode): void {
51
+ _mode = mode;
52
+ _notify();
53
+ }
54
+
55
+ /**
56
+ * React hook — subscribe to theme state.
57
+ *
58
+ * @example
59
+ * const { isDark, mode, setMode } = useTheme();
60
+ *
61
+ * // Auto nav bar colour based on theme
62
+ * useEffect(() => {
63
+ * setNavigationBarColor(isDark ? "#000000" : "#ffffff");
64
+ * setNavigationBarButtonStyle(isDark ? "light" : "dark");
65
+ * }, [isDark]);
66
+ *
67
+ * // Manual override
68
+ * <Button onPress={() => setMode("dark")} title="Force Dark" />
69
+ * <Button onPress={() => setMode("light")} title="Force Light" />
70
+ * <Button onPress={() => setMode("system")}title="Follow OS" />
71
+ */
72
+ export function useTheme(): ThemeState {
73
+ const [state, setState] = useState<{ isDark: boolean; mode: ThemeMode }>(
74
+ () => ({
75
+ isDark: resolveIsDark(_mode, _systemIsDark),
76
+ mode: _mode,
77
+ }),
78
+ );
79
+
80
+ useEffect(() => {
81
+ // Sync on mount in case singleton changed between renders
82
+ setState({ isDark: resolveIsDark(_mode, _systemIsDark), mode: _mode });
83
+
84
+ _listeners.add(setState);
85
+ return () => {
86
+ _listeners.delete(setState);
87
+ };
88
+ }, []);
89
+
90
+ const setMode = useCallback((newMode: ThemeMode) => {
91
+ setGlobalThemeMode(newMode);
92
+ }, []);
93
+
94
+ return { ...state, setMode };
95
+ }