Switch to disable update checking (#362)

* codex thinks it did it

* run format

* properly disable on startup

* text
This commit is contained in:
CJ Pais 2025-11-27 19:17:13 -08:00 committed by GitHub
parent ee571b6810
commit 39adf1b455
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 187 additions and 96 deletions

View file

@ -165,8 +165,11 @@ fn initialize_core_logic(app_handle: &AppHandle) {
show_main_window(app);
}
"check_updates" => {
show_main_window(app);
let _ = app.emit("check-for-updates", ());
let settings = settings::get_settings(app);
if settings.update_checks_enabled {
show_main_window(app);
let _ = app.emit("check-for-updates", ());
}
}
"cancel" => {
use crate::utils::cancel_current_operation;
@ -205,6 +208,10 @@ fn initialize_core_logic(app_handle: &AppHandle) {
#[tauri::command]
#[specta::specta]
fn trigger_update_check(app: AppHandle) -> Result<(), String> {
let settings = settings::get_settings(&app);
if !settings.update_checks_enabled {
return Ok(());
}
app.emit("check-for-updates", ())
.map_err(|e| e.to_string())?;
Ok(())
@ -246,6 +253,7 @@ pub fn run() {
shortcut::suspend_binding,
shortcut::resume_binding,
shortcut::change_mute_while_recording_setting,
shortcut::change_update_checks_setting,
trigger_update_check,
commands::cancel_operation,
commands::get_app_dir_path,
@ -300,30 +308,29 @@ pub fn run() {
)
.expect("Failed to export typescript bindings");
let mut builder = tauri::Builder::default()
.plugin(
LogBuilder::new()
.level(log::LevelFilter::Trace) // Set to most verbose level globally
.max_file_size(500_000)
.rotation_strategy(RotationStrategy::KeepOne)
.clear_targets()
.targets([
// Console output respects RUST_LOG environment variable
Target::new(TargetKind::Stdout).filter({
let console_filter = console_filter.clone();
move |metadata| console_filter.enabled(metadata)
}),
// File logs respect the user's settings (stored in FILE_LOG_LEVEL atomic)
Target::new(TargetKind::LogDir {
file_name: Some("handy".into()),
})
.filter(|metadata| {
let file_level = FILE_LOG_LEVEL.load(Ordering::Relaxed);
metadata.level() <= level_filter_from_u8(file_level)
}),
])
.build(),
);
let mut builder = tauri::Builder::default().plugin(
LogBuilder::new()
.level(log::LevelFilter::Trace) // Set to most verbose level globally
.max_file_size(500_000)
.rotation_strategy(RotationStrategy::KeepOne)
.clear_targets()
.targets([
// Console output respects RUST_LOG environment variable
Target::new(TargetKind::Stdout).filter({
let console_filter = console_filter.clone();
move |metadata| console_filter.enabled(metadata)
}),
// File logs respect the user's settings (stored in FILE_LOG_LEVEL atomic)
Target::new(TargetKind::LogDir {
file_name: Some("handy".into()),
})
.filter(|metadata| {
let file_level = FILE_LOG_LEVEL.load(Ordering::Relaxed);
metadata.level() <= level_filter_from_u8(file_level)
}),
])
.build(),
);
#[cfg(target_os = "macos")]
{

View file

@ -44,7 +44,7 @@ const OVERLAY_BOTTOM_OFFSET: f64 = 40.0;
#[cfg(target_os = "windows")]
fn force_overlay_topmost(overlay_window: &tauri::webview::WebviewWindow) {
use windows::Win32::UI::WindowsAndMessaging::{
SetWindowPos, HWND_TOPMOST, SWP_NOMOVE, SWP_NOSIZE, SWP_NOACTIVATE, SWP_SHOWWINDOW,
SetWindowPos, HWND_TOPMOST, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, SWP_SHOWWINDOW,
};
// Clone because run_on_main_thread takes 'static
@ -58,7 +58,10 @@ fn force_overlay_topmost(overlay_window: &tauri::webview::WebviewWindow) {
let _ = SetWindowPos(
hwnd,
Some(HWND_TOPMOST),
0, 0, 0, 0,
0,
0,
0,
0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW,
);
}
@ -173,32 +176,26 @@ pub fn create_recording_overlay(app_handle: &AppHandle) {
if let Some((x, y)) = calculate_overlay_position(app_handle) {
// PanelBuilder creates a Tauri window then converts it to NSPanel.
// The window remains registered, so get_webview_window() still works.
match PanelBuilder::<_, RecordingOverlayPanel>::new(
app_handle,
"recording_overlay"
)
.url(WebviewUrl::App("src/overlay/index.html".into()))
.title("Recording")
.position(tauri::Position::Logical(tauri::LogicalPosition { x, y }))
.level(PanelLevel::Status)
.size(tauri::Size::Logical(tauri::LogicalSize {
width: OVERLAY_WIDTH,
height: OVERLAY_HEIGHT
}))
.has_shadow(false)
.transparent(true)
.no_activate(true)
.corner_radius(0.0)
.with_window(|w| {
w.decorations(false)
.transparent(true)
})
.collection_behavior(
CollectionBehavior::new()
.can_join_all_spaces()
.full_screen_auxiliary()
)
.build()
match PanelBuilder::<_, RecordingOverlayPanel>::new(app_handle, "recording_overlay")
.url(WebviewUrl::App("src/overlay/index.html".into()))
.title("Recording")
.position(tauri::Position::Logical(tauri::LogicalPosition { x, y }))
.level(PanelLevel::Status)
.size(tauri::Size::Logical(tauri::LogicalSize {
width: OVERLAY_WIDTH,
height: OVERLAY_HEIGHT,
}))
.has_shadow(false)
.transparent(true)
.no_activate(true)
.corner_radius(0.0)
.with_window(|w| w.decorations(false).transparent(true))
.collection_behavior(
CollectionBehavior::new()
.can_join_all_spaces()
.full_screen_auxiliary(),
)
.build()
{
Ok(panel) => {
let _ = panel.hide();
@ -221,7 +218,8 @@ pub fn show_recording_overlay(app_handle: &AppHandle) {
if let Some(overlay_window) = app_handle.get_webview_window("recording_overlay") {
// Update position before showing to prevent flicker from position changes
if let Some((x, y)) = calculate_overlay_position(app_handle) {
let _ = overlay_window.set_position(tauri::Position::Logical(tauri::LogicalPosition { x, y }));
let _ = overlay_window
.set_position(tauri::Position::Logical(tauri::LogicalPosition { x, y }));
}
let _ = overlay_window.show();

View file

@ -233,6 +233,8 @@ pub struct AppSettings {
pub start_hidden: bool,
#[serde(default = "default_autostart_enabled")]
pub autostart_enabled: bool,
#[serde(default = "default_update_checks_enabled")]
pub update_checks_enabled: bool,
#[serde(default = "default_model")]
pub selected_model: String,
#[serde(default = "default_always_on_microphone")]
@ -305,6 +307,10 @@ fn default_autostart_enabled() -> bool {
false
}
fn default_update_checks_enabled() -> bool {
true
}
fn default_selected_language() -> String {
"auto".to_string()
}
@ -451,6 +457,7 @@ pub fn get_default_settings() -> AppSettings {
sound_theme: default_sound_theme(),
start_hidden: default_start_hidden(),
autostart_enabled: default_autostart_enabled(),
update_checks_enabled: default_update_checks_enabled(),
selected_model: "".to_string(),
always_on_microphone: false,
selected_microphone: None,

View file

@ -18,7 +18,6 @@ pub fn init_shortcuts(app: &AppHandle) {
let default_bindings = settings::get_default_settings().bindings;
let user_settings = settings::load_or_create_app_settings(app);
// Register all default shortcuts, applying user customizations
for (id, default_binding) in default_bindings {
if id == "cancel" {
@ -284,6 +283,24 @@ pub fn change_autostart_setting(app: AppHandle, enabled: bool) -> Result<(), Str
Ok(())
}
#[tauri::command]
#[specta::specta]
pub fn change_update_checks_setting(app: AppHandle, enabled: bool) -> Result<(), String> {
let mut settings = settings::get_settings(&app);
settings.update_checks_enabled = enabled;
settings::write_settings(&app, settings);
let _ = app.emit(
"settings-changed",
serde_json::json!({
"setting": "update_checks_enabled",
"value": enabled
}),
);
Ok(())
}
#[tauri::command]
#[specta::specta]
pub fn update_custom_words(app: AppHandle, words: Vec<String>) -> Result<(), String> {

View file

@ -1,3 +1,4 @@
use crate::settings;
use tauri::image::Image;
use tauri::menu::{Menu, MenuItem, PredefinedMenuItem};
use tauri::tray::TrayIcon;
@ -74,6 +75,8 @@ pub fn change_tray_icon(app: &AppHandle, icon: TrayIconState) {
}
pub fn update_tray_menu(app: &AppHandle, state: &TrayIconState) {
let settings = settings::get_settings(app);
// Platform-specific accelerators
#[cfg(target_os = "macos")]
let (settings_accelerator, quit_accelerator) = (Some("Cmd+,"), Some("Cmd+Q"));
@ -90,7 +93,7 @@ pub fn update_tray_menu(app: &AppHandle, state: &TrayIconState) {
app,
"check_updates",
"Check for Updates...",
true,
settings.update_checks_enabled,
None::<&str>,
)
.expect("failed to create check updates item");

View file

@ -244,6 +244,14 @@ async changeMuteWhileRecordingSetting(enabled: boolean) : Promise<Result<null, s
else return { status: "error", error: e as any };
}
},
async changeUpdateChecksSetting(enabled: boolean) : Promise<Result<null, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("change_update_checks_setting", { enabled }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async triggerUpdateCheck() : Promise<Result<null, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("trigger_update_check") };
@ -594,7 +602,7 @@ async isLaptop() : Promise<Result<boolean, string>> {
/** user-defined types **/
export type AppSettings = { bindings: Partial<{ [key in string]: ShortcutBinding }>; push_to_talk: boolean; audio_feedback: boolean; audio_feedback_volume?: number; sound_theme?: SoundTheme; start_hidden?: boolean; autostart_enabled?: boolean; selected_model?: string; always_on_microphone?: boolean; selected_microphone?: string | null; clamshell_microphone?: string | null; selected_output_device?: string | null; translate_to_english?: boolean; selected_language?: string; overlay_position?: OverlayPosition; debug_mode?: boolean; log_level?: LogLevel; custom_words?: string[]; model_unload_timeout?: ModelUnloadTimeout; word_correction_threshold?: number; history_limit?: string; recording_retention_period?: RecordingRetentionPeriod; paste_method?: PasteMethod; clipboard_handling?: ClipboardHandling; post_process_enabled?: boolean; post_process_provider_id?: string; post_process_providers?: PostProcessProvider[]; post_process_api_keys?: Partial<{ [key in string]: string }>; post_process_models?: Partial<{ [key in string]: string }>; post_process_prompts?: LLMPrompt[]; post_process_selected_prompt_id?: string | null; mute_while_recording?: boolean }
export type AppSettings = { bindings: Partial<{ [key in string]: ShortcutBinding }>; push_to_talk: boolean; audio_feedback: boolean; audio_feedback_volume?: number; sound_theme?: SoundTheme; start_hidden?: boolean; autostart_enabled?: boolean; update_checks_enabled?: boolean; selected_model?: string; always_on_microphone?: boolean; selected_microphone?: string | null; clamshell_microphone?: string | null; selected_output_device?: string | null; translate_to_english?: boolean; selected_language?: string; overlay_position?: OverlayPosition; debug_mode?: boolean; log_level?: LogLevel; custom_words?: string[]; model_unload_timeout?: ModelUnloadTimeout; word_correction_threshold?: number; history_limit?: string; recording_retention_period?: RecordingRetentionPeriod; paste_method?: PasteMethod; clipboard_handling?: ClipboardHandling; post_process_enabled?: boolean; post_process_provider_id?: string; post_process_providers?: PostProcessProvider[]; post_process_api_keys?: Partial<{ [key in string]: string }>; post_process_models?: Partial<{ [key in string]: string }>; post_process_prompts?: LLMPrompt[]; post_process_selected_prompt_id?: string | null; mute_while_recording?: boolean }
export type AudioDevice = { index: string; name: string; is_default: boolean }
export type BindingResponse = { success: boolean; binding: ShortcutBinding | null; error: string | null }
export type ClipboardHandling = "dont_modify" | "copy_to_clipboard"

View file

@ -84,17 +84,15 @@ export const HandyShortcut: React.FC<HandyShortcutProps> = ({
if (editingShortcutId && originalBinding) {
try {
await updateBinding(editingShortcutId, originalBinding);
await commands.resumeBinding(editingShortcutId).catch(
console.error,
);
await commands
.resumeBinding(editingShortcutId)
.catch(console.error);
} catch (error) {
console.error("Failed to restore original binding:", error);
toast.error("Failed to restore original shortcut");
}
} else if (editingShortcutId) {
await commands.resumeBinding(editingShortcutId).catch(
console.error,
);
await commands.resumeBinding(editingShortcutId).catch(console.error);
}
setEditingShortcutId(null);
setKeyPressed([]);
@ -138,9 +136,9 @@ export const HandyShortcut: React.FC<HandyShortcutProps> = ({
try {
await updateBinding(editingShortcutId, newShortcut);
// Re-register the shortcut now that recording is finished
await commands.resumeBinding(editingShortcutId).catch(
console.error,
);
await commands
.resumeBinding(editingShortcutId)
.catch(console.error);
} catch (error) {
console.error("Failed to change binding:", error);
toast.error(`Failed to set shortcut: ${error}`);
@ -149,9 +147,9 @@ export const HandyShortcut: React.FC<HandyShortcutProps> = ({
if (originalBinding) {
try {
await updateBinding(editingShortcutId, originalBinding);
await commands.resumeBinding(editingShortcutId).catch(
console.error,
);
await commands
.resumeBinding(editingShortcutId)
.catch(console.error);
} catch (resetError) {
console.error("Failed to reset binding:", resetError);
toast.error("Failed to reset shortcut to original value");
@ -177,17 +175,15 @@ export const HandyShortcut: React.FC<HandyShortcutProps> = ({
if (editingShortcutId && originalBinding) {
try {
await updateBinding(editingShortcutId, originalBinding);
await commands.resumeBinding(editingShortcutId).catch(
console.error,
);
await commands
.resumeBinding(editingShortcutId)
.catch(console.error);
} catch (error) {
console.error("Failed to restore original binding:", error);
toast.error("Failed to restore original shortcut");
}
} else if (editingShortcutId) {
commands.resumeBinding(editingShortcutId).catch(
console.error,
);
commands.resumeBinding(editingShortcutId).catch(console.error);
}
setEditingShortcutId(null);
setKeyPressed([]);

View file

@ -0,0 +1,28 @@
import React from "react";
import { ToggleSwitch } from "../ui/ToggleSwitch";
import { useSettings } from "../../hooks/useSettings";
interface UpdateChecksToggleProps {
descriptionMode?: "inline" | "tooltip";
grouped?: boolean;
}
export const UpdateChecksToggle: React.FC<UpdateChecksToggleProps> = ({
descriptionMode = "tooltip",
grouped = false,
}) => {
const { getSetting, updateSetting, isUpdating } = useSettings();
const updateChecksEnabled = getSetting("update_checks_enabled") ?? true;
return (
<ToggleSwitch
checked={updateChecksEnabled}
onChange={(enabled) => updateSetting("update_checks_enabled", enabled)}
isUpdating={isUpdating("update_checks_enabled")}
label="Check for Updates"
description="Allow Handy to automatically check for updates and enable manual checks from the footer or tray menu."
descriptionMode={descriptionMode}
grouped={grouped}
/>
);
};

View file

@ -11,6 +11,7 @@ import { MuteWhileRecording } from "../MuteWhileRecording";
import { RecordingRetentionPeriodSelector } from "../RecordingRetentionPeriod";
import { ClamshellMicrophoneSelector } from "../ClamshellMicrophoneSelector";
import { HandyShortcut } from "../HandyShortcut";
import { UpdateChecksToggle } from "../UpdateChecksToggle";
import { useSettings } from "../../../hooks/useSettings";
export const DebugSettings: React.FC = () => {
@ -22,6 +23,7 @@ export const DebugSettings: React.FC = () => {
<SettingsGroup title="Debug">
<LogDirectory grouped={true} />
<LogLevelSelector grouped={true} />
<UpdateChecksToggle descriptionMode="tooltip" grouped={true} />
<SoundPicker
label="Sound Theme"
description="Choose a sound theme for recording start and stop feedback"

View file

@ -26,3 +26,4 @@ export { StartHidden } from "./StartHidden";
export { HistoryLimit } from "./HistoryLimit";
export { RecordingRetentionPeriodSelector } from "./RecordingRetentionPeriod";
export { AutostartToggle } from "./AutostartToggle";
export { UpdateChecksToggle } from "./UpdateChecksToggle";

View file

@ -180,7 +180,7 @@ const PostProcessingSettingsPromptsComponent: React.FC = () => {
try {
const result = await commands.addPostProcessPrompt(
draftName.trim(),
draftText.trim()
draftText.trim(),
);
if (result.status === "ok") {
await refreshSettings();
@ -199,7 +199,7 @@ const PostProcessingSettingsPromptsComponent: React.FC = () => {
await commands.updatePostProcessPrompt(
selectedPromptId,
draftName.trim(),
draftText.trim()
draftText.trim(),
);
await refreshSettings();
} catch (error) {

View file

@ -3,6 +3,7 @@ import { check } from "@tauri-apps/plugin-updater";
import { relaunch } from "@tauri-apps/plugin-process";
import { listen } from "@tauri-apps/api/event";
import { ProgressBar } from "../shared";
import { useSettings } from "../../hooks/useSettings";
interface UpdateCheckerProps {
className?: string;
@ -16,12 +17,29 @@ const UpdateChecker: React.FC<UpdateCheckerProps> = ({ className = "" }) => {
const [downloadProgress, setDownloadProgress] = useState(0);
const [showUpToDate, setShowUpToDate] = useState(false);
const { settings, isLoading } = useSettings();
const settingsLoaded = !isLoading && settings !== null;
const updateChecksEnabled = settings?.update_checks_enabled ?? false;
const upToDateTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
const isManualCheckRef = useRef(false);
const downloadedBytesRef = useRef(0);
const contentLengthRef = useRef(0);
useEffect(() => {
// Wait for settings to load before doing anything
if (!settingsLoaded) return;
if (!updateChecksEnabled) {
if (upToDateTimeoutRef.current) {
clearTimeout(upToDateTimeoutRef.current);
}
setIsChecking(false);
setUpdateAvailable(false);
setShowUpToDate(false);
return;
}
checkForUpdates();
// Listen for update check events
@ -35,11 +53,11 @@ const UpdateChecker: React.FC<UpdateCheckerProps> = ({ className = "" }) => {
}
updateUnlisten.then((fn) => fn());
};
}, []);
}, [settingsLoaded, updateChecksEnabled]);
// Update checking functions
const checkForUpdates = async () => {
if (isChecking) return;
if (!updateChecksEnabled || isChecking) return;
try {
setIsChecking(true);
@ -70,11 +88,13 @@ const UpdateChecker: React.FC<UpdateCheckerProps> = ({ className = "" }) => {
};
const handleManualUpdateCheck = () => {
if (!updateChecksEnabled) return;
isManualCheckRef.current = true;
checkForUpdates();
};
const installUpdate = async () => {
if (!updateChecksEnabled) return;
try {
setIsInstalling(true);
setDownloadProgress(0);
@ -119,6 +139,9 @@ const UpdateChecker: React.FC<UpdateCheckerProps> = ({ className = "" }) => {
// Update status functions
const getUpdateStatusText = () => {
if (!updateChecksEnabled) {
return "Update Checking Disabled";
}
if (isInstalling) {
return downloadProgress > 0 && downloadProgress < 100
? `Downloading... ${downloadProgress.toString().padStart(3)}%`
@ -133,13 +156,14 @@ const UpdateChecker: React.FC<UpdateCheckerProps> = ({ className = "" }) => {
};
const getUpdateStatusAction = () => {
if (!updateChecksEnabled) return undefined;
if (updateAvailable && !isInstalling) return installUpdate;
if (!isChecking && !isInstalling && !updateAvailable)
return handleManualUpdateCheck;
return undefined;
};
const isUpdateDisabled = isChecking || isInstalling;
const isUpdateDisabled = !updateChecksEnabled || isChecking || isInstalling;
const isUpdateClickable =
!isUpdateDisabled && (updateAvailable || (!isChecking && !showUpToDate));

View file

@ -76,24 +76,28 @@ const settingUpdaters: {
commands.changeAudioFeedbackSetting(value as boolean),
audio_feedback_volume: (value) =>
commands.changeAudioFeedbackVolumeSetting(value as number),
sound_theme: (value) =>
commands.changeSoundThemeSetting(value as string),
start_hidden: (value) =>
commands.changeStartHiddenSetting(value as boolean),
sound_theme: (value) => commands.changeSoundThemeSetting(value as string),
start_hidden: (value) => commands.changeStartHiddenSetting(value as boolean),
autostart_enabled: (value) =>
commands.changeAutostartSetting(value as boolean),
update_checks_enabled: (value) =>
commands.changeUpdateChecksSetting(value as boolean),
push_to_talk: (value) => commands.changePttSetting(value as boolean),
selected_microphone: (value) =>
commands.setSelectedMicrophone(
(value as string) === "Default" || value === null ? "default" : (value as string)
(value as string) === "Default" || value === null
? "default"
: (value as string),
),
clamshell_microphone: (value) =>
commands.setClamshellMicrophone(
(value as string) === "Default" ? "default" : (value as string)
(value as string) === "Default" ? "default" : (value as string),
),
selected_output_device: (value) =>
commands.setSelectedOutputDevice(
(value as string) === "Default" || value === null ? "default" : (value as string)
(value as string) === "Default" || value === null
? "default"
: (value as string),
),
recording_retention_period: (value) =>
commands.updateRecordingRetentionPeriod(value as string),
@ -103,13 +107,11 @@ const settingUpdaters: {
commands.changeSelectedLanguageSetting(value as string),
overlay_position: (value) =>
commands.changeOverlayPositionSetting(value as string),
debug_mode: (value) =>
commands.changeDebugModeSetting(value as boolean),
debug_mode: (value) => commands.changeDebugModeSetting(value as boolean),
custom_words: (value) => commands.updateCustomWords(value as string[]),
word_correction_threshold: (value) =>
commands.changeWordCorrectionThresholdSetting(value as number),
paste_method: (value) =>
commands.changePasteMethodSetting(value as string),
paste_method: (value) => commands.changePasteMethodSetting(value as string),
clipboard_handling: (value) =>
commands.changeClipboardHandlingSetting(value as string),
history_limit: (value) => commands.updateHistoryLimit(value as string),
@ -158,10 +160,8 @@ export const useSettingsStore = create<SettingsStore>()(
const normalizedSettings: Settings = {
...settings,
always_on_microphone: settings.always_on_microphone ?? false,
selected_microphone:
settings.selected_microphone ?? "Default",
clamshell_microphone:
settings.clamshell_microphone ?? "Default",
selected_microphone: settings.selected_microphone ?? "Default",
clamshell_microphone: settings.clamshell_microphone ?? "Default",
selected_output_device:
settings.selected_output_device ?? "Default",
};