feat: onboarding window to check perms
This commit is contained in:
parent
0c64d43ec6
commit
f8095d8ac0
14 changed files with 663 additions and 33 deletions
|
|
@ -356,6 +356,7 @@ const config: ForgeConfig = {
|
|||
new MakerDMG(
|
||||
{
|
||||
// macOS DMG files will be named like: Amical-0.0.1-arm64.dmg
|
||||
icon: "./assets/logo.icns",
|
||||
},
|
||||
["darwin"],
|
||||
),
|
||||
|
|
@ -378,6 +379,11 @@ const config: ForgeConfig = {
|
|||
config: "vite.preload.config.mts",
|
||||
target: "preload",
|
||||
},
|
||||
{
|
||||
entry: "src/main/onboarding-preload.ts",
|
||||
config: "vite.onboarding-preload.config.mts",
|
||||
target: "preload",
|
||||
},
|
||||
],
|
||||
renderer: [
|
||||
{
|
||||
|
|
@ -388,6 +394,10 @@ const config: ForgeConfig = {
|
|||
name: "widget_window",
|
||||
config: "vite.widget.config.mts",
|
||||
},
|
||||
{
|
||||
name: "onboarding_window",
|
||||
config: "vite.onboarding.config.mts",
|
||||
},
|
||||
],
|
||||
}),
|
||||
// Fuses are used to enable/disable various Electron functionality
|
||||
|
|
|
|||
12
apps/desktop/onboarding.html
Normal file
12
apps/desktop/onboarding.html
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Amical - Setup</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/renderer/onboarding/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -10,11 +10,15 @@
|
|||
},
|
||||
"scripts": {
|
||||
"start": "pnpm build:swift-helper && electron-forge start",
|
||||
"start:onboarding": "FORCE_ONBOARDING=true pnpm start",
|
||||
"package": "pnpm build:swift-helper && electron-forge package",
|
||||
"make": "pnpm build:swift-helper && electron-forge make --platform=darwin --arch=arm64,x64",
|
||||
"make:dmg": "pnpm build:swift-helper && electron-forge make --targets=@electron-forge/maker-dmg --platform=darwin --arch=arm64",
|
||||
"make:dmg:arm64": "pnpm build:swift-helper && electron-forge make --targets=@electron-forge/maker-dmg --platform=darwin --arch=arm64",
|
||||
"make:dmg:x64": "pnpm build:swift-helper && electron-forge make --targets=@electron-forge/maker-dmg --platform=darwin --arch=x64",
|
||||
"make:zip:arm64": "pnpm build:swift-helper && electron-forge make --targets=@electron-forge/maker-zip --platform=darwin --arch=arm64",
|
||||
"make:zip:x64": "pnpm build:swift-helper && electron-forge make --targets=@electron-forge/maker-zip --platform=darwin --arch=x64",
|
||||
"package:arm64": "pnpm build:swift-helper && electron-forge package --platform=darwin --arch=arm64",
|
||||
"package:x64": "pnpm build:swift-helper && electron-forge package --platform=darwin --arch=x64",
|
||||
"publish": "electron-forge publish",
|
||||
"lint": "eslint --ext .ts,.tsx .",
|
||||
|
|
|
|||
|
|
@ -17,25 +17,27 @@ export class AppManager {
|
|||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
try {
|
||||
await this.initializeDatabase();
|
||||
await this.initializeDatabase();
|
||||
|
||||
await this.requestPermissions();
|
||||
await this.serviceManager.initialize();
|
||||
const needsOnboarding = await this.checkNeedsOnboarding();
|
||||
|
||||
await this.serviceManager.initialize();
|
||||
|
||||
if (needsOnboarding) {
|
||||
await this.showOnboarding();
|
||||
} else {
|
||||
await this.setupWindows();
|
||||
await this.setupMenu();
|
||||
|
||||
// Setup event handlers
|
||||
this.eventHandlers = new EventHandlers(this);
|
||||
this.eventHandlers.setupEventHandlers();
|
||||
|
||||
// Auto-update is now handled by update-electron-app in main.ts
|
||||
|
||||
logger.main.info("Application initialized successfully");
|
||||
} catch (error) {
|
||||
logger.main.error("Error initializing app:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
await this.setupMenu();
|
||||
|
||||
// Setup event handlers
|
||||
this.eventHandlers = new EventHandlers(this);
|
||||
this.eventHandlers.setupEventHandlers();
|
||||
|
||||
// Auto-update is now handled by update-electron-app in main.ts
|
||||
|
||||
logger.main.info("Application initialized successfully");
|
||||
}
|
||||
|
||||
private async initializeDatabase(): Promise<void> {
|
||||
|
|
@ -45,26 +47,49 @@ export class AppManager {
|
|||
);
|
||||
}
|
||||
|
||||
private async requestPermissions(): Promise<void> {
|
||||
if (process.platform === "darwin") {
|
||||
const accessibilityEnabled =
|
||||
systemPreferences.isTrustedAccessibilityClient(false);
|
||||
if (!accessibilityEnabled) {
|
||||
logger.main.debug(
|
||||
"Please enable accessibility permissions in System Preferences > Security & Privacy > Privacy > Accessibility",
|
||||
);
|
||||
}
|
||||
private async checkNeedsOnboarding(): Promise<boolean> {
|
||||
// Force show onboarding for development testing
|
||||
if (process.env.FORCE_ONBOARDING === "true") {
|
||||
logger.main.info("Forcing onboarding window for testing");
|
||||
return true;
|
||||
}
|
||||
|
||||
const microphoneEnabled =
|
||||
if (process.platform !== "darwin") {
|
||||
// For non-macOS platforms, we might still want to check microphone
|
||||
const microphoneStatus =
|
||||
systemPreferences.getMediaAccessStatus("microphone");
|
||||
return microphoneStatus !== "granted";
|
||||
}
|
||||
|
||||
// Check both microphone and accessibility permissions on macOS
|
||||
const microphoneStatus =
|
||||
systemPreferences.getMediaAccessStatus("microphone");
|
||||
logger.main.info("Microphone access status:", {
|
||||
status: microphoneEnabled,
|
||||
const accessibilityStatus =
|
||||
systemPreferences.isTrustedAccessibilityClient(false);
|
||||
|
||||
logger.main.info("Permission status:", {
|
||||
microphone: microphoneStatus,
|
||||
accessibility: accessibilityStatus,
|
||||
});
|
||||
|
||||
if (microphoneEnabled !== "granted") {
|
||||
await systemPreferences.askForMediaAccess("microphone");
|
||||
}
|
||||
return microphoneStatus !== "granted" || !accessibilityStatus;
|
||||
}
|
||||
|
||||
private async showOnboarding(): Promise<void> {
|
||||
this.windowManager.createOnboardingWindow();
|
||||
|
||||
// The onboarding window will handle the permission flow
|
||||
// and call back to complete setup when done
|
||||
}
|
||||
|
||||
completeOnboarding(): void {
|
||||
logger.main.info(
|
||||
"Onboarding completed, restarting app for permissions to take effect",
|
||||
);
|
||||
|
||||
// Relaunch the app to ensure all permissions take effect
|
||||
app.relaunch();
|
||||
app.quit();
|
||||
}
|
||||
|
||||
private async setupWindows(): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { HelperEvent } from "@amical/types";
|
||||
import { AppManager } from "./app-manager";
|
||||
import { logger } from "../logger";
|
||||
import { ipcMain, shell } from "electron";
|
||||
import { ipcMain, shell, systemPreferences, app } from "electron";
|
||||
|
||||
export class EventHandlers {
|
||||
private appManager: AppManager;
|
||||
|
|
@ -13,6 +13,7 @@ export class EventHandlers {
|
|||
setupEventHandlers(): void {
|
||||
this.setupSwiftBridgeEventHandlers();
|
||||
this.setupGeneralIPCHandlers();
|
||||
this.setupOnboardingIPCHandlers();
|
||||
// Note: Audio IPC handlers are now managed by RecordingService
|
||||
}
|
||||
|
||||
|
|
@ -53,4 +54,50 @@ export class EventHandlers {
|
|||
logger.main.debug("Opening external URL", { url });
|
||||
});
|
||||
}
|
||||
|
||||
private setupOnboardingIPCHandlers(): void {
|
||||
// Permission checks
|
||||
ipcMain.handle("onboarding:check-microphone-permission", async () => {
|
||||
return systemPreferences.getMediaAccessStatus("microphone");
|
||||
});
|
||||
|
||||
ipcMain.handle("onboarding:check-accessibility-permission", async () => {
|
||||
if (process.platform !== "darwin") {
|
||||
return true; // Non-macOS platforms don't need accessibility permission
|
||||
}
|
||||
return systemPreferences.isTrustedAccessibilityClient(false);
|
||||
});
|
||||
|
||||
// Permission requests
|
||||
ipcMain.handle("onboarding:request-microphone-permission", async () => {
|
||||
const status = await systemPreferences.askForMediaAccess("microphone");
|
||||
logger.main.info("Microphone permission request result:", status);
|
||||
return status;
|
||||
});
|
||||
|
||||
ipcMain.handle("onboarding:request-accessibility-permission", async () => {
|
||||
if (process.platform !== "darwin") {
|
||||
return; // Non-macOS platforms don't need accessibility permission
|
||||
}
|
||||
// This will prompt the user to open System Preferences
|
||||
systemPreferences.isTrustedAccessibilityClient(true);
|
||||
});
|
||||
|
||||
// Navigation
|
||||
ipcMain.handle("onboarding:complete", async () => {
|
||||
logger.main.info("Onboarding completed");
|
||||
this.appManager.completeOnboarding();
|
||||
});
|
||||
|
||||
// System info
|
||||
ipcMain.handle("onboarding:get-platform", async () => {
|
||||
return process.platform;
|
||||
});
|
||||
|
||||
// Quit app
|
||||
ipcMain.handle("onboarding:quit-app", async () => {
|
||||
logger.main.info("Quitting app from onboarding");
|
||||
app.quit();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,10 +6,12 @@ import { ServiceManager } from "../managers/service-manager";
|
|||
declare const MAIN_WINDOW_VITE_DEV_SERVER_URL: string;
|
||||
declare const MAIN_WINDOW_VITE_NAME: string;
|
||||
declare const WIDGET_WINDOW_VITE_NAME: string;
|
||||
declare const ONBOARDING_WINDOW_VITE_NAME: string;
|
||||
|
||||
export class WindowManager {
|
||||
private mainWindow: BrowserWindow | null = null;
|
||||
private widgetWindow: BrowserWindow | null = null;
|
||||
private onboardingWindow: BrowserWindow | null = null;
|
||||
private widgetDisplayId: number | null = null;
|
||||
private cursorPollingInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
|
|
@ -144,6 +146,65 @@ export class WindowManager {
|
|||
logger.main.info("Widget window shown");
|
||||
}
|
||||
|
||||
createOnboardingWindow(): void {
|
||||
if (this.onboardingWindow && !this.onboardingWindow.isDestroyed()) {
|
||||
this.onboardingWindow.show();
|
||||
this.onboardingWindow.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
this.onboardingWindow = new BrowserWindow({
|
||||
width: 700,
|
||||
height: 600,
|
||||
frame: false,
|
||||
resizable: false,
|
||||
center: true,
|
||||
modal: true,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, "onboarding-preload.js"),
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
|
||||
const devUrl = new URL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
|
||||
devUrl.pathname = "onboarding.html";
|
||||
this.onboardingWindow.loadURL(devUrl.toString());
|
||||
} else {
|
||||
this.onboardingWindow.loadFile(
|
||||
path.join(
|
||||
__dirname,
|
||||
`../renderer/${ONBOARDING_WINDOW_VITE_NAME}/onboarding.html`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
this.onboardingWindow.on("closed", () => {
|
||||
this.onboardingWindow = null;
|
||||
});
|
||||
|
||||
// Disable main window while onboarding is open
|
||||
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
||||
this.mainWindow.setEnabled(false);
|
||||
}
|
||||
|
||||
logger.main.info("Onboarding window created");
|
||||
}
|
||||
|
||||
closeOnboardingWindow(): void {
|
||||
if (this.onboardingWindow && !this.onboardingWindow.isDestroyed()) {
|
||||
this.onboardingWindow.close();
|
||||
}
|
||||
|
||||
// Re-enable main window
|
||||
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
||||
this.mainWindow.setEnabled(true);
|
||||
this.mainWindow.show();
|
||||
this.mainWindow.focus();
|
||||
}
|
||||
}
|
||||
|
||||
private setupDisplayChangeNotifications(): void {
|
||||
// Set up comprehensive display event listeners
|
||||
screen.on("display-added", () => this.handleDisplayChange("display-added"));
|
||||
|
|
@ -267,8 +328,12 @@ export class WindowManager {
|
|||
return this.widgetWindow;
|
||||
}
|
||||
|
||||
getOnboardingWindow(): BrowserWindow | null {
|
||||
return this.onboardingWindow;
|
||||
}
|
||||
|
||||
getAllWindows(): (BrowserWindow | null)[] {
|
||||
return [this.mainWindow, this.widgetWindow];
|
||||
return [this.mainWindow, this.widgetWindow, this.onboardingWindow];
|
||||
}
|
||||
|
||||
openAllDevTools(): void {
|
||||
|
|
|
|||
70
apps/desktop/src/main/onboarding-preload.ts
Normal file
70
apps/desktop/src/main/onboarding-preload.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { contextBridge, ipcRenderer } from "electron";
|
||||
import { exposeElectronTRPC } from "electron-trpc-experimental/preload";
|
||||
|
||||
interface OnboardingAPI {
|
||||
// Permission checks
|
||||
checkMicrophonePermission: () => Promise<string>;
|
||||
checkAccessibilityPermission: () => Promise<boolean>;
|
||||
|
||||
// Permission requests
|
||||
requestMicrophonePermission: () => Promise<boolean>;
|
||||
requestAccessibilityPermission: () => Promise<void>;
|
||||
|
||||
// Navigation
|
||||
completeOnboarding: () => Promise<void>;
|
||||
|
||||
// Window controls
|
||||
quitApp: () => Promise<void>;
|
||||
|
||||
// System info
|
||||
getPlatform: () => Promise<string>;
|
||||
|
||||
// External links
|
||||
openExternal: (url: string) => Promise<void>;
|
||||
|
||||
// Logging
|
||||
log: {
|
||||
error: (...args: any[]) => Promise<void>;
|
||||
};
|
||||
}
|
||||
|
||||
const api: OnboardingAPI = {
|
||||
// Permission checks
|
||||
checkMicrophonePermission: () =>
|
||||
ipcRenderer.invoke("onboarding:check-microphone-permission"),
|
||||
|
||||
checkAccessibilityPermission: () =>
|
||||
ipcRenderer.invoke("onboarding:check-accessibility-permission"),
|
||||
|
||||
// Permission requests
|
||||
requestMicrophonePermission: () =>
|
||||
ipcRenderer.invoke("onboarding:request-microphone-permission"),
|
||||
|
||||
requestAccessibilityPermission: () =>
|
||||
ipcRenderer.invoke("onboarding:request-accessibility-permission"),
|
||||
|
||||
// Navigation
|
||||
completeOnboarding: () => ipcRenderer.invoke("onboarding:complete"),
|
||||
|
||||
// Window controls
|
||||
quitApp: () => ipcRenderer.invoke("onboarding:quit-app"),
|
||||
|
||||
// System info
|
||||
getPlatform: () => ipcRenderer.invoke("onboarding:get-platform"),
|
||||
|
||||
// External links
|
||||
openExternal: (url: string) => ipcRenderer.invoke("open-external", url),
|
||||
|
||||
// Logging
|
||||
log: {
|
||||
error: (...args: any[]) =>
|
||||
ipcRenderer.invoke("log-message", "error", "onboarding", ...args),
|
||||
},
|
||||
};
|
||||
|
||||
contextBridge.exposeInMainWorld("onboardingAPI", api);
|
||||
|
||||
// Expose tRPC for electron-trpc-experimental
|
||||
process.once("loaded", async () => {
|
||||
exposeElectronTRPC();
|
||||
});
|
||||
50
apps/desktop/src/renderer/onboarding/App.tsx
Normal file
50
apps/desktop/src/renderer/onboarding/App.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { UnifiedPermissionsStep } from "./components/UnifiedPermissionsStep";
|
||||
|
||||
interface PermissionStatus {
|
||||
microphone: "granted" | "denied" | "not-determined";
|
||||
accessibility: boolean;
|
||||
}
|
||||
|
||||
export function App() {
|
||||
const [permissions, setPermissions] = useState<PermissionStatus>({
|
||||
microphone: "not-determined",
|
||||
accessibility: false,
|
||||
});
|
||||
const [platform, setPlatform] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
// Check initial permissions and platform
|
||||
checkPermissions();
|
||||
window.onboardingAPI.getPlatform().then(setPlatform);
|
||||
}, []);
|
||||
|
||||
const checkPermissions = async () => {
|
||||
const [micStatus, accessStatus] = await Promise.all([
|
||||
window.onboardingAPI.checkMicrophonePermission(),
|
||||
window.onboardingAPI.checkAccessibilityPermission(),
|
||||
]);
|
||||
|
||||
setPermissions({
|
||||
microphone: micStatus as "granted" | "denied" | "not-determined",
|
||||
accessibility: accessStatus,
|
||||
});
|
||||
};
|
||||
|
||||
const handleComplete = () => {
|
||||
window.onboardingAPI.completeOnboarding();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-screen w-screen bg-background text-foreground overflow-hidden">
|
||||
<div className="h-full flex items-center justify-center p-10">
|
||||
<UnifiedPermissionsStep
|
||||
permissions={permissions}
|
||||
platform={platform}
|
||||
onComplete={handleComplete}
|
||||
checkPermissions={checkPermissions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,252 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import {
|
||||
CheckCircle,
|
||||
AlertCircle,
|
||||
Mic,
|
||||
Accessibility,
|
||||
ExternalLink,
|
||||
RefreshCw,
|
||||
} from "lucide-react";
|
||||
|
||||
interface UnifiedPermissionsStepProps {
|
||||
permissions: {
|
||||
microphone: "granted" | "denied" | "not-determined";
|
||||
accessibility: boolean;
|
||||
};
|
||||
platform: string;
|
||||
onComplete: () => void;
|
||||
checkPermissions: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function UnifiedPermissionsStep({
|
||||
permissions,
|
||||
platform,
|
||||
onComplete,
|
||||
checkPermissions,
|
||||
}: UnifiedPermissionsStepProps) {
|
||||
const [isRequestingMic, setIsRequestingMic] = useState(false);
|
||||
const [isPolling, setIsPolling] = useState(false);
|
||||
|
||||
const allPermissionsGranted =
|
||||
permissions.microphone === "granted" &&
|
||||
(permissions.accessibility || platform !== "darwin");
|
||||
|
||||
// Poll for permission changes continuously to keep UI in sync
|
||||
useEffect(() => {
|
||||
// Always poll to detect permission changes in real-time
|
||||
const interval = setInterval(async () => {
|
||||
await checkPermissions();
|
||||
}, 2000);
|
||||
|
||||
// Show polling indicator only when permissions are not all granted
|
||||
setIsPolling(!allPermissionsGranted);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [checkPermissions, allPermissionsGranted]);
|
||||
|
||||
const handleRequestMicrophone = async () => {
|
||||
setIsRequestingMic(true);
|
||||
try {
|
||||
await window.onboardingAPI.requestMicrophonePermission();
|
||||
await checkPermissions();
|
||||
} finally {
|
||||
setIsRequestingMic(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenAccessibility = async () => {
|
||||
// Open System Preferences > Security & Privacy > Privacy > Accessibility
|
||||
await window.onboardingAPI.openExternal(
|
||||
"x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility",
|
||||
);
|
||||
};
|
||||
|
||||
const handleOpenMicrophoneSettings = async () => {
|
||||
// Open System Preferences > Security & Privacy > Privacy > Microphone
|
||||
await window.onboardingAPI.openExternal(
|
||||
"x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone",
|
||||
);
|
||||
};
|
||||
|
||||
const getMicrophoneStatus = () => {
|
||||
switch (permissions.microphone) {
|
||||
case "granted":
|
||||
return {
|
||||
icon: CheckCircle,
|
||||
color: "text-green-500",
|
||||
bg: "bg-green-500/10",
|
||||
};
|
||||
case "denied":
|
||||
return {
|
||||
icon: AlertCircle,
|
||||
color: "text-red-500",
|
||||
bg: "bg-red-500/10",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
icon: RefreshCw,
|
||||
color: "text-blue-500",
|
||||
bg: "bg-blue-500/10",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const getAccessibilityStatus = () => {
|
||||
if (permissions.accessibility) {
|
||||
return {
|
||||
icon: CheckCircle,
|
||||
color: "text-green-500",
|
||||
bg: "bg-green-500/10",
|
||||
};
|
||||
}
|
||||
return {
|
||||
icon: AlertCircle,
|
||||
color: "text-amber-500",
|
||||
bg: "bg-amber-500/10",
|
||||
};
|
||||
};
|
||||
|
||||
const micStatus = getMicrophoneStatus();
|
||||
const accessStatus = getAccessibilityStatus();
|
||||
|
||||
return (
|
||||
<div className="max-w-lg w-full space-y-6">
|
||||
{/* Header with logo */}
|
||||
<div className="text-center space-y-4">
|
||||
<img
|
||||
src="/assets/logo.svg"
|
||||
alt="Amical"
|
||||
className="w-20 h-20 mx-auto"
|
||||
/>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Permissions Required</h1>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Amical needs these permissions to work properly
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Permission Cards */}
|
||||
<div className="space-y-3">
|
||||
{/* Microphone Permission */}
|
||||
<Card className="p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div
|
||||
className={`w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0 ${micStatus.bg}`}
|
||||
>
|
||||
<Mic className={`w-5 h-5 ${micStatus.color}`} />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-medium">Microphone</h3>
|
||||
<div className={`flex items-center gap-1 ${micStatus.color}`}>
|
||||
<micStatus.icon className="w-4 h-4" />
|
||||
<span className="text-sm capitalize">
|
||||
{permissions.microphone}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Required to transcribe your voice into text
|
||||
</p>
|
||||
|
||||
{permissions.microphone === "denied" && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={handleOpenMicrophoneSettings}
|
||||
className="mt-3 w-full"
|
||||
>
|
||||
Open Microphone Settings
|
||||
<ExternalLink className="w-3 h-3 ml-2" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{permissions.microphone === "not-determined" && (
|
||||
<Button
|
||||
size="sm"
|
||||
className="mt-3 w-full"
|
||||
onClick={handleRequestMicrophone}
|
||||
disabled={isRequestingMic}
|
||||
>
|
||||
{isRequestingMic ? "Requesting..." : "Grant Permission"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Accessibility Permission (macOS only) */}
|
||||
{platform === "darwin" && (
|
||||
<Card className="p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div
|
||||
className={`w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0 ${accessStatus.bg}`}
|
||||
>
|
||||
<Accessibility className={`w-5 h-5 ${accessStatus.color}`} />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-medium">Accessibility</h3>
|
||||
<div
|
||||
className={`flex items-center gap-1 ${accessStatus.color}`}
|
||||
>
|
||||
<accessStatus.icon className="w-4 h-4" />
|
||||
<span className="text-sm">
|
||||
{permissions.accessibility ? "Enabled" : "Disabled"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Required for context based formating and push to talk
|
||||
</p>
|
||||
|
||||
{!permissions.accessibility && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={handleOpenAccessibility}
|
||||
className="mt-3 w-full"
|
||||
>
|
||||
Open Accessibility Settings
|
||||
<ExternalLink className="w-3 h-3 ml-2" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Status message */}
|
||||
{isPolling && !allPermissionsGranted && (
|
||||
<div className="text-center text-sm text-muted-foreground flex items-center justify-center gap-2">
|
||||
<RefreshCw className="w-4 h-4 animate-spin" />
|
||||
Checking permissions...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex justify-between gap-4 pt-4">
|
||||
<Button
|
||||
onClick={() => window.onboardingAPI.quitApp()}
|
||||
variant="outline"
|
||||
size="lg"
|
||||
>
|
||||
Quit Amical
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onComplete}
|
||||
size="lg"
|
||||
disabled={!allPermissionsGranted}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
25
apps/desktop/src/renderer/onboarding/index.tsx
Normal file
25
apps/desktop/src/renderer/onboarding/index.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { App } from "./App";
|
||||
import "@/styles/globals.css";
|
||||
|
||||
// Handle uncaught errors
|
||||
window.addEventListener("unhandledrejection", (event) => {
|
||||
console.error("Unhandled promise rejection:", event.reason);
|
||||
window.onboardingAPI.log.error("Unhandled promise rejection:", event.reason);
|
||||
});
|
||||
|
||||
window.addEventListener("error", (event) => {
|
||||
console.error("Uncaught error:", event.error);
|
||||
window.onboardingAPI.log.error("Uncaught error:", event.error);
|
||||
});
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById("root") as HTMLElement,
|
||||
);
|
||||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
7
apps/desktop/src/renderer/onboarding/types.d.ts
vendored
Normal file
7
apps/desktop/src/renderer/onboarding/types.d.ts
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import type { OnboardingAPI } from "@/types/onboarding-api";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
onboardingAPI: OnboardingAPI;
|
||||
}
|
||||
}
|
||||
26
apps/desktop/src/types/onboarding-api.ts
Normal file
26
apps/desktop/src/types/onboarding-api.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
export interface OnboardingAPI {
|
||||
// Permission checks
|
||||
checkMicrophonePermission: () => Promise<string>;
|
||||
checkAccessibilityPermission: () => Promise<boolean>;
|
||||
|
||||
// Permission requests
|
||||
requestMicrophonePermission: () => Promise<boolean>;
|
||||
requestAccessibilityPermission: () => Promise<void>;
|
||||
|
||||
// Navigation
|
||||
completeOnboarding: () => Promise<void>;
|
||||
|
||||
// Window controls
|
||||
quitApp: () => Promise<void>;
|
||||
|
||||
// System info
|
||||
getPlatform: () => Promise<string>;
|
||||
|
||||
// External links
|
||||
openExternal: (url: string) => Promise<void>;
|
||||
|
||||
// Logging
|
||||
log: {
|
||||
error: (...args: any[]) => Promise<void>;
|
||||
};
|
||||
}
|
||||
10
apps/desktop/vite.onboarding-preload.config.mts
Normal file
10
apps/desktop/vite.onboarding-preload.config.mts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { defineConfig } from "vite";
|
||||
|
||||
// https://vitejs.dev/config
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": new URL("./src", import.meta.url).pathname,
|
||||
},
|
||||
},
|
||||
});
|
||||
27
apps/desktop/vite.onboarding.config.mts
Normal file
27
apps/desktop/vite.onboarding.config.mts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { defineConfig } from "vite";
|
||||
import { resolve } from "path";
|
||||
|
||||
// https://vitejs.dev/config
|
||||
export default defineConfig(async () => {
|
||||
// @ts-ignore
|
||||
const { default: tailwindcss } = await import("@tailwindcss/vite");
|
||||
|
||||
return {
|
||||
plugins: [tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": resolve(__dirname, "src"),
|
||||
},
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: ["better-sqlite3"],
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: {
|
||||
main: resolve(__dirname, "onboarding.html"),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue