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
package/src/useSystemBar.ts
CHANGED
|
@@ -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
|
|
22
|
+
import { useTheme } from "./useTheme";
|
|
16
23
|
|
|
17
24
|
// ─────────────────────────────────────────────
|
|
18
|
-
//
|
|
19
|
-
// All async calls run inside useEffect — no Suspense crash
|
|
25
|
+
// SystemBarConfig
|
|
20
26
|
// ─────────────────────────────────────────────
|
|
27
|
+
|
|
21
28
|
export interface SystemBarConfig {
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
if (config.navigationBarVisibility
|
|
50
|
-
|
|
51
|
-
if (config.
|
|
52
|
-
|
|
53
|
-
if (config.
|
|
54
|
-
|
|
55
|
-
if (config.
|
|
56
|
-
|
|
57
|
-
if (config.
|
|
58
|
-
|
|
59
|
-
if (config.
|
|
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__)
|
|
101
|
+
if (__DEV__)
|
|
102
|
+
console.warn(
|
|
103
|
+
"[rn-system-bar] useSystemBar: one or more settings failed.",
|
|
104
|
+
);
|
|
64
105
|
});
|
|
65
|
-
|
|
106
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
66
107
|
}, [configStr]);
|
|
67
108
|
};
|
|
68
109
|
|
|
69
110
|
// ─────────────────────────────────────────────
|
|
70
|
-
//
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
+
};
|
package/src/useTheme.ts
ADDED
|
@@ -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
|
+
}
|