feat: add update check functionality and improve menu options for macOS

This commit is contained in:
Bohdan Triapitsyn
2025-12-17 18:57:20 +02:00
parent 7425e446ef
commit 9bd866ade1
7 changed files with 310 additions and 2 deletions
+1 -1
View File
@@ -2847,7 +2847,7 @@ dependencies = [
[[package]]
name = "openchamber-desktop"
version = "1.2.2"
version = "1.2.3"
dependencies = [
"anyhow",
"axum",
+159
View File
@@ -79,6 +79,18 @@ const MODELS_DEV_API_URL: &str = "https://models.dev/api.json";
const MODELS_METADATA_CACHE_TTL: Duration = Duration::from_secs(5 * 60);
const MODELS_METADATA_REQUEST_TIMEOUT: Duration = Duration::from_secs(8);
const CHECK_FOR_UPDATES_EVENT: &str = "openchamber:check-for-updates";
#[cfg(target_os = "macos")]
const MENU_ITEM_CHECK_FOR_UPDATES_ID: &str = "openchamber_check_for_updates";
#[cfg(target_os = "macos")]
const MENU_ITEM_REPORT_BUG_ID: &str = "openchamber_report_bug";
#[cfg(target_os = "macos")]
const MENU_ITEM_REQUEST_FEATURE_ID: &str = "openchamber_request_feature";
const GITHUB_BUG_REPORT_URL: &str = "https://github.com/btriapitsyn/openchamber/issues/new?template=bug_report.yml";
const GITHUB_FEATURE_REQUEST_URL: &str = "https://github.com/btriapitsyn/openchamber/issues/new?template=feature_request.yml";
#[derive(Clone)]
pub(crate) struct DesktopRuntime {
server_port: u16,
@@ -277,6 +289,116 @@ fn prevent_app_nap() {
info!("[macos] App Nap prevention enabled via objc2");
}
#[cfg(target_os = "macos")]
fn build_macos_menu<R: tauri::Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<tauri::menu::Menu<R>> {
use tauri::menu::{AboutMetadata, Menu, MenuItem, PredefinedMenuItem, Submenu, HELP_SUBMENU_ID, WINDOW_SUBMENU_ID};
let pkg_info = app.package_info();
let config = app.config();
let about_metadata = AboutMetadata {
name: Some(pkg_info.name.clone()),
version: Some(pkg_info.version.to_string()),
copyright: config.bundle.copyright.clone(),
authors: config.bundle.publisher.clone().map(|p| vec![p]),
..Default::default()
};
let check_for_updates = MenuItem::with_id(
app,
MENU_ITEM_CHECK_FOR_UPDATES_ID,
"Check for Updates…",
true,
None::<&str>,
)?;
let report_bug = MenuItem::with_id(
app,
MENU_ITEM_REPORT_BUG_ID,
"Report a Bug…",
true,
None::<&str>,
)?;
let request_feature = MenuItem::with_id(
app,
MENU_ITEM_REQUEST_FEATURE_ID,
"Request a Feature…",
true,
None::<&str>,
)?;
let window_menu = Submenu::with_id_and_items(
app,
WINDOW_SUBMENU_ID,
"Window",
true,
&[
&PredefinedMenuItem::minimize(app, None)?,
&PredefinedMenuItem::maximize(app, None)?,
&PredefinedMenuItem::separator(app)?,
&PredefinedMenuItem::close_window(app, None)?,
],
)?;
let help_menu = Submenu::with_id_and_items(
app,
HELP_SUBMENU_ID,
"Help",
true,
&[&report_bug, &request_feature],
)?;
Menu::with_items(
app,
&[
&Submenu::with_items(
app,
pkg_info.name.clone(),
true,
&[
&PredefinedMenuItem::about(app, None, Some(about_metadata))?,
&check_for_updates,
&PredefinedMenuItem::separator(app)?,
&PredefinedMenuItem::services(app, None)?,
&PredefinedMenuItem::separator(app)?,
&PredefinedMenuItem::hide(app, None)?,
&PredefinedMenuItem::hide_others(app, None)?,
&PredefinedMenuItem::separator(app)?,
&PredefinedMenuItem::quit(app, None)?,
],
)?,
&Submenu::with_items(
app,
"File",
true,
&[&PredefinedMenuItem::close_window(app, None)?],
)?,
&Submenu::with_items(
app,
"Edit",
true,
&[
&PredefinedMenuItem::undo(app, None)?,
&PredefinedMenuItem::redo(app, None)?,
&PredefinedMenuItem::separator(app)?,
&PredefinedMenuItem::cut(app, None)?,
&PredefinedMenuItem::copy(app, None)?,
&PredefinedMenuItem::paste(app, None)?,
&PredefinedMenuItem::select_all(app, None)?,
],
)?,
&Submenu::with_items(
app,
"View",
true,
&[&PredefinedMenuItem::fullscreen(app, None)?],
)?,
&window_menu,
&help_menu,
],
)
}
fn main() {
let mut log_builder = tauri_plugin_log::Builder::default()
.level(log::LevelFilter::Info)
@@ -299,6 +421,16 @@ fn main() {
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_process::init())
.plugin(log_builder.build())
.menu(|app| {
#[cfg(target_os = "macos")]
{
return build_macos_menu(app);
}
#[cfg(not(target_os = "macos"))]
{
return tauri::menu::Menu::default(app);
}
})
.setup(|app| {
#[cfg(target_os = "macos")]
prevent_app_nap();
@@ -418,6 +550,33 @@ fn main() {
fetch_desktop_logs,
desktop_notify,
])
.on_menu_event(|app, event| {
#[cfg(target_os = "macos")]
{
if event.id() == MENU_ITEM_CHECK_FOR_UPDATES_ID {
let _ = app.emit(CHECK_FOR_UPDATES_EVENT, ());
return;
}
if event.id() == MENU_ITEM_REPORT_BUG_ID {
use tauri_plugin_shell::ShellExt;
#[allow(deprecated)]
{
let _ = app.shell().open(GITHUB_BUG_REPORT_URL, None);
}
return;
}
if event.id() == MENU_ITEM_REQUEST_FEATURE_ID {
use tauri_plugin_shell::ShellExt;
#[allow(deprecated)]
{
let _ = app.shell().open(GITHUB_FEATURE_REQUEST_URL, None);
}
return;
}
}
})
.on_window_event(|window, event| {
let window_state_manager = window.state::<WindowStateManager>().inner().clone();
+7
View File
@@ -45,6 +45,8 @@ declare global {
}
}
const CHECK_FOR_UPDATES_EVENT = 'openchamber:check-for-updates';
const cleanupFunctions: Array<() => void | Promise<void>> = [];
try {
@@ -55,6 +57,11 @@ try {
});
cleanupFunctions.push(() => activityUnlisten());
const updateCheckUnlisten = await listen(CHECK_FOR_UPDATES_EVENT, () => {
window.dispatchEvent(new CustomEvent(CHECK_FOR_UPDATES_EVENT));
});
cleanupFunctions.push(() => updateCheckUnlisten());
requestInitialNotificationPermission().catch(err => {
console.error('[main] Failed to request notification permission:', err);
});
+41 -1
View File
@@ -10,6 +10,7 @@ export const SIDEBAR_CONTENT_WIDTH = 264;
const SIDEBAR_MIN_WIDTH = 200;
const SIDEBAR_MAX_WIDTH = 500;
const MAC_TITLEBAR_SAFE_AREA = 40;
const CHECK_FOR_UPDATES_EVENT = 'openchamber:check-for-updates';
interface SidebarProps {
isOpen: boolean;
@@ -25,6 +26,10 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen, isMobile, children })
const [updateDialogOpen, setUpdateDialogOpen] = React.useState(false);
const update = useUpdateCheck();
const pendingMenuUpdateCheckRef = React.useRef(false);
const checkForUpdates = update.checkForUpdates;
const { available, downloaded, checking } = update;
const [isDesktopApp, setIsDesktopApp] = React.useState<boolean>(() => {
if (typeof window === 'undefined') {
@@ -48,6 +53,41 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen, isMobile, children })
setIsDesktopApp(detected);
}, []);
React.useEffect(() => {
if (typeof window === 'undefined') {
return;
}
const handleMenuUpdateCheck = () => {
const hasDesktopApi =
typeof (window as typeof window & { opencodeDesktop?: unknown }).opencodeDesktop !== 'undefined';
if (!hasDesktopApi) {
return;
}
pendingMenuUpdateCheckRef.current = true;
void checkForUpdates();
};
window.addEventListener(CHECK_FOR_UPDATES_EVENT, handleMenuUpdateCheck as EventListener);
return () => {
window.removeEventListener(CHECK_FOR_UPDATES_EVENT, handleMenuUpdateCheck as EventListener);
};
}, [checkForUpdates]);
React.useEffect(() => {
if (!pendingMenuUpdateCheckRef.current) {
return;
}
if (checking) {
return;
}
if (available || downloaded) {
setUpdateDialogOpen(true);
}
pendingMenuUpdateCheckRef.current = false;
}, [available, downloaded, checking]);
React.useEffect(() => {
if (isMobile || !isResizing) {
return;
@@ -184,7 +224,7 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen, isMobile, children })
<RiSettings3Line className="h-4 w-4" />
<span>Settings</span>
</button>
{(update.available || update.downloaded) && (
{(available || downloaded) && (
<button
onClick={() => setUpdateDialogOpen(true)}
className={cn(