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/README.md +81 -40
- 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 +161 -12
- package/lib/src/types.d.ts +61 -1
- package/lib/src/useSystemBar.d.ts +35 -4
- package/lib/src/useSystemBar.js +71 -7
- 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 +194 -21
- package/src/types.ts +85 -1
- package/src/useSystemBar.ts +206 -28
- package/src/useTheme.ts +95 -0
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
|
-
|
|
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 {}
|
package/src/useSystemBar.ts
CHANGED
|
@@ -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
|
-
|
|
15
|
+
StatusBarColorValue,
|
|
13
16
|
StatusBarStyle,
|
|
17
|
+
SystemScreencastInfo,
|
|
18
|
+
ThemeMode,
|
|
19
|
+
ThemeState,
|
|
14
20
|
} from "./types";
|
|
15
|
-
import
|
|
21
|
+
import { useTheme } from "./useTheme";
|
|
16
22
|
|
|
17
23
|
// ─────────────────────────────────────────────
|
|
18
|
-
//
|
|
19
|
-
// All async calls run inside useEffect — no Suspense crash
|
|
24
|
+
// SystemBarConfig
|
|
20
25
|
// ─────────────────────────────────────────────
|
|
26
|
+
|
|
21
27
|
export interface SystemBarConfig {
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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__)
|
|
100
|
+
if (__DEV__)
|
|
101
|
+
console.warn(
|
|
102
|
+
"[rn-system-bar] useSystemBar: one or more settings failed.",
|
|
103
|
+
);
|
|
64
104
|
});
|
|
65
|
-
|
|
105
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
66
106
|
}, [configStr]);
|
|
67
107
|
};
|
|
68
108
|
|
|
69
109
|
// ─────────────────────────────────────────────
|
|
70
|
-
//
|
|
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
|
-
|
|
73
|
-
|
|
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.
|
|
81
|
-
|
|
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
|
+
};
|
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
|
+
}
|