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/src/types.ts CHANGED
@@ -12,6 +12,45 @@ export type NavigationBarStyle = "auto" | "inverted" | "light" | "dark";
12
12
  export type NavigationBarVisibility = "visible" | "hidden";
13
13
  export type StatusBarStyle = "light" | "dark";
14
14
 
15
+ /**
16
+ * Navigation bar color value.
17
+ *
18
+ * - Any CSS hex string → solid colour e.g. "#1a1a2e", "#000"
19
+ * - `"transparent"` → fully transparent (FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS +
20
+ * transparent color; content draws behind bar)
21
+ * - `"translucent"` → semi-transparent (FLAG_TRANSLUCENT_NAVIGATION)
22
+ */
23
+ export type NavigationBarColorValue = string | "transparent" | "translucent";
24
+
25
+ /**
26
+ * Status bar background color value.
27
+ *
28
+ * - Any CSS hex string → solid colour
29
+ * - `"transparent"` → fully transparent (draws behind content)
30
+ * - `"translucent"` → semi-transparent
31
+ */
32
+ export type StatusBarColorValue = string | "transparent" | "translucent";
33
+
34
+ // ─────────────────────────────────────────────
35
+ // Theme
36
+ // ─────────────────────────────────────────────
37
+
38
+ /**
39
+ * "system" → follow the OS Appearance (dark/light).
40
+ * "dark" → force dark.
41
+ * "light" → force light.
42
+ */
43
+ export type ThemeMode = "system" | "dark" | "light";
44
+
45
+ export interface ThemeState {
46
+ /** Resolved dark-mode flag. Always a concrete boolean. */
47
+ isDark: boolean;
48
+ /** Currently active override ("system" = following OS). */
49
+ mode: ThemeMode;
50
+ /** Set the override mode. Pass "system" to revert to OS. */
51
+ setMode: (mode: ThemeMode) => void;
52
+ }
53
+
15
54
  export type Orientation =
16
55
  | "portrait"
17
56
  | "landscape"
@@ -26,14 +65,59 @@ export type VolumeStream =
26
65
  | "alarm"
27
66
  | "system";
28
67
 
68
+ // ─────────────────────────────────────────────
69
+ // System screencast (external display / HDMI)
70
+ // ─────────────────────────────────────────────
71
+
29
72
  export interface ScreencastDisplay {
30
73
  id: number;
31
74
  name: string;
32
75
  isValid: boolean;
33
76
  }
34
77
 
35
- export interface ScreencastInfo {
78
+ /** Result of getSystemScreencastInfo() — physical/HDMI/Miracast external display. */
79
+ export interface SystemScreencastInfo {
36
80
  isCasting: boolean;
37
81
  displayName: string | null;
38
82
  displays: ScreencastDisplay[];
39
83
  }
84
+
85
+ // ─────────────────────────────────────────────
86
+ // App-only cast (MediaRouter — Chromecast / TV)
87
+ // ─────────────────────────────────────────────
88
+
89
+ /** Connection state of the in-app cast session. */
90
+ export type AppCastState =
91
+ | "idle"
92
+ | "scanning"
93
+ | "connecting"
94
+ | "connected"
95
+ | "disconnecting";
96
+
97
+ /** A discovered castable device (TV, Chromecast, etc.). */
98
+ export interface AppCastDevice {
99
+ /** Unique route ID from MediaRouter. */
100
+ id: string;
101
+ /** Human-readable device name e.g. "Living Room TV". */
102
+ name: string;
103
+ /** Device description / model string (may be null). */
104
+ description: string | null;
105
+ /** Signal strength 0–100, or null if unavailable. */
106
+ signalStrength: number | null;
107
+ /** Whether a pairing/PIN step is required. */
108
+ requiresPairing: boolean;
109
+ }
110
+
111
+ /** Full snapshot of in-app cast state. */
112
+ export interface AppCastInfo {
113
+ state: AppCastState;
114
+ /** Devices found during the last scan. */
115
+ devices: AppCastDevice[];
116
+ /** The device currently connected (or connecting). */
117
+ connectedDevice: AppCastDevice | null;
118
+ /** Error message from the last failed operation. */
119
+ error: string | null;
120
+ }
121
+
122
+ /** @deprecated Use SystemScreencastInfo */
123
+ export interface ScreencastInfo extends SystemScreencastInfo {}
@@ -2,32 +2,43 @@
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
- ScreencastInfo,
15
+ StatusBarColorValue,
13
16
  StatusBarStyle,
17
+ SystemScreencastInfo,
18
+ ThemeMode,
19
+ ThemeState,
14
20
  } from "./types";
15
- import * as SystemBar from "./SystemBar";
21
+ import { useTheme } from "./useTheme";
16
22
 
17
23
  // ─────────────────────────────────────────────
18
- // useSystemBar — apply system bar config safely
19
- // All async calls run inside useEffect — no Suspense crash
24
+ // SystemBarConfig
20
25
  // ─────────────────────────────────────────────
26
+
21
27
  export interface SystemBarConfig {
22
- navigationBarColor?: string;
28
+ // Navigation bar (Android-only)
29
+ navigationBarColor?: NavigationBarColorValue;
23
30
  navigationBarVisibility?: NavigationBarVisibility;
24
31
  navigationBarButtonStyle?: NavigationBarButtonStyle;
25
32
  navigationBarStyle?: NavigationBarStyle;
26
33
  navigationBarBehavior?: NavigationBarBehavior;
27
- statusBarColor?: string;
34
+
35
+ // Status bar
36
+ statusBarColor?: StatusBarColorValue;
28
37
  statusBarStyle?: StatusBarStyle;
29
38
  statusBarVisible?: boolean;
30
39
  statusBarAnimated?: boolean;
40
+
41
+ // Screen
31
42
  keepScreenOn?: boolean;
32
43
  immersiveMode?: boolean;
33
44
  brightness?: number;
@@ -35,7 +46,20 @@ export interface SystemBarConfig {
35
46
  secureScreen?: boolean;
36
47
  }
37
48
 
38
- export const useSystemBar = (config: SystemBarConfig) => {
49
+ // ─────────────────────────────────────────────
50
+ // useSystemBar
51
+ // Apply system bar settings declaratively.
52
+ //
53
+ // @example
54
+ // useSystemBar({
55
+ // navigationBarColor: "transparent",
56
+ // navigationBarButtonStyle: "light",
57
+ // statusBarStyle: "light",
58
+ // keepScreenOn: true,
59
+ // });
60
+ // ─────────────────────────────────────────────
61
+
62
+ export const useSystemBar = (config: SystemBarConfig): void => {
39
63
  const configRef = useRef("");
40
64
  const configStr = JSON.stringify(config);
41
65
 
@@ -44,43 +68,197 @@ export const useSystemBar = (config: SystemBarConfig) => {
44
68
  configRef.current = configStr;
45
69
 
46
70
  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);
71
+ if (config.navigationBarColor !== undefined)
72
+ SystemBar.setNavigationBarColor(config.navigationBarColor);
73
+ if (config.navigationBarVisibility !== undefined)
74
+ SystemBar.setNavigationBarVisibility(config.navigationBarVisibility);
75
+ if (config.navigationBarButtonStyle !== undefined)
76
+ SystemBar.setNavigationBarButtonStyle(config.navigationBarButtonStyle);
77
+ if (config.navigationBarStyle !== undefined)
78
+ SystemBar.setNavigationBarStyle(config.navigationBarStyle);
79
+ if (config.navigationBarBehavior !== undefined)
80
+ SystemBar.setNavigationBarBehavior(config.navigationBarBehavior);
81
+ if (config.statusBarColor !== undefined)
82
+ SystemBar.setStatusBarColor(config.statusBarColor);
83
+ if (config.statusBarStyle !== undefined)
84
+ SystemBar.setStatusBarStyle(config.statusBarStyle);
85
+ if (config.statusBarVisible !== undefined)
86
+ SystemBar.setStatusBarVisibility(config.statusBarVisible);
87
+ if (config.keepScreenOn !== undefined)
88
+ SystemBar.keepScreenOn(config.keepScreenOn);
89
+ if (config.immersiveMode !== undefined)
90
+ SystemBar.immersiveMode(config.immersiveMode);
91
+ if (config.brightness !== undefined)
92
+ SystemBar.setBrightness(config.brightness);
93
+ if (config.orientation !== undefined)
94
+ SystemBar.setOrientation(config.orientation);
95
+ if (config.secureScreen !== undefined)
96
+ SystemBar.setSecureScreen(config.secureScreen);
60
97
  };
61
98
 
62
99
  apply().catch(() => {
63
- if (__DEV__) console.warn("[rn-system-bar] useSystemBar: one or more settings failed.");
100
+ if (__DEV__)
101
+ console.warn(
102
+ "[rn-system-bar] useSystemBar: one or more settings failed.",
103
+ );
64
104
  });
65
- // eslint-disable-next-line react-hooks/exhaustive-deps
105
+ // eslint-disable-next-line react-hooks/exhaustive-deps
66
106
  }, [configStr]);
67
107
  };
68
108
 
69
109
  // ─────────────────────────────────────────────
70
- // useScreencast
110
+ // ThemedSystemBarConfig + useThemeSystemBar
111
+ //
112
+ // Combines useTheme() + useSystemBar() — nav/status bars
113
+ // automatically update whenever the theme changes (OS or manual).
114
+ //
115
+ // @example
116
+ // const { isDark, mode, setMode } = useThemeSystemBar({
117
+ // dark: { navigationBarColor: "#000000", statusBarStyle: "light" },
118
+ // light: { navigationBarColor: "#ffffff", statusBarStyle: "dark" },
119
+ // base: { keepScreenOn: true },
120
+ // });
121
+ // ─────────────────────────────────────────────
122
+
123
+ export interface ThemedSystemBarConfig {
124
+ /** Config applied when the resolved theme is DARK. */
125
+ dark?: SystemBarConfig;
126
+ /** Config applied when the resolved theme is LIGHT. */
127
+ light?: SystemBarConfig;
128
+ /**
129
+ * Static config merged under both themes.
130
+ * Theme-specific (dark/light) values always win.
131
+ */
132
+ base?: SystemBarConfig;
133
+ }
134
+
135
+ export const useThemeSystemBar = (
136
+ config: ThemedSystemBarConfig,
137
+ ): ThemeState => {
138
+ const theme = useTheme();
139
+
140
+ const resolved: SystemBarConfig = {
141
+ ...(config.base ?? {}),
142
+ ...(theme.isDark ? (config.dark ?? {}) : (config.light ?? {})),
143
+ };
144
+
145
+ useSystemBar(resolved);
146
+
147
+ return theme;
148
+ };
149
+
150
+ // ─────────────────────────────────────────────
151
+ // Re-export useTheme + theme types
152
+ // ─────────────────────────────────────────────
153
+
154
+ export { useTheme } from "./useTheme";
155
+ export type { ThemeMode, ThemeState };
156
+
157
+ // ─────────────────────────────────────────────
158
+ // useSystemScreencast
159
+ // Tracks OS-level external display state.
160
+ // Android: DisplayManager (HDMI / Miracast / Presentation display).
161
+ // iOS: UIScreen.screens (AirPlay mirror).
162
+ //
163
+ // @example
164
+ // const { isCasting, displayName, displays } = useSystemScreencast();
71
165
  // ─────────────────────────────────────────────
72
- export const useScreencast = () => {
73
- const [info, setInfo] = useState<ScreencastInfo>({
166
+
167
+ export const useSystemScreencast = (): SystemScreencastInfo => {
168
+ const [info, setInfo] = useState<SystemScreencastInfo>({
74
169
  isCasting: false,
75
170
  displayName: null,
76
171
  displays: [],
77
172
  });
78
173
 
79
174
  useEffect(() => {
80
- SystemBar.getScreencastInfo().then(setInfo).catch(() => {});
81
- const unsub = SystemBar.onScreencastChange(setInfo);
175
+ SystemBar.getSystemScreencastInfo()
176
+ .then(setInfo)
177
+ .catch(() => {});
178
+ const unsub = SystemBar.onSystemScreencastChange(setInfo);
82
179
  return () => unsub();
83
180
  }, []);
84
181
 
85
182
  return info;
86
183
  };
184
+
185
+ /** @deprecated Renamed to useSystemScreencast() */
186
+ export const useScreencast = useSystemScreencast;
187
+
188
+ // ─────────────────────────────────────────────
189
+ // UseAppCastReturn + useAppCast
190
+ //
191
+ // In-app device discovery + casting via Android MediaRouter.
192
+ // Mirrors ONLY this app's screen — NOT the whole system.
193
+ // iOS always returns idle (AirPlay is system-managed, no public API).
194
+ //
195
+ // Flow:
196
+ // 1. Mount the component — calls getAppCastInfo() for initial state
197
+ // 2. call scan() → state = "scanning", devices populate
198
+ // 3. call connect(device.id) → state = "connecting" → "connected"
199
+ // 4. call disconnect() → state = "disconnecting" → "idle"
200
+ // 5. Unmount → scan auto-stopped (battery safe)
201
+ //
202
+ // @example
203
+ // const { state, devices, connectedDevice, error,
204
+ // scan, stopScan, connect, disconnect } = useAppCast();
205
+ //
206
+ // <Button title="Find TVs" onPress={scan} />
207
+ // {devices.map(d => (
208
+ // <Button key={d.id} title={`Cast to ${d.name}`}
209
+ // onPress={() => connect(d.id)} />
210
+ // ))}
211
+ // {connectedDevice && (
212
+ // <Button title="Stop casting" onPress={disconnect} />
213
+ // )}
214
+ // ─────────────────────────────────────────────
215
+
216
+ export interface UseAppCastReturn extends AppCastInfo {
217
+ /** Start scanning for nearby castable devices (TV, Chromecast, etc.). */
218
+ scan: () => void;
219
+ /** Stop the device discovery scan. */
220
+ stopScan: () => void;
221
+ /**
222
+ * Connect to a discovered device and begin casting this app's screen.
223
+ * @param deviceId The `id` field from `AppCastDevice`.
224
+ * @param pairingPin Optional PIN string if `requiresPairing` is true.
225
+ */
226
+ connect: (deviceId: string, pairingPin?: string) => void;
227
+ /** End the active cast session and return to idle. */
228
+ disconnect: () => void;
229
+ }
230
+
231
+ export const useAppCast = (): UseAppCastReturn => {
232
+ const [info, setInfo] = useState<AppCastInfo>({
233
+ state: "idle",
234
+ devices: [],
235
+ connectedDevice: null,
236
+ error: null,
237
+ });
238
+
239
+ useEffect(() => {
240
+ // Sync initial state
241
+ SystemBar.getAppCastInfo()
242
+ .then(setInfo)
243
+ .catch(() => {});
244
+
245
+ // Subscribe to all cast events (device found/lost, state changes, errors)
246
+ const unsub = SystemBar.onAppCastChange(setInfo);
247
+
248
+ return () => {
249
+ unsub();
250
+ // Auto-stop scan on unmount — avoids background battery drain
251
+ SystemBar.stopAppCastScan();
252
+ };
253
+ }, []);
254
+
255
+ const scan = useCallback(() => SystemBar.startAppCastScan(), []);
256
+ const stopScan = useCallback(() => SystemBar.stopAppCastScan(), []);
257
+ const connect = useCallback(
258
+ (id: string, pin?: string) => SystemBar.connectAppCast(id, pin),
259
+ [],
260
+ );
261
+ const disconnect = useCallback(() => SystemBar.disconnectAppCast(), []);
262
+
263
+ return { ...info, scan, stopScan, connect, disconnect };
264
+ };
@@ -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
+ }