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(history_manager.clone());
|
||||
|
||||
// Initialize the shortcuts
|
||||
shortcut::init_shortcuts(app_handle);
|
||||
// Note: Shortcuts are NOT initialized here.
|
||||
// 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)]
|
||||
let signals = Signals::new(&[SIGUSR2]).unwrap();
|
||||
|
|
@ -285,6 +287,7 @@ pub fn run() {
|
|||
commands::open_app_data_dir,
|
||||
commands::check_apple_intelligence_available,
|
||||
commands::initialize_enigo,
|
||||
commands::initialize_shortcuts,
|
||||
commands::models::get_available_models,
|
||||
commands::models::get_model_info,
|
||||
commands::models::download_model,
|
||||
|
|
|
|||
48
src/App.tsx
48
src/App.tsx
|
|
@ -1,5 +1,10 @@
|
|||
import { useEffect, useState, useRef } from "react";
|
||||
import { Toaster } from "sonner";
|
||||
import { platform } from "@tauri-apps/plugin-os";
|
||||
import {
|
||||
checkAccessibilityPermission,
|
||||
checkMicrophonePermission,
|
||||
} from "tauri-plugin-macos-permissions-api";
|
||||
import "./App.css";
|
||||
import AccessibilityPermissions from "./components/AccessibilityPermissions";
|
||||
import Footer from "./components/footer";
|
||||
|
|
@ -21,6 +26,9 @@ function App() {
|
|||
const [onboardingStep, setOnboardingStep] = useState<OnboardingStep | 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] =
|
||||
useState<SidebarSection>("general");
|
||||
const { settings, updateSetting } = useSettings();
|
||||
|
|
@ -36,12 +44,15 @@ function App() {
|
|||
checkOnboardingStatus();
|
||||
}, []);
|
||||
|
||||
// Initialize Enigo and refresh audio devices when main app loads
|
||||
// Initialize Enigo, shortcuts, and refresh audio devices when main app loads
|
||||
useEffect(() => {
|
||||
if (onboardingStep === "done" && !hasCompletedPostOnboardingInit.current) {
|
||||
hasCompletedPostOnboardingInit.current = true;
|
||||
commands.initializeEnigo().catch((e) => {
|
||||
console.warn("Failed to initialize Enigo:", e);
|
||||
Promise.all([
|
||||
commands.initializeEnigo(),
|
||||
commands.initializeShortcuts(),
|
||||
]).catch((e) => {
|
||||
console.warn("Failed to initialize:", e);
|
||||
});
|
||||
refreshAudioDevices();
|
||||
refreshOutputDevices();
|
||||
|
|
@ -77,10 +88,31 @@ function App() {
|
|||
try {
|
||||
// Check if they have any models available
|
||||
const result = await commands.hasAnyModelsAvailable();
|
||||
if (result.status === "ok") {
|
||||
// If they have models/downloads, they're done. Otherwise start permissions step.
|
||||
setOnboardingStep(result.data ? "done" : "accessibility");
|
||||
const hasModels = result.status === "ok" && result.data;
|
||||
|
||||
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 {
|
||||
// New user - start full onboarding
|
||||
setIsReturningUser(false);
|
||||
setOnboardingStep("accessibility");
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -90,7 +122,9 @@ function App() {
|
|||
};
|
||||
|
||||
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 = () => {
|
||||
|
|
|
|||
|
|
@ -412,6 +412,19 @@ async initializeEnigo() : Promise<Result<null, string>> {
|
|||
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>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_available_models") };
|
||||
|
|
|
|||
|
|
@ -68,12 +68,15 @@ const AccessibilityOnboarding: React.FC<AccessibilityOnboardingProps> = ({
|
|||
checkMicrophonePermission(),
|
||||
]);
|
||||
|
||||
// If accessibility is granted, initialize Enigo
|
||||
// If accessibility is granted, initialize Enigo and shortcuts
|
||||
if (accessibilityGranted) {
|
||||
try {
|
||||
await commands.initializeEnigo();
|
||||
await Promise.all([
|
||||
commands.initializeEnigo(),
|
||||
commands.initializeShortcuts(),
|
||||
]);
|
||||
} 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") {
|
||||
newState.accessibility = "granted";
|
||||
// Initialize Enigo when accessibility is granted
|
||||
commands.initializeEnigo().catch((e) => {
|
||||
console.warn("Failed to initialize Enigo:", e);
|
||||
// Initialize Enigo and shortcuts when accessibility is granted
|
||||
Promise.all([
|
||||
commands.initializeEnigo(),
|
||||
commands.initializeShortcuts(),
|
||||
]).catch((e) => {
|
||||
console.warn("Failed to initialize after permission grant:", e);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue