fix first launch on macos (#690)
This commit is contained in:
parent
6827fb51d4
commit
9b361f661b
5 changed files with 96 additions and 15 deletions
|
|
@ -164,3 +164,28 @@ pub fn initialize_enigo(app: AppHandle) -> Result<(), String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Marker state to track if shortcuts have been initialized.
|
||||||
|
pub struct ShortcutsInitialized;
|
||||||
|
|
||||||
|
/// Initialize keyboard shortcuts.
|
||||||
|
/// On macOS, this should be called after accessibility permissions are granted.
|
||||||
|
/// This is idempotent - calling it multiple times is safe.
|
||||||
|
#[specta::specta]
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn initialize_shortcuts(app: AppHandle) -> Result<(), String> {
|
||||||
|
// Check if already initialized
|
||||||
|
if app.try_state::<ShortcutsInitialized>().is_some() {
|
||||||
|
log::debug!("Shortcuts already initialized");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize shortcuts
|
||||||
|
crate::shortcut::init_shortcuts(&app);
|
||||||
|
|
||||||
|
// Mark as initialized
|
||||||
|
app.manage(ShortcutsInitialized);
|
||||||
|
|
||||||
|
log::info!("Shortcuts initialized successfully");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -134,8 +134,10 @@ fn initialize_core_logic(app_handle: &AppHandle) {
|
||||||
app_handle.manage(transcription_manager.clone());
|
app_handle.manage(transcription_manager.clone());
|
||||||
app_handle.manage(history_manager.clone());
|
app_handle.manage(history_manager.clone());
|
||||||
|
|
||||||
// Initialize the shortcuts
|
// Note: Shortcuts are NOT initialized here.
|
||||||
shortcut::init_shortcuts(app_handle);
|
// The frontend is responsible for calling the `initialize_shortcuts` command
|
||||||
|
// after permissions are confirmed (on macOS) or after onboarding completes.
|
||||||
|
// This matches the pattern used for Enigo initialization.
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
let signals = Signals::new(&[SIGUSR2]).unwrap();
|
let signals = Signals::new(&[SIGUSR2]).unwrap();
|
||||||
|
|
@ -285,6 +287,7 @@ pub fn run() {
|
||||||
commands::open_app_data_dir,
|
commands::open_app_data_dir,
|
||||||
commands::check_apple_intelligence_available,
|
commands::check_apple_intelligence_available,
|
||||||
commands::initialize_enigo,
|
commands::initialize_enigo,
|
||||||
|
commands::initialize_shortcuts,
|
||||||
commands::models::get_available_models,
|
commands::models::get_available_models,
|
||||||
commands::models::get_model_info,
|
commands::models::get_model_info,
|
||||||
commands::models::download_model,
|
commands::models::download_model,
|
||||||
|
|
|
||||||
48
src/App.tsx
48
src/App.tsx
|
|
@ -1,5 +1,10 @@
|
||||||
import { useEffect, useState, useRef } from "react";
|
import { useEffect, useState, useRef } from "react";
|
||||||
import { Toaster } from "sonner";
|
import { Toaster } from "sonner";
|
||||||
|
import { platform } from "@tauri-apps/plugin-os";
|
||||||
|
import {
|
||||||
|
checkAccessibilityPermission,
|
||||||
|
checkMicrophonePermission,
|
||||||
|
} from "tauri-plugin-macos-permissions-api";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
import AccessibilityPermissions from "./components/AccessibilityPermissions";
|
import AccessibilityPermissions from "./components/AccessibilityPermissions";
|
||||||
import Footer from "./components/footer";
|
import Footer from "./components/footer";
|
||||||
|
|
@ -21,6 +26,9 @@ function App() {
|
||||||
const [onboardingStep, setOnboardingStep] = useState<OnboardingStep | null>(
|
const [onboardingStep, setOnboardingStep] = useState<OnboardingStep | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
// Track if this is a returning user who just needs to grant permissions
|
||||||
|
// (vs a new user who needs full onboarding including model selection)
|
||||||
|
const [isReturningUser, setIsReturningUser] = useState(false);
|
||||||
const [currentSection, setCurrentSection] =
|
const [currentSection, setCurrentSection] =
|
||||||
useState<SidebarSection>("general");
|
useState<SidebarSection>("general");
|
||||||
const { settings, updateSetting } = useSettings();
|
const { settings, updateSetting } = useSettings();
|
||||||
|
|
@ -36,12 +44,15 @@ function App() {
|
||||||
checkOnboardingStatus();
|
checkOnboardingStatus();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Initialize Enigo and refresh audio devices when main app loads
|
// Initialize Enigo, shortcuts, and refresh audio devices when main app loads
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (onboardingStep === "done" && !hasCompletedPostOnboardingInit.current) {
|
if (onboardingStep === "done" && !hasCompletedPostOnboardingInit.current) {
|
||||||
hasCompletedPostOnboardingInit.current = true;
|
hasCompletedPostOnboardingInit.current = true;
|
||||||
commands.initializeEnigo().catch((e) => {
|
Promise.all([
|
||||||
console.warn("Failed to initialize Enigo:", e);
|
commands.initializeEnigo(),
|
||||||
|
commands.initializeShortcuts(),
|
||||||
|
]).catch((e) => {
|
||||||
|
console.warn("Failed to initialize:", e);
|
||||||
});
|
});
|
||||||
refreshAudioDevices();
|
refreshAudioDevices();
|
||||||
refreshOutputDevices();
|
refreshOutputDevices();
|
||||||
|
|
@ -77,10 +88,31 @@ function App() {
|
||||||
try {
|
try {
|
||||||
// Check if they have any models available
|
// Check if they have any models available
|
||||||
const result = await commands.hasAnyModelsAvailable();
|
const result = await commands.hasAnyModelsAvailable();
|
||||||
if (result.status === "ok") {
|
const hasModels = result.status === "ok" && result.data;
|
||||||
// If they have models/downloads, they're done. Otherwise start permissions step.
|
|
||||||
setOnboardingStep(result.data ? "done" : "accessibility");
|
if (hasModels) {
|
||||||
|
// Returning user - but check if they need to grant permissions on macOS
|
||||||
|
setIsReturningUser(true);
|
||||||
|
if (platform() === "macos") {
|
||||||
|
try {
|
||||||
|
const [hasAccessibility, hasMicrophone] = await Promise.all([
|
||||||
|
checkAccessibilityPermission(),
|
||||||
|
checkMicrophonePermission(),
|
||||||
|
]);
|
||||||
|
if (!hasAccessibility || !hasMicrophone) {
|
||||||
|
// Missing permissions - show accessibility onboarding
|
||||||
|
setOnboardingStep("accessibility");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Failed to check permissions:", e);
|
||||||
|
// If we can't check, proceed to main app and let them fix it there
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setOnboardingStep("done");
|
||||||
} else {
|
} else {
|
||||||
|
// New user - start full onboarding
|
||||||
|
setIsReturningUser(false);
|
||||||
setOnboardingStep("accessibility");
|
setOnboardingStep("accessibility");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -90,7 +122,9 @@ function App() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAccessibilityComplete = () => {
|
const handleAccessibilityComplete = () => {
|
||||||
setOnboardingStep("model");
|
// Returning users already have models, skip to main app
|
||||||
|
// New users need to select a model
|
||||||
|
setOnboardingStep(isReturningUser ? "done" : "model");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleModelSelected = () => {
|
const handleModelSelected = () => {
|
||||||
|
|
|
||||||
|
|
@ -412,6 +412,19 @@ async initializeEnigo() : Promise<Result<null, string>> {
|
||||||
else return { status: "error", error: e as any };
|
else return { status: "error", error: e as any };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Initialize keyboard shortcuts.
|
||||||
|
* On macOS, this should be called after accessibility permissions are granted.
|
||||||
|
* This is idempotent - calling it multiple times is safe.
|
||||||
|
*/
|
||||||
|
async initializeShortcuts() : Promise<Result<null, string>> {
|
||||||
|
try {
|
||||||
|
return { status: "ok", data: await TAURI_INVOKE("initialize_shortcuts") };
|
||||||
|
} catch (e) {
|
||||||
|
if(e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
async getAvailableModels() : Promise<Result<ModelInfo[], string>> {
|
async getAvailableModels() : Promise<Result<ModelInfo[], string>> {
|
||||||
try {
|
try {
|
||||||
return { status: "ok", data: await TAURI_INVOKE("get_available_models") };
|
return { status: "ok", data: await TAURI_INVOKE("get_available_models") };
|
||||||
|
|
|
||||||
|
|
@ -68,12 +68,15 @@ const AccessibilityOnboarding: React.FC<AccessibilityOnboardingProps> = ({
|
||||||
checkMicrophonePermission(),
|
checkMicrophonePermission(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// If accessibility is granted, initialize Enigo
|
// If accessibility is granted, initialize Enigo and shortcuts
|
||||||
if (accessibilityGranted) {
|
if (accessibilityGranted) {
|
||||||
try {
|
try {
|
||||||
await commands.initializeEnigo();
|
await Promise.all([
|
||||||
|
commands.initializeEnigo(),
|
||||||
|
commands.initializeShortcuts(),
|
||||||
|
]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Failed to initialize Enigo:", e);
|
console.warn("Failed to initialize after permission grant:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,9 +121,12 @@ const AccessibilityOnboarding: React.FC<AccessibilityOnboardingProps> = ({
|
||||||
|
|
||||||
if (accessibilityGranted && prev.accessibility !== "granted") {
|
if (accessibilityGranted && prev.accessibility !== "granted") {
|
||||||
newState.accessibility = "granted";
|
newState.accessibility = "granted";
|
||||||
// Initialize Enigo when accessibility is granted
|
// Initialize Enigo and shortcuts when accessibility is granted
|
||||||
commands.initializeEnigo().catch((e) => {
|
Promise.all([
|
||||||
console.warn("Failed to initialize Enigo:", e);
|
commands.initializeEnigo(),
|
||||||
|
commands.initializeShortcuts(),
|
||||||
|
]).catch((e) => {
|
||||||
|
console.warn("Failed to initialize after permission grant:", e);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue