refactor(FN-1240): remove dashboard Capacitor coupling in favor of @fusion/mobile
- Remove legacy dashboard Capacitor config, scripts, and package dependencies now managed by @fusion/mobile - Rewrite network, splash-screen, and status-bar plugin managers to use guarded global Capacitor plugin adapters without direct @capacitor imports - Update plugin tests to stub Capacitor globals and drop obsolete capacitor config test coverage - Simplify dashboard mobile README guidance and add a @gsxdsm/fusion patch changeset documenting the migration
This commit is contained in:
@@ -479,51 +479,15 @@ The dashboard includes several runtime safeguards to stay responsive during long
|
||||
|
||||
## Mobile Development (Capacitor)
|
||||
|
||||
Capacitor wraps the existing Fusion dashboard web build into native iOS and Android shells, so mobile uses the same React UI and API surface as desktop web.
|
||||
Mobile builds use the dedicated `@fusion/mobile` package. See [`../../MOBILE.md`](../../MOBILE.md) for the full workflow.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **iOS**: Xcode + CocoaPods installed
|
||||
- **Android**: Android Studio + Android SDK installed
|
||||
|
||||
### Setup
|
||||
Quick start:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm mobile:build
|
||||
pnpm mobile:ios # or pnpm mobile:android
|
||||
```
|
||||
|
||||
`mobile:build` builds `@fusion/dashboard` and syncs web assets into the `@fusion/mobile` Capacitor project.
|
||||
|
||||
### Running / Opening Native Projects
|
||||
|
||||
```bash
|
||||
pnpm mobile:ios
|
||||
pnpm mobile:android
|
||||
```
|
||||
|
||||
### Development with Live Reload
|
||||
|
||||
```bash
|
||||
pnpm mobile:dev:ios
|
||||
pnpm mobile:dev:android
|
||||
```
|
||||
|
||||
The live-reload helper sets `FUSION_LIVE_RELOAD=true` automatically and defaults `FUSION_SERVER_URL` to `http://localhost:5173`.
|
||||
|
||||
For the full workflow (CI pipeline, PWA details, troubleshooting), see [`../../MOBILE.md`](../../MOBILE.md).
|
||||
|
||||
### Mobile Scripts
|
||||
|
||||
Workspace (`package.json`):
|
||||
|
||||
- `mobile:build`
|
||||
- `mobile:ios`
|
||||
- `mobile:android`
|
||||
- `mobile:dev:ios`
|
||||
- `mobile:dev:android`
|
||||
- `mobile:sync`
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import type { CapacitorConfig } from "@capacitor/cli";
|
||||
|
||||
const config: CapacitorConfig = {
|
||||
appId: "com.fusion.dashboard",
|
||||
appName: "Fusion",
|
||||
webDir: "dist/client",
|
||||
server: {
|
||||
// In development, override url to connect to the Fusion backend.
|
||||
// Set FUSION_BACKEND_URL env var before running cap sync/run.
|
||||
// Example: FUSION_BACKEND_URL=http://192.168.1.100:4040 pnpm cap:run:ios
|
||||
//
|
||||
// In production, the app serves static assets from webDir and connects
|
||||
// to the backend at the configured server url.
|
||||
url: process.env.FUSION_BACKEND_URL || undefined,
|
||||
cleartext: true, // Allow HTTP connections to local dev servers
|
||||
iosScheme: "fusion",
|
||||
androidScheme: "fusion",
|
||||
},
|
||||
plugins: {
|
||||
SplashScreen: {
|
||||
launchAutoHide: false,
|
||||
backgroundColor: "#0a0a0a",
|
||||
showSpinner: true,
|
||||
spinnerColor: "#6366f1",
|
||||
androidSplashAssetName: "splash",
|
||||
androidScaleType: "CENTER_CROP",
|
||||
},
|
||||
StatusBar: {
|
||||
style: "DARK",
|
||||
backgroundColor: "#0a0a0a",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -22,26 +22,13 @@
|
||||
"scripts": {
|
||||
"build": "vite build && tsc",
|
||||
"build:client": "vite build",
|
||||
"cap:open:android": "cap open android",
|
||||
"cap:open:ios": "cap open ios",
|
||||
"cap:run:android": "cap run android",
|
||||
"cap:run:ios": "cap run ios",
|
||||
"cap:sync": "cap sync",
|
||||
"dev": "pnpm build && pnpm typecheck && pnpm dev:serve",
|
||||
"dev:serve": "vite dev",
|
||||
"mobile:prepare": "pnpm build:client && pnpm cap:sync",
|
||||
"postinstall": "chmod +x node_modules/.pnpm/node-pty*/node_modules/node-pty/prebuilds/darwin-*/spawn-helper node_modules/.pnpm/node-pty*/node_modules/node-pty/prebuilds/darwin-*/*.node 2>/dev/null || true",
|
||||
"test": "vitest run",
|
||||
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.app.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@capacitor/app": "^7.1.2",
|
||||
"@capacitor/core": "^7.0.0",
|
||||
"@capacitor/network": "^7.0.4",
|
||||
"@capacitor/push-notifications": "^7.0.6",
|
||||
"@capacitor/share": "^7.0.4",
|
||||
"@capacitor/splash-screen": "^7.0.5",
|
||||
"@capacitor/status-bar": "^7.0.6",
|
||||
"@codemirror/basic-setup": "^0.20.0",
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
"@codemirror/lang-javascript": "^6.2.3",
|
||||
@@ -71,9 +58,6 @@
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/android": "^7.0.0",
|
||||
"@capacitor/cli": "^7.0.0",
|
||||
"@capacitor/ios": "^7.0.0",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.5.0",
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import type { CapacitorConfig } from "@capacitor/cli";
|
||||
|
||||
const configPath = resolve(__dirname, "../../capacitor.config.ts");
|
||||
|
||||
function evaluateConfig(env: NodeJS.ProcessEnv = {}): CapacitorConfig {
|
||||
const content = readFileSync(configPath, "utf8");
|
||||
const executableSource = content
|
||||
.replace('import type { CapacitorConfig } from "@capacitor/cli";\n\n', "")
|
||||
.replace("const config: CapacitorConfig = {", "const config = {")
|
||||
.replace("export default config;", "return config;");
|
||||
|
||||
const fn = new Function("process", executableSource) as (processLike: { env: NodeJS.ProcessEnv }) => CapacitorConfig;
|
||||
return fn({ env });
|
||||
}
|
||||
|
||||
describe("capacitor.config", () => {
|
||||
|
||||
it("exists and exports a TypeScript Capacitor config", () => {
|
||||
const content = readFileSync(configPath, "utf8");
|
||||
|
||||
expect(content.length).toBeGreaterThan(0);
|
||||
expect(content).toContain("import type { CapacitorConfig } from \"@capacitor/cli\"");
|
||||
expect(content).toContain("const config: CapacitorConfig = {");
|
||||
expect(content).toContain("export default config;");
|
||||
});
|
||||
|
||||
it("sets webDir to dist/client", () => {
|
||||
const config = evaluateConfig();
|
||||
expect(config.webDir).toBe("dist/client");
|
||||
});
|
||||
|
||||
it("uses the expected app identity", () => {
|
||||
const config = evaluateConfig();
|
||||
expect(config.appId).toBe("com.fusion.dashboard");
|
||||
expect(config.appName).toBe("Fusion");
|
||||
});
|
||||
|
||||
it("enables cleartext server access for local development", () => {
|
||||
const config = evaluateConfig();
|
||||
expect(config.server?.cleartext).toBe(true);
|
||||
});
|
||||
|
||||
it("defaults server.url to undefined when FUSION_BACKEND_URL is unset", () => {
|
||||
const config = evaluateConfig({});
|
||||
expect(config.server?.url).toBeUndefined();
|
||||
});
|
||||
|
||||
it("uses FUSION_BACKEND_URL when provided", () => {
|
||||
const config = evaluateConfig({ FUSION_BACKEND_URL: "http://192.168.1.100:4040" });
|
||||
expect(config.server?.url).toBe("http://192.168.1.100:4040");
|
||||
});
|
||||
|
||||
it("references the FUSION_BACKEND_URL env variable in source", () => {
|
||||
const content = readFileSync(configPath, "utf8");
|
||||
expect(content).toContain("process.env.FUSION_BACKEND_URL || undefined");
|
||||
});
|
||||
});
|
||||
@@ -1,24 +1,14 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { Network } from "@capacitor/network";
|
||||
import { NetworkManager } from "../plugins/network.js";
|
||||
|
||||
const { getStatusMock, addListenerMock, removeListenerMock } = vi.hoisted(() => ({
|
||||
getStatusMock: vi.fn(),
|
||||
addListenerMock: vi.fn(),
|
||||
removeListenerMock: vi.fn(),
|
||||
}));
|
||||
const getStatusMock = vi.fn();
|
||||
const addListenerMock = vi.fn();
|
||||
const removeListenerMock = vi.fn();
|
||||
|
||||
let networkStatusChangeHandler:
|
||||
| ((status: { connected: boolean; connectionType: "wifi" | "cellular" | "none" | "unknown" }) => void)
|
||||
| null = null;
|
||||
|
||||
vi.mock("@capacitor/network", () => ({
|
||||
Network: {
|
||||
getStatus: getStatusMock,
|
||||
addListener: addListenerMock,
|
||||
},
|
||||
}));
|
||||
|
||||
describe("NetworkManager", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -40,6 +30,15 @@ describe("NetworkManager", () => {
|
||||
}
|
||||
return { remove: removeListenerMock };
|
||||
});
|
||||
|
||||
vi.stubGlobal("Capacitor", {
|
||||
Plugins: {
|
||||
Network: {
|
||||
getStatus: getStatusMock,
|
||||
addListener: addListenerMock,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("initialize() queries current network status", async () => {
|
||||
@@ -47,7 +46,7 @@ describe("NetworkManager", () => {
|
||||
|
||||
await manager.initialize();
|
||||
|
||||
expect(Network.getStatus).toHaveBeenCalledTimes(1);
|
||||
expect(getStatusMock).toHaveBeenCalledTimes(1);
|
||||
expect(manager.getStatus()).toEqual({ connected: true, connectionType: "wifi" });
|
||||
});
|
||||
|
||||
@@ -56,8 +55,8 @@ describe("NetworkManager", () => {
|
||||
|
||||
await manager.startMonitoring();
|
||||
|
||||
expect(Network.addListener).toHaveBeenCalledTimes(1);
|
||||
expect(Network.addListener).toHaveBeenCalledWith("networkStatusChange", expect.any(Function));
|
||||
expect(addListenerMock).toHaveBeenCalledTimes(1);
|
||||
expect(addListenerMock).toHaveBeenCalledWith("networkStatusChange", expect.any(Function));
|
||||
expect(manager.isMonitoring).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
import { beforeEach, afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { SplashScreen } from "@capacitor/splash-screen";
|
||||
import { SplashScreenManager } from "../plugins/splash-screen.js";
|
||||
|
||||
vi.mock("@capacitor/splash-screen", () => ({
|
||||
SplashScreen: {
|
||||
hide: vi.fn(),
|
||||
show: vi.fn(),
|
||||
},
|
||||
}));
|
||||
const hideMock = vi.fn();
|
||||
const showMock = vi.fn();
|
||||
|
||||
describe("SplashScreenManager", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(SplashScreen.hide).mockResolvedValue(undefined);
|
||||
vi.mocked(SplashScreen.show).mockResolvedValue(undefined);
|
||||
hideMock.mockResolvedValue(undefined);
|
||||
showMock.mockResolvedValue(undefined);
|
||||
|
||||
vi.stubGlobal("Capacitor", {
|
||||
Plugins: {
|
||||
SplashScreen: {
|
||||
hide: hideMock,
|
||||
show: showMock,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("initialize() with autoHide=true triggers hide after delay", async () => {
|
||||
@@ -25,12 +30,12 @@ describe("SplashScreenManager", () => {
|
||||
const manager = new SplashScreenManager({ autoHide: true, hideDelay: 100 });
|
||||
|
||||
await manager.initialize();
|
||||
expect(SplashScreen.hide).not.toHaveBeenCalled();
|
||||
expect(hideMock).not.toHaveBeenCalled();
|
||||
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
expect(SplashScreen.hide).toHaveBeenCalledTimes(1);
|
||||
expect(SplashScreen.hide).toHaveBeenCalledWith({ fadeOutDuration: 300 });
|
||||
expect(hideMock).toHaveBeenCalledTimes(1);
|
||||
expect(hideMock).toHaveBeenCalledWith({ fadeOutDuration: 300 });
|
||||
});
|
||||
|
||||
it("initialize() with autoHide=false does not auto-hide", async () => {
|
||||
@@ -40,7 +45,7 @@ describe("SplashScreenManager", () => {
|
||||
await manager.initialize();
|
||||
await vi.advanceTimersByTimeAsync(500);
|
||||
|
||||
expect(SplashScreen.hide).not.toHaveBeenCalled();
|
||||
expect(hideMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("hide() delegates to SplashScreen.hide()", async () => {
|
||||
@@ -48,8 +53,8 @@ describe("SplashScreenManager", () => {
|
||||
|
||||
await manager.hide();
|
||||
|
||||
expect(SplashScreen.hide).toHaveBeenCalledTimes(1);
|
||||
expect(SplashScreen.hide).toHaveBeenCalledWith({ fadeOutDuration: 300 });
|
||||
expect(hideMock).toHaveBeenCalledTimes(1);
|
||||
expect(hideMock).toHaveBeenCalledWith({ fadeOutDuration: 300 });
|
||||
});
|
||||
|
||||
it("show() delegates to SplashScreen.show()", async () => {
|
||||
@@ -57,8 +62,8 @@ describe("SplashScreenManager", () => {
|
||||
|
||||
await manager.show();
|
||||
|
||||
expect(SplashScreen.show).toHaveBeenCalledTimes(1);
|
||||
expect(SplashScreen.show).toHaveBeenCalledWith({ autoHide: false });
|
||||
expect(showMock).toHaveBeenCalledTimes(1);
|
||||
expect(showMock).toHaveBeenCalledWith({ autoHide: false });
|
||||
});
|
||||
|
||||
it("initialize() is idempotent", async () => {
|
||||
@@ -69,11 +74,11 @@ describe("SplashScreenManager", () => {
|
||||
await manager.initialize();
|
||||
await vi.advanceTimersByTimeAsync(50);
|
||||
|
||||
expect(SplashScreen.hide).toHaveBeenCalledTimes(1);
|
||||
expect(hideMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("hide() swallows errors gracefully", async () => {
|
||||
vi.mocked(SplashScreen.hide).mockRejectedValue(new Error("unavailable"));
|
||||
hideMock.mockRejectedValue(new Error("unavailable"));
|
||||
const manager = new SplashScreenManager();
|
||||
|
||||
await expect(manager.hide()).resolves.toBeUndefined();
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { StatusBar, Style } from "@capacitor/status-bar";
|
||||
import { StatusBarManager } from "../plugins/status-bar.js";
|
||||
|
||||
vi.mock("@capacitor/status-bar", () => ({
|
||||
StatusBar: {
|
||||
setStyle: vi.fn(),
|
||||
},
|
||||
Style: {
|
||||
Dark: "DARK",
|
||||
Light: "LIGHT",
|
||||
},
|
||||
}));
|
||||
const setStyleMock = vi.fn();
|
||||
|
||||
describe("StatusBarManager", () => {
|
||||
const originalMatchMedia = globalThis.window?.matchMedia;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(StatusBar.setStyle).mockResolvedValue(undefined);
|
||||
setStyleMock.mockResolvedValue(undefined);
|
||||
|
||||
vi.stubGlobal("Capacitor", {
|
||||
Plugins: {
|
||||
StatusBar: {
|
||||
setStyle: setStyleMock,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
|
||||
if (globalThis.window) {
|
||||
Object.defineProperty(globalThis.window, "matchMedia", {
|
||||
configurable: true,
|
||||
@@ -35,8 +36,8 @@ describe("StatusBarManager", () => {
|
||||
|
||||
await manager.initialize();
|
||||
|
||||
expect(StatusBar.setStyle).toHaveBeenCalledTimes(1);
|
||||
expect(StatusBar.setStyle).toHaveBeenCalledWith({ style: Style.Dark });
|
||||
expect(setStyleMock).toHaveBeenCalledTimes(1);
|
||||
expect(setStyleMock).toHaveBeenCalledWith({ style: "DARK" });
|
||||
});
|
||||
|
||||
it("setTheme('dark') sets dark style", async () => {
|
||||
@@ -44,7 +45,7 @@ describe("StatusBarManager", () => {
|
||||
|
||||
await manager.setTheme("dark");
|
||||
|
||||
expect(StatusBar.setStyle).toHaveBeenCalledWith({ style: Style.Dark });
|
||||
expect(setStyleMock).toHaveBeenCalledWith({ style: "DARK" });
|
||||
expect(manager.getTheme()).toBe("dark");
|
||||
});
|
||||
|
||||
@@ -53,7 +54,7 @@ describe("StatusBarManager", () => {
|
||||
|
||||
await manager.setTheme("light");
|
||||
|
||||
expect(StatusBar.setStyle).toHaveBeenCalledWith({ style: Style.Light });
|
||||
expect(setStyleMock).toHaveBeenCalledWith({ style: "LIGHT" });
|
||||
expect(manager.getTheme()).toBe("light");
|
||||
});
|
||||
|
||||
@@ -68,7 +69,7 @@ describe("StatusBarManager", () => {
|
||||
|
||||
await manager.setTheme("system");
|
||||
|
||||
expect(StatusBar.setStyle).toHaveBeenLastCalledWith({ style: Style.Dark });
|
||||
expect(setStyleMock).toHaveBeenLastCalledWith({ style: "DARK" });
|
||||
|
||||
Object.defineProperty(window, "matchMedia", {
|
||||
configurable: true,
|
||||
@@ -78,7 +79,7 @@ describe("StatusBarManager", () => {
|
||||
|
||||
await manager.setTheme("system");
|
||||
|
||||
expect(StatusBar.setStyle).toHaveBeenLastCalledWith({ style: Style.Light });
|
||||
expect(setStyleMock).toHaveBeenLastCalledWith({ style: "LIGHT" });
|
||||
});
|
||||
|
||||
it("onThemeChange callback fires on theme change", async () => {
|
||||
@@ -105,7 +106,7 @@ describe("StatusBarManager", () => {
|
||||
});
|
||||
|
||||
it("initialize() swallows errors", async () => {
|
||||
vi.mocked(StatusBar.setStyle).mockRejectedValue(new Error("not available"));
|
||||
setStyleMock.mockRejectedValue(new Error("not available"));
|
||||
const manager = new StatusBarManager({ themeMode: "dark" });
|
||||
|
||||
await expect(manager.initialize()).resolves.toBeUndefined();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Network, type ConnectionType } from "@capacitor/network";
|
||||
import type {
|
||||
PluginManager,
|
||||
NetworkStatus,
|
||||
@@ -6,6 +5,38 @@ import type {
|
||||
PluginNetworkListenerHandle,
|
||||
} from "./types.js";
|
||||
|
||||
type NativeConnectionType = "wifi" | "cellular" | "none" | "unknown";
|
||||
|
||||
interface NativeNetworkStatus {
|
||||
connected: boolean;
|
||||
connectionType: NativeConnectionType;
|
||||
}
|
||||
|
||||
interface NativeNetworkPlugin {
|
||||
getStatus: () => Promise<NativeNetworkStatus>;
|
||||
addListener: (
|
||||
eventName: "networkStatusChange",
|
||||
callback: (status: NativeNetworkStatus) => void,
|
||||
) => PluginNetworkListenerHandle | Promise<PluginNetworkListenerHandle>;
|
||||
}
|
||||
|
||||
interface CapacitorGlobal {
|
||||
Capacitor?: {
|
||||
Plugins?: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
function getNativeNetworkPlugin(): NativeNetworkPlugin | null {
|
||||
const plugins = (globalThis as CapacitorGlobal).Capacitor?.Plugins;
|
||||
const candidate = plugins?.Network as Partial<NativeNetworkPlugin> | undefined;
|
||||
|
||||
if (!candidate || typeof candidate.getStatus !== "function" || typeof candidate.addListener !== "function") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return candidate as NativeNetworkPlugin;
|
||||
}
|
||||
|
||||
export class NetworkManager implements PluginManager {
|
||||
private status: NetworkStatus;
|
||||
private listeners: Array<NetworkStatusCallback> = [];
|
||||
@@ -22,9 +53,15 @@ export class NetworkManager implements PluginManager {
|
||||
return;
|
||||
}
|
||||
|
||||
const networkPlugin = getNativeNetworkPlugin();
|
||||
|
||||
try {
|
||||
const currentStatus = await Network.getStatus();
|
||||
this.status = this.toNetworkStatus(currentStatus.connected, currentStatus.connectionType);
|
||||
if (networkPlugin) {
|
||||
const currentStatus = await networkPlugin.getStatus();
|
||||
this.status = this.toNetworkStatus(currentStatus.connected, currentStatus.connectionType);
|
||||
} else {
|
||||
this.status = { connected: true, connectionType: "unknown" };
|
||||
}
|
||||
} catch {
|
||||
// Network plugin may not be available in browser context
|
||||
this.status = { connected: true, connectionType: "unknown" };
|
||||
@@ -39,10 +76,17 @@ export class NetworkManager implements PluginManager {
|
||||
return;
|
||||
}
|
||||
|
||||
const networkPlugin = getNativeNetworkPlugin();
|
||||
if (!networkPlugin) {
|
||||
this.networkListenerHandle = null;
|
||||
this.monitoring = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.networkListenerHandle = await Network.addListener(
|
||||
const listenerHandle = networkPlugin.addListener(
|
||||
"networkStatusChange",
|
||||
(status) => {
|
||||
(status: NativeNetworkStatus) => {
|
||||
const nextStatus = this.toNetworkStatus(status.connected, status.connectionType);
|
||||
const previousConnected = this.status.connected;
|
||||
this.status = nextStatus;
|
||||
@@ -58,6 +102,8 @@ export class NetworkManager implements PluginManager {
|
||||
this.emit("network:change", nextStatus);
|
||||
},
|
||||
);
|
||||
|
||||
this.networkListenerHandle = await Promise.resolve(listenerHandle);
|
||||
this.monitoring = true;
|
||||
} catch {
|
||||
// Network plugin may not be available in browser context
|
||||
@@ -108,7 +154,7 @@ export class NetworkManager implements PluginManager {
|
||||
}
|
||||
}
|
||||
|
||||
private toNetworkStatus(connected: boolean, connectionType: ConnectionType): NetworkStatus {
|
||||
private toNetworkStatus(connected: boolean, connectionType: NativeConnectionType): NetworkStatus {
|
||||
return {
|
||||
connected,
|
||||
connectionType: connectionType as NetworkStatus["connectionType"],
|
||||
|
||||
@@ -1,6 +1,27 @@
|
||||
import { SplashScreen } from "@capacitor/splash-screen";
|
||||
import type { PluginManager } from "./types.js";
|
||||
|
||||
interface NativeSplashScreenPlugin {
|
||||
hide: (options: { fadeOutDuration: number }) => Promise<void>;
|
||||
show: (options: { autoHide: boolean }) => Promise<void>;
|
||||
}
|
||||
|
||||
interface CapacitorGlobal {
|
||||
Capacitor?: {
|
||||
Plugins?: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
function getNativeSplashScreenPlugin(): NativeSplashScreenPlugin | null {
|
||||
const plugins = (globalThis as CapacitorGlobal).Capacitor?.Plugins;
|
||||
const candidate = plugins?.SplashScreen as Partial<NativeSplashScreenPlugin> | undefined;
|
||||
|
||||
if (!candidate || typeof candidate.hide !== "function" || typeof candidate.show !== "function") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return candidate as NativeSplashScreenPlugin;
|
||||
}
|
||||
|
||||
export interface SplashScreenOptions {
|
||||
autoHide?: boolean;
|
||||
hideDelay?: number;
|
||||
@@ -34,16 +55,26 @@ export class SplashScreenManager implements PluginManager {
|
||||
}
|
||||
|
||||
async hide(): Promise<void> {
|
||||
const splashScreenPlugin = getNativeSplashScreenPlugin();
|
||||
if (!splashScreenPlugin) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await SplashScreen.hide({ fadeOutDuration: 300 });
|
||||
await splashScreenPlugin.hide({ fadeOutDuration: 300 });
|
||||
} catch {
|
||||
// Ignore errors — splash screen may not be available in browser/web context
|
||||
}
|
||||
}
|
||||
|
||||
async show(): Promise<void> {
|
||||
const splashScreenPlugin = getNativeSplashScreenPlugin();
|
||||
if (!splashScreenPlugin) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await SplashScreen.show({ autoHide: false });
|
||||
await splashScreenPlugin.show({ autoHide: false });
|
||||
} catch {
|
||||
// Ignore errors — splash screen may not be available in browser/web context
|
||||
}
|
||||
|
||||
@@ -1,10 +1,30 @@
|
||||
import { StatusBar, Style } from "@capacitor/status-bar";
|
||||
import type {
|
||||
PluginManager,
|
||||
ThemeMode,
|
||||
ThemeChangeCallback,
|
||||
} from "./types.js";
|
||||
|
||||
interface NativeStatusBarPlugin {
|
||||
setStyle: (options: { style: "DARK" | "LIGHT" }) => Promise<void>;
|
||||
}
|
||||
|
||||
interface CapacitorGlobal {
|
||||
Capacitor?: {
|
||||
Plugins?: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
function getNativeStatusBarPlugin(): NativeStatusBarPlugin | null {
|
||||
const plugins = (globalThis as CapacitorGlobal).Capacitor?.Plugins;
|
||||
const candidate = plugins?.StatusBar as Partial<NativeStatusBarPlugin> | undefined;
|
||||
|
||||
if (!candidate || typeof candidate.setStyle !== "function") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return candidate as NativeStatusBarPlugin;
|
||||
}
|
||||
|
||||
export interface StatusBarOptions {
|
||||
themeMode?: ThemeMode;
|
||||
}
|
||||
@@ -50,11 +70,16 @@ export class StatusBarManager implements PluginManager {
|
||||
}
|
||||
|
||||
private async applyTheme(mode: ThemeMode): Promise<void> {
|
||||
const statusBarPlugin = getNativeStatusBarPlugin();
|
||||
if (!statusBarPlugin) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isDark = mode === "dark" || (mode === "system" && this.isSystemDark());
|
||||
|
||||
try {
|
||||
await StatusBar.setStyle({
|
||||
style: isDark ? Style.Dark : Style.Light,
|
||||
await statusBarPlugin.setStyle({
|
||||
style: isDark ? "DARK" : "LIGHT",
|
||||
});
|
||||
} catch {
|
||||
// StatusBar plugin may not be available in browser context
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import type { PluginListenerHandle } from "@capacitor/core";
|
||||
|
||||
/** Network connectivity status */
|
||||
export interface NetworkStatus {
|
||||
connected: boolean;
|
||||
@@ -38,6 +36,11 @@ export interface PluginManager {
|
||||
destroy(): Promise<void>;
|
||||
}
|
||||
|
||||
/** Minimal listener handle contract used by network plugin adapters. */
|
||||
export interface PluginListenerHandle {
|
||||
remove: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
/** Shared network listener handle type for manager implementations. */
|
||||
export type PluginNetworkListenerHandle = PluginListenerHandle;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user