diff --git a/apps/desktop/src/components/WindowsTitleBar.svelte b/apps/desktop/src/components/WindowsTitleBar.svelte new file mode 100644 index 0000000000..00d3cd6b60 --- /dev/null +++ b/apps/desktop/src/components/WindowsTitleBar.svelte @@ -0,0 +1,654 @@ + + +{#snippet editorBadgeSnippet()} + {#if editorDisplayName} + + {editorDisplayName} + + {/if} +{/snippet} + +{#if showTitleBar} +
+
+ {#if appIcon} + + {/if} + {#if getBadgeStyle()} +
+ + {getBadgeText()} + +
+ {/if} +
+ + +
+ + toggleDropdown('file')} + > + File + {#snippet contextMenuSlot()} + + { + menuActions.addLocalRepository(); + closeDropdown('file'); + }} + /> + { + menuActions.cloneRepository(); + closeDropdown('file'); + }} + /> + + + { + goto(newSettingsPath()); + closeDropdown('file'); + }} + /> + { + try { + await tauri.checkUpdate(); + } catch (error) { + console.error('Failed to check for updates:', error); + } + closeDropdown('file'); + }} + /> + + {/snippet} + + + + toggleDropdown('view')} + > + View + {#snippet contextMenuSlot()} + + { + menuActions.switchTheme(); + closeDropdown('view'); + }} + /> + + + { + menuActions.zoomIn(); + closeDropdown('view'); + }} + /> + { + menuActions.zoomOut(); + closeDropdown('view'); + }} + /> + { + menuActions.resetZoom(); + closeDropdown('view'); + }} + /> + + {#if import.meta.env.DEV} + + { + menuActions.openDevTools(); + closeDropdown('view'); + }} + /> + { + location.reload(); + closeDropdown('view'); + }} + /> + + {/if} + {/snippet} + + + + toggleDropdown('project')} + > + Project + {#snippet contextMenuSlot()} + + { + menuActions.openProjectHistory(); + closeDropdown('project'); + }} + /> + { + menuActions.openInEditor(); + closeDropdown('project'); + }} + control={editorBadgeSnippet} + /> + + + { + if (project) goto(projectSettingsPath(project.id)); + closeDropdown('project'); + }} + /> + + {/snippet} + + + + toggleDropdown('help')} + > + Help + {#snippet contextMenuSlot()} + + { + openExternalUrl('https://docs.gitbutler.com'); + closeDropdown('help'); + }} + /> + { + openExternalUrl('https://github.com/gitbutlerapp/gitbutler'); + closeDropdown('help'); + }} + /> + { + openExternalUrl('https://github.com/gitbutlerapp/gitbutler/releases'); + closeDropdown('help'); + }} + /> + + + { + menuActions.openKeyboardShortcuts(); + closeDropdown('help'); + }} + /> + + + { + menuActions.shareDebugInfo(); + closeDropdown('help'); + }} + /> + { + openExternalUrl('https://github.com/gitbutlerapp/gitbutler/issues/new/choose'); + closeDropdown('help'); + }} + /> + + + { + openExternalUrl('https://discord.com/invite/MmFkmaJ42D'); + closeDropdown('help'); + }} + /> + { + openExternalUrl('https://www.youtube.com/@gitbutlerapp'); + closeDropdown('help'); + }} + /> + { + openExternalUrl('https://x.com/gitbutler'); + closeDropdown('help'); + }} + /> + + + {}} /> + + {/snippet} + +
+ + +
+ + +
+ + + +
+
+{/if} + + diff --git a/apps/desktop/src/components/v3/projectSettings/AppearanceSettings.svelte b/apps/desktop/src/components/v3/projectSettings/AppearanceSettings.svelte index 8f5c093fb7..546e3d7bcc 100644 --- a/apps/desktop/src/components/v3/projectSettings/AppearanceSettings.svelte +++ b/apps/desktop/src/components/v3/projectSettings/AppearanceSettings.svelte @@ -47,6 +47,7 @@ {/snippet} +
{#snippet title()} diff --git a/apps/desktop/src/lib/backend/tauri.ts b/apps/desktop/src/lib/backend/tauri.ts index df857c60c2..43660c23d0 100644 --- a/apps/desktop/src/lib/backend/tauri.ts +++ b/apps/desktop/src/lib/backend/tauri.ts @@ -1,5 +1,6 @@ import { invoke as invokeIpc, listen as listenIpc } from '$lib/backend/ipc'; import { getVersion } from '@tauri-apps/api/app'; +import { getCurrentWindow } from '@tauri-apps/api/window'; import { check } from '@tauri-apps/plugin-updater'; export class Tauri { @@ -7,4 +8,27 @@ export class Tauri { listen = listenIpc; checkUpdate = check; currentVersion = getVersion; + + private window = getCurrentWindow(); + + async minimize() { + await this.window.minimize(); + } + + async toggleMaximize() { + const isMaximized = await this.window.isMaximized(); + if (isMaximized) { + await this.window.unmaximize(); + } else { + await this.window.maximize(); + } + } + + async close() { + await this.window.close(); + } + + async setDecorations(decorations: boolean) { + await this.window.setDecorations(decorations); + } } diff --git a/apps/desktop/src/routes/+layout.svelte b/apps/desktop/src/routes/+layout.svelte index cca93ea038..de0e1b382a 100644 --- a/apps/desktop/src/routes/+layout.svelte +++ b/apps/desktop/src/routes/+layout.svelte @@ -15,12 +15,14 @@ import ShareIssueModal from '$components/ShareIssueModal.svelte'; import SwitchThemeMenuAction from '$components/SwitchThemeMenuAction.svelte'; import ToastController from '$components/ToastController.svelte'; + import WindowsTitleBar from '$components/WindowsTitleBar.svelte'; import ZoomInOutMenuAction from '$components/ZoomInOutMenuAction.svelte'; import { ActionService } from '$lib/actions/actionService.svelte'; import { PromptService as AIPromptService } from '$lib/ai/promptService'; import { AIService } from '$lib/ai/service'; import { PostHogWrapper } from '$lib/analytics/posthog'; import { CommandService, invoke } from '$lib/backend/ipc'; + import { Tauri } from '$lib/backend/tauri'; import BaseBranchService from '$lib/baseBranch/baseBranchService.svelte'; import { BranchService } from '$lib/branches/branchService.svelte'; import { @@ -99,6 +101,7 @@ const gitLabClient = new GitLabClient(); setContext(GitHubClient, gitHubClient); setContext(GitLabClient, gitLabClient); + setContext(Tauri, data.tauri); const user = data.userService.user; const accessToken = $derived($user?.github_access_token); $effect(() => gitHubClient.setToken(accessToken)); @@ -264,6 +267,13 @@ let shareIssueModal: ShareIssueModal; onMount(() => { + // Initialize window decorations for Windows + // Always use custom title bar on Windows, so hide default decorations + if (platformName === 'windows') { + // Decorations are inverted: true = show default title bar, false = hide default title bar + data.tauri.setDecorations(false); + } + return unsubscribe( events.on('goto', async (path: string) => await goto(path)), events.on('openSendIssueModal', () => shareIssueModal?.show()) @@ -302,10 +312,18 @@ onkeydown={handleKeyDown} /> -
!dev && e.preventDefault()}> +
!dev && e.preventDefault()} +> {#if platformName === 'macos' && !$settingsStore?.featureFlags.v3}
{/if} + + + {@render children()}
diff --git a/apps/desktop/src/routes/[projectId]/+layout.svelte b/apps/desktop/src/routes/[projectId]/+layout.svelte index f82ed37c3a..e5edacba4e 100644 --- a/apps/desktop/src/routes/[projectId]/+layout.svelte +++ b/apps/desktop/src/routes/[projectId]/+layout.svelte @@ -126,11 +126,22 @@ setContext(StackingReorderDropzoneManagerFactory, stackingReorderDropzoneManagerFactory); }); + const focusManager = new FocusManager(); + setContext(FocusManager, focusManager); + + // Set the Project context immediately so child components can access it + // Even if project is undefined, we set the context to prevent "no instance" errors + setContext(Project, data.project); + + // Debug logging to help diagnose the issue + if (!data.project) { + console.warn('Project is undefined in [projectId] layout, projectId:', data.projectId); + } + $effect.pre(() => { setContext(HistoryService, data.historyService); setContext(TemplateService, data.templateService); setContext(BaseBranch, baseBranch); - setContext(Project, project); setContext(GitBranchService, data.gitBranchService); setContext(UncommitedFilesWatcher, data.uncommitedFileWatcher); setContext(ProjectService, data.projectService); @@ -141,9 +152,6 @@ setContext(StackPublishingService, data.stackPublishingService); }); - const focusManager = new FocusManager(); - setContext(FocusManager, focusManager); - let intervalId: any; const forgeFactory = getContext(DefaultForgeFactory); diff --git a/apps/desktop/static/icons/128x128.png b/apps/desktop/static/icons/128x128.png new file mode 100644 index 0000000000..2a6955a943 Binary files /dev/null and b/apps/desktop/static/icons/128x128.png differ diff --git a/apps/desktop/static/icons/dev/128x128.png b/apps/desktop/static/icons/dev/128x128.png new file mode 100644 index 0000000000..958d0c65d7 Binary files /dev/null and b/apps/desktop/static/icons/dev/128x128.png differ diff --git a/apps/desktop/static/icons/nightly/128x128.png b/apps/desktop/static/icons/nightly/128x128.png new file mode 100644 index 0000000000..266e75898f Binary files /dev/null and b/apps/desktop/static/icons/nightly/128x128.png differ diff --git a/crates/gitbutler-tauri/capabilities/main.json b/crates/gitbutler-tauri/capabilities/main.json index 6a633d04b6..49bde9102c 100644 --- a/crates/gitbutler-tauri/capabilities/main.json +++ b/crates/gitbutler-tauri/capabilities/main.json @@ -7,6 +7,12 @@ "permissions": [ "core:default", "core:window:allow-start-dragging", + "core:window:allow-close", + "core:window:allow-minimize", + "core:window:allow-maximize", + "core:window:allow-unmaximize", + "core:window:allow-is-maximized", + "core:window:allow-set-decorations", "core:window:default", "dialog:allow-open", "fs:allow-read-file", diff --git a/crates/gitbutler-tauri/src/window.rs b/crates/gitbutler-tauri/src/window.rs index af2a9783d8..72c77d1d54 100644 --- a/crates/gitbutler-tauri/src/window.rs +++ b/crates/gitbutler-tauri/src/window.rs @@ -255,6 +255,22 @@ pub fn create( window_relative_url: String, ) -> tauri::Result { tracing::info!("creating window '{label}' created at '{window_relative_url}'"); + + #[cfg(target_os = "windows")] + let window = tauri::WebviewWindowBuilder::new( + handle, + label, + tauri::WebviewUrl::App(window_relative_url.into()), + ) + .resizable(true) + .title(handle.package_info().name.clone()) + .disable_drag_drop_handler() + .min_inner_size(1000.0, 600.0) + .inner_size(1160.0, 720.0) + .decorations(true) // Start with decorations enabled, frontend will disable if user has custom title bar enabled + .build()?; + + #[cfg(not(any(target_os = "windows", target_os = "macos")))] let window = tauri::WebviewWindowBuilder::new( handle, label, @@ -265,6 +281,7 @@ pub fn create( .disable_drag_drop_handler() .min_inner_size(1000.0, 600.0) .inner_size(1160.0, 720.0) + .decorations(true) // Start with decorations enabled, frontend will disable if user has custom title bar enabled .build()?; Ok(window) } diff --git a/packages/ui/src/styles/core/variables.css b/packages/ui/src/styles/core/variables.css index 59607f346f..5fec265dfe 100644 --- a/packages/ui/src/styles/core/variables.css +++ b/packages/ui/src/styles/core/variables.css @@ -20,4 +20,7 @@ --lexical-input-client-padding: 12px; --lexical-input-client-toolbar-height: 48px; --lexical-input-font-size: 13px; + + /* OS related */ + --windows-title-bar-height: 30px; } diff --git a/turbo.json b/turbo.json index f5ee383f13..799ae363e2 100644 --- a/turbo.json +++ b/turbo.json @@ -20,7 +20,8 @@ "dev": { "dependsOn": ["package"], "cache": false, - "persistent": true + "persistent": true, + "interruptible": true }, "check": { "dependsOn": ["package"]