diff --git a/packages/tauri/src/bin.rs b/packages/tauri/src/bin.rs index c9804f4cc..9ed90b6ac 100644 --- a/packages/tauri/src/bin.rs +++ b/packages/tauri/src/bin.rs @@ -4,7 +4,7 @@ use anyhow::Context; use tauri::{generate_context, Manager}; use gblib::{ - analytics, app, assets, commands, database, deltas, github, keys, logs, projects, sentry, + analytics, app, assets, commands, database, deltas, github, keys, logs, menu, projects, sentry, sessions, storage, users, virtual_branches, watcher, zip, }; @@ -27,13 +27,7 @@ fn main() { let tray_menu = tauri::SystemTrayMenu::new().add_item(hide).add_item(quit); let tray = tauri::SystemTray::new().with_menu(tray_menu); - let project_settings = tauri::CustomMenuItem::new("projectsettings".to_string(), "Project Settings"); - let project_submenu = tauri::Submenu::new("Project", tauri::Menu::new().add_item(project_settings.disabled())); - let menu = tauri::Menu::os_default(&app_title) - .add_submenu(project_submenu); - tauri::Builder::default() - .menu(menu) .system_tray(tray) .on_system_tray_event(|app_handle, event| { if let tauri::SystemTrayEvent::MenuItemClick { id, .. } = event { @@ -68,11 +62,6 @@ fn main() { api.prevent_close(); } }) - .on_menu_event(move |event| { - if event.menu_item_id() == "projectsettings" { - _ = event.window().emit("menuAction", Some("projectSettings")); - } - }) .setup(move |tauri_app| { let window = create_window(&tauri_app.handle()).expect("Failed to create window"); @@ -168,8 +157,6 @@ fn main() { users::commands::set_user, users::commands::delete_user, users::commands::get_user, - users::commands::get_current_project, - users::commands::set_current_project, projects::commands::add_project, projects::commands::get_project, projects::commands::update_project, @@ -199,10 +186,13 @@ fn main() { virtual_branches::commands::amend_virtual_branch, virtual_branches::commands::list_remote_branches, virtual_branches::commands::squash_branch_commit, + menu::menu_item_set_enabled, keys::commands::get_public_key, github::commands::init_device_oauth, github::commands::check_auth_status, ]) + .menu(menu::build(tauri_context.package_info())) + .on_menu_event(|event|menu::handle_event(&event)) .build(tauri_context) .expect("Failed to build tauri app") .run(|app_handle, event| match event { diff --git a/packages/tauri/src/error.rs b/packages/tauri/src/error.rs index 3869ef4df..233812a95 100644 --- a/packages/tauri/src/error.rs +++ b/packages/tauri/src/error.rs @@ -12,11 +12,13 @@ pub enum Code { ProjectGitRemote, ProjectConflict, ProjectHead, + Menu, } impl fmt::Display for Code { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + Code::Menu => write!(f, "errors.menu"), Code::Unknown => write!(f, "errors.unknown"), Code::Validation => write!(f, "errors.validation"), Code::Projects => write!(f, "errors.projects"), diff --git a/packages/tauri/src/lib.rs b/packages/tauri/src/lib.rs index 172aa20bb..388de383e 100644 --- a/packages/tauri/src/lib.rs +++ b/packages/tauri/src/lib.rs @@ -17,6 +17,7 @@ pub mod id; pub mod keys; pub mod lock; pub mod logs; +pub mod menu; pub mod paths; pub mod project_repository; pub mod projects; diff --git a/packages/tauri/src/menu.rs b/packages/tauri/src/menu.rs new file mode 100644 index 000000000..a634e6d07 --- /dev/null +++ b/packages/tauri/src/menu.rs @@ -0,0 +1,58 @@ +use serde_json::json; +use tauri::{ + AppHandle, CustomMenuItem, Manager, Menu, MenuEntry, PackageInfo, Runtime, Submenu, + WindowMenuEvent, +}; +use tracing::instrument; + +use crate::error::{Code, Error}; + +#[tauri::command(async)] +#[instrument(skip(handle))] +pub async fn menu_item_set_enabled( + handle: AppHandle, + menu_item_id: &str, + enabled: bool, +) -> Result<(), Error> { + let window = handle + .get_window("main") + .expect("main window always present"); + let menu_item = window + .menu_handle() + .try_get_item(menu_item_id) + .ok_or_else(|| Error::UserError { + message: format!("menu item not found: {}", menu_item_id), + code: Code::Menu, + })?; + menu_item.set_enabled(enabled).map_err(|error| { + tracing::error!(error = ?error, "failed to set menu item enabled state"); + Error::Unknown + })?; + Ok(()) +} + +pub fn build(package_info: &PackageInfo) -> Menu { + Menu::os_default(&package_info.name).add_submenu(Submenu::new( + "Project", + Menu::with_items([disabled_menu_item("project/settings", "Project Settings")]), + )) +} + +fn disabled_menu_item(id: &str, title: &str) -> MenuEntry { + let mut item = CustomMenuItem::new(id, title); + item.enabled = false; + item.into() +} + +pub fn handle_event(event: &WindowMenuEvent) { + emit( + event.window(), + format!("menu://{}/clicked", event.menu_item_id()).as_str(), + ); +} + +fn emit(window: &tauri::Window, event: &str) { + if let Err(err) = window.emit(event, json!({})) { + tracing::error!(error = ?err, "failed to emit event"); + } +} diff --git a/packages/tauri/src/users/commands.rs b/packages/tauri/src/users/commands.rs index c4dcd2238..70cef0153 100644 --- a/packages/tauri/src/users/commands.rs +++ b/packages/tauri/src/users/commands.rs @@ -55,48 +55,6 @@ pub async fn set_user(handle: AppHandle, user: User) -> Result { Ok(proxy.proxy_user(user).await) } -#[tauri::command(async)] -#[instrument(skip(handle))] -pub async fn set_current_project( - handle: tauri::AppHandle, - project_id: Option<&str>, -) -> Result<(), Error> { - let app = handle.state::(); - if let Some(user) = app.get_user()? { - let mut user = user; - match project_id { - Some(project_id) => { - user.current_project = Some(project_id.to_string()); - } - None => { - user.current_project = None; - } - } - app.set_user(&user)?; - if let Some(win) = handle.get_window("main") { - let menu_handle = win.menu_handle(); - if let Some(project_settings) = menu_handle.try_get_item("projectsettings") { - _ = project_settings.set_enabled(project_id.is_some()); - } - } - } - // TODO: persist this not on the user object - Ok(()) -} - -#[tauri::command(async)] -#[instrument(skip(handle))] -pub async fn get_current_project(handle: tauri::AppHandle) -> Result, Error> { - let app = handle.state::(); - match app.get_user()? { - Some(user) => Ok(user.current_project), - None => Err({ - tracing::error!("failed to get user"); - Error::Unknown - }), - } -} - impl From for Error { fn from(value: controller::DeleteError) -> Self { match value { diff --git a/packages/tauri/src/users/user.rs b/packages/tauri/src/users/user.rs index f07f9b4ef..0255222a1 100644 --- a/packages/tauri/src/users/user.rs +++ b/packages/tauri/src/users/user.rs @@ -17,8 +17,6 @@ pub struct User { pub github_access_token: Option, #[serde(default)] pub github_username: Option, - #[serde(default)] - pub current_project: Option, } impl TryFrom for git::Signature<'_> { diff --git a/packages/ui/src/hooks.client.ts b/packages/ui/src/hooks.client.ts index 4644528e9..cdebbc496 100644 --- a/packages/ui/src/hooks.client.ts +++ b/packages/ui/src/hooks.client.ts @@ -2,8 +2,6 @@ import { handleErrorWithSentry, init } from '@sentry/sveltekit'; import type { NavigationEvent } from '@sveltejs/kit'; import { dev } from '$app/environment'; import { PUBLIC_SENTRY_ENVIRONMENT } from '$env/static/public'; -import { goto } from '$app/navigation'; -import { getCurrentProject } from '$lib/backend/users'; init({ enabled: !dev, @@ -30,10 +28,3 @@ window.onunhandledrejection = (event: PromiseRejectionEvent) => { console.log('Unhandled exception', event.reason); originalUnhandledHandler?.bind(window)(event); }; - -// getCurrentProject().then((projectId) => { -// // Start on the last used project -// if (projectId) { -// goto(`/${projectId}/base`); -// } -// }); diff --git a/packages/ui/src/lib/backend/users.ts b/packages/ui/src/lib/backend/users.ts index 0056498f7..1d48dfb10 100644 --- a/packages/ui/src/lib/backend/users.ts +++ b/packages/ui/src/lib/backend/users.ts @@ -9,13 +9,5 @@ export async function set(params: { user: User }) { return invoke('set_user', params); } -export async function setCurrentProject(params: { projectId: string | undefined }) { - return invoke('set_current_project', params); -} - -export async function getCurrentProject() { - return invoke('get_current_project'); -} - const del = () => invoke('delete_user'); export { del as delete }; diff --git a/packages/ui/src/lib/menu.ts b/packages/ui/src/lib/menu.ts new file mode 100644 index 000000000..0567f5f2b --- /dev/null +++ b/packages/ui/src/lib/menu.ts @@ -0,0 +1,19 @@ +import { emit } from '$lib/utils/events'; +import { invoke, listen as listenIpc } from '$lib/backend/ipc'; + +export function subscribe(projectId: string) { + invoke('menu_item_set_enabled', { + menuItemId: 'project/settings', + enabled: true + }); + const unsubscribeProjectSettings = listenIpc('menu://project/settings/clicked', () => { + emit('goto', `/${projectId}/settings/`); + }); + return () => { + unsubscribeProjectSettings(); + invoke('menu_item_set_enabled', { + menuItemId: 'project/settings', + enabled: false + }); + }; +} diff --git a/packages/ui/src/routes/+layout.svelte b/packages/ui/src/routes/+layout.svelte index e8d766cb7..697ba0d44 100644 --- a/packages/ui/src/routes/+layout.svelte +++ b/packages/ui/src/routes/+layout.svelte @@ -16,7 +16,7 @@ import { SETTINGS_CONTEXT, loadUserSettings } from '$lib/settings/userSettings'; import { initTheme } from './user/theme'; import { navigating } from '$app/stores'; - import { setCurrentProject } from '$lib/backend/users'; + import { subscribe as menuSubscribe } from '$lib/menu'; export let data: LayoutData; const { projectService, cloud, user$ } = data; @@ -31,10 +31,17 @@ $: zoom = $userSettings.zoom || 1; $: document.documentElement.style.fontSize = zoom + 'rem'; $: userSettings.update((s) => ({ ...s, zoom: zoom })); + + // listen for current project events + let unsubscribeMenu = () => {}; $: if ($navigating) { - // Keeps the backend aware of what is the current project - let projectId = $navigating?.to?.params?.projectId; - setCurrentProject({ projectId }); + const fromProject = $navigating?.from?.params?.projectId; + const toProject = $navigating?.to?.params?.projectId; + const projectHasChanged = fromProject !== toProject; + if (projectHasChanged && toProject) { + unsubscribeMenu?.(); + unsubscribeMenu = menuSubscribe(toProject); + } } onMount(() =>