From 6ed280e0cc8146259d3b1ff54ebc93a5ce4ca4f9 Mon Sep 17 00:00:00 2001 From: Innei Date: Mon, 19 Jan 2026 21:27:30 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20improve=20desktop=20onboard?= =?UTF-8?q?ing=20window=20management=20and=20footer=20actions=20(#11619)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ feat: improve desktop onboarding window management and footer actions - Add APP_WINDOW_MIN_SIZE constant for consistent window constraints - Extract reusable OnboardingFooterActions component for step navigation - Implement setWindowMinimumSize API in electron system service - Apply dedicated minimum size (1200x900) during onboarding flow - Restore app-level defaults (860x500) when onboarding completes - Add windowMinimumSize parameter support in BrowserManager Resolves: LOBE-3643, LOBE-3225, LOBE-2588 * chore: update .gitignore to include pnpm-lock.yaml and remove pnpm-lock.yaml file Signed-off-by: Innei --------- Signed-off-by: Innei --- .gitignore | 2 + apps/desktop/src/main/appBrowsers.ts | 5 +- .../src/main/controllers/BrowserWindowsCtr.ts | 18 ++++- apps/desktop/src/main/core/browser/Browser.ts | 18 +++-- .../src/main/core/browser/BrowserManager.ts | 9 ++- packages/desktop-bridge/src/index.ts | 5 ++ .../electron-client-ipc/src/types/window.ts | 5 +- .../desktop-onboarding/_layout/index.tsx | 9 ++- .../components/OnboardingFooterActions.tsx | 38 ++++++++++ .../features/DataModeStep.tsx | 33 +++++---- .../desktop-onboarding/features/LoginStep.tsx | 72 ++++++++++++++----- .../features/PermissionsStep.tsx | 33 +++++---- .../(desktop)/desktop-onboarding/index.tsx | 15 ++-- src/services/electron/system.ts | 10 +-- 14 files changed, 198 insertions(+), 74 deletions(-) create mode 100644 src/app/[variants]/(desktop)/desktop-onboarding/components/OnboardingFooterActions.tsx diff --git a/.gitignore b/.gitignore index 6497d86cc0..847230601e 100644 --- a/.gitignore +++ b/.gitignore @@ -117,3 +117,5 @@ e2e/reports out i18n-unused-keys-report.json .vitest-reports + +pnpm-lock.yaml \ No newline at end of file diff --git a/apps/desktop/src/main/appBrowsers.ts b/apps/desktop/src/main/appBrowsers.ts index 89f3a0f3e8..290e705b5a 100644 --- a/apps/desktop/src/main/appBrowsers.ts +++ b/apps/desktop/src/main/appBrowsers.ts @@ -1,3 +1,5 @@ +import { APP_WINDOW_MIN_SIZE } from '@lobechat/desktop-bridge'; + import type { BrowserWindowOpts } from './core/browser/Browser'; export const BrowsersIdentifiers = { @@ -11,7 +13,8 @@ export const appBrowsers = { height: 800, identifier: 'app', keepAlive: true, - minWidth: 400, + minHeight: APP_WINDOW_MIN_SIZE.height, + minWidth: APP_WINDOW_MIN_SIZE.width, path: '/', showOnInit: true, titleBarStyle: 'hidden', diff --git a/apps/desktop/src/main/controllers/BrowserWindowsCtr.ts b/apps/desktop/src/main/controllers/BrowserWindowsCtr.ts index ed9128f05e..04aa557532 100644 --- a/apps/desktop/src/main/controllers/BrowserWindowsCtr.ts +++ b/apps/desktop/src/main/controllers/BrowserWindowsCtr.ts @@ -1,7 +1,7 @@ import type { InterceptRouteParams, OpenSettingsWindowOptions, - WindowResizableParams, + WindowMinimumSizeParams, WindowSizeParams, } from '@lobechat/electron-client-ipc'; import { findMatchingRoute } from '~common/routes'; @@ -81,9 +81,21 @@ export default class BrowserWindowsCtr extends ControllerModule { } @IpcMethod() - setWindowResizable(params: WindowResizableParams) { + setWindowMinimumSize(params: WindowMinimumSizeParams) { this.withSenderIdentifier((identifier) => { - this.app.browserManager.setWindowResizable(identifier, params.resizable); + const currentSize = this.app.browserManager.getWindowSize(identifier); + const nextWindowSize = { + ...currentSize, + }; + if (params.height) { + nextWindowSize.height = Math.max(currentSize.height, params.height); + } + if (params.width) { + nextWindowSize.width = Math.max(currentSize.width, params.width); + } + + this.app.browserManager.setWindowSize(identifier, nextWindowSize); + this.app.browserManager.setWindowMinimumSize(identifier, params); }); } diff --git a/apps/desktop/src/main/core/browser/Browser.ts b/apps/desktop/src/main/core/browser/Browser.ts index 7fe8bc6365..a8aaff616f 100644 --- a/apps/desktop/src/main/core/browser/Browser.ts +++ b/apps/desktop/src/main/core/browser/Browser.ts @@ -1,4 +1,4 @@ -import { TITLE_BAR_HEIGHT } from '@lobechat/desktop-bridge'; +import { APP_WINDOW_MIN_SIZE, TITLE_BAR_HEIGHT } from '@lobechat/desktop-bridge'; import { MainBroadcastEventKey, MainBroadcastParams } from '@lobechat/electron-client-ipc'; import { BrowserWindow, @@ -291,9 +291,19 @@ export default class Browser { }); } - setWindowResizable(resizable: boolean): void { - logger.debug(`[${this.identifier}] Setting window resizable: ${resizable}`); - this._browserWindow?.setResizable(resizable); + setWindowMinimumSize(size: { height?: number; width?: number }): void { + logger.debug(`[${this.identifier}] Setting window minimum size: ${JSON.stringify(size)}`); + + const currentMinimumSize = this._browserWindow?.getMinimumSize?.() ?? [0, 0]; + const rawWidth = size.width ?? currentMinimumSize[0]; + const rawHeight = size.height ?? currentMinimumSize[1]; + + // Electron doesn't "reset" minimum size with 0x0 reliably. + // Treat 0 / negative as fallback to app-level default preset. + const width = rawWidth > 0 ? rawWidth : APP_WINDOW_MIN_SIZE.width; + const height = rawHeight > 0 ? rawHeight : APP_WINDOW_MIN_SIZE.height; + + this._browserWindow?.setMinimumSize?.(width, height); } // ==================== Window Position ==================== diff --git a/apps/desktop/src/main/core/browser/BrowserManager.ts b/apps/desktop/src/main/core/browser/BrowserManager.ts index 5f3d9b0fa6..2c30d13822 100644 --- a/apps/desktop/src/main/core/browser/BrowserManager.ts +++ b/apps/desktop/src/main/core/browser/BrowserManager.ts @@ -250,9 +250,14 @@ export class BrowserManager { browser?.setWindowSize(size); } - setWindowResizable(identifier: string, resizable: boolean) { + getWindowSize(identifier: string) { const browser = this.browsers.get(identifier); - browser?.setWindowResizable(resizable); + return browser?.browserWindow.getBounds(); + } + + setWindowMinimumSize(identifier: string, size: { height?: number; width?: number }) { + const browser = this.browsers.get(identifier); + browser?.setWindowMinimumSize(size); } getIdentifierByWebContents(webContents: WebContents): string | null { diff --git a/packages/desktop-bridge/src/index.ts b/packages/desktop-bridge/src/index.ts index 5050e59e74..6b22de6378 100644 --- a/packages/desktop-bridge/src/index.ts +++ b/packages/desktop-bridge/src/index.ts @@ -10,3 +10,8 @@ export { // Desktop window constants export const TITLE_BAR_HEIGHT = 38; + +export const APP_WINDOW_MIN_SIZE = { + height: 600, + width: 1000, +} as const; diff --git a/packages/electron-client-ipc/src/types/window.ts b/packages/electron-client-ipc/src/types/window.ts index 9aae6bfcb6..0a7b263127 100644 --- a/packages/electron-client-ipc/src/types/window.ts +++ b/packages/electron-client-ipc/src/types/window.ts @@ -3,6 +3,7 @@ export interface WindowSizeParams { width?: number; } -export interface WindowResizableParams { - resizable: boolean; +export interface WindowMinimumSizeParams { + height?: number; + width?: number; } diff --git a/src/app/[variants]/(desktop)/desktop-onboarding/_layout/index.tsx b/src/app/[variants]/(desktop)/desktop-onboarding/_layout/index.tsx index 523c14726a..25d2ecee16 100644 --- a/src/app/[variants]/(desktop)/desktop-onboarding/_layout/index.tsx +++ b/src/app/[variants]/(desktop)/desktop-onboarding/_layout/index.tsx @@ -3,7 +3,7 @@ import { TITLE_BAR_HEIGHT } from '@lobechat/desktop-bridge'; import { Center, Flexbox, Text } from '@lobehub/ui'; import { Divider } from 'antd'; -import { cx } from 'antd-style'; +import { css, cx } from 'antd-style'; import type { FC, PropsWithChildren } from 'react'; import SimpleTitleBar from '@/features/Electron/titlebar/SimpleTitleBar'; @@ -13,6 +13,9 @@ import { useIsDark } from '@/hooks/useIsDark'; import { styles } from './style'; +const contentContainer = css` + overflow: auto; +`; const OnboardingContainer: FC = ({ children }) => { const isDarkMode = useIsDark(); return ( @@ -44,9 +47,9 @@ const OnboardingContainer: FC = ({ children }) => { -
+ {children} -
+
© 2025 LobeHub. All rights reserved. diff --git a/src/app/[variants]/(desktop)/desktop-onboarding/components/OnboardingFooterActions.tsx b/src/app/[variants]/(desktop)/desktop-onboarding/components/OnboardingFooterActions.tsx new file mode 100644 index 0000000000..1a920971d4 --- /dev/null +++ b/src/app/[variants]/(desktop)/desktop-onboarding/components/OnboardingFooterActions.tsx @@ -0,0 +1,38 @@ +import { Flexbox, type FlexboxProps } from '@lobehub/ui'; +import { cssVar } from 'antd-style'; +import { type ReactNode, memo } from 'react'; + +interface OnboardingFooterActionsProps extends Omit { + left?: ReactNode; + right?: ReactNode; +} + +const OnboardingFooterActions = memo( + ({ left, right, style, ...rest }) => { + return ( + +
{left}
+
{right}
+
+ ); + }, +); + +OnboardingFooterActions.displayName = 'OnboardingFooterActions'; + +export default OnboardingFooterActions; diff --git a/src/app/[variants]/(desktop)/desktop-onboarding/features/DataModeStep.tsx b/src/app/[variants]/(desktop)/desktop-onboarding/features/DataModeStep.tsx index 54c3fb6c39..5ddbe24fcd 100644 --- a/src/app/[variants]/(desktop)/desktop-onboarding/features/DataModeStep.tsx +++ b/src/app/[variants]/(desktop)/desktop-onboarding/features/DataModeStep.tsx @@ -10,6 +10,7 @@ import { useUserStore } from '@/store/user'; import { userGeneralSettingsSelectors } from '@/store/user/selectors'; import LobeMessage from '../components/LobeMessage'; +import OnboardingFooterActions from '../components/OnboardingFooterActions'; type DataMode = 'share' | 'privacy'; @@ -48,7 +49,7 @@ const DataModeStep = memo(({ onBack, onNext }) => { ); return ( - + {t('screen4.description')} @@ -113,19 +114,23 @@ const DataModeStep = memo(({ onBack, onNext }) => { {t('screen4.footerNote')} - - - - + + {t('back')} + + } + right={ + + } + /> ); }); diff --git a/src/app/[variants]/(desktop)/desktop-onboarding/features/LoginStep.tsx b/src/app/[variants]/(desktop)/desktop-onboarding/features/LoginStep.tsx index d6c664e8b8..0481b9e18d 100644 --- a/src/app/[variants]/(desktop)/desktop-onboarding/features/LoginStep.tsx +++ b/src/app/[variants]/(desktop)/desktop-onboarding/features/LoginStep.tsx @@ -1,6 +1,10 @@ 'use client'; -import { AuthorizationProgress, useWatchBroadcast } from '@lobechat/electron-client-ipc'; +import { + AuthorizationPhase, + AuthorizationProgress, + useWatchBroadcast, +} from '@lobechat/electron-client-ipc'; import { Alert, Button, Center, Flexbox, Icon, Input, Text } from '@lobehub/ui'; import { Divider } from 'antd'; import { cssVar } from 'antd-style'; @@ -9,6 +13,7 @@ import { memo, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { isDesktop } from '@/const/version'; +import UserInfo from '@/features/User/UserInfo'; import { remoteServerService } from '@/services/electron/remoteServer'; import { useElectronStore } from '@/store/electron'; import { setDesktopAutoOidcFirstOpenHandled } from '@/utils/electron/autoOidc'; @@ -21,6 +26,13 @@ type LoginMethod = 'cloud' | 'selfhost'; // 登录状态类型 type LoginStatus = 'idle' | 'loading' | 'success' | 'error'; +const authorizationPhaseI18nKeyMap: Record = { + browser_opened: 'screen5.auth.phase.browserOpened', + cancelled: 'screen5.actions.cancel', + verifying: 'screen5.auth.phase.verifying', + waiting_for_auth: 'screen5.auth.phase.waitingForAuth', +}; + const loginMethodMetas = { cloud: { descriptionKey: 'screen5.methods.cloud.description', @@ -181,6 +193,7 @@ const LoginStep = memo(({ onBack, onNext }) => { setAuthProgress(progress); if (progress.phase === 'cancelled') { setCloudLoginStatus('idle'); + setSelfhostLoginStatus('idle'); setAuthProgress(null); } }); @@ -188,6 +201,7 @@ const LoginStep = memo(({ onBack, onNext }) => { const handleCancelAuth = async () => { await remoteServerService.cancelAuthorization(); setCloudLoginStatus('idle'); + setSelfhostLoginStatus('idle'); setAuthProgress(null); }; @@ -195,13 +209,19 @@ const LoginStep = memo(({ onBack, onNext }) => { const renderCloudContent = () => { if (cloudLoginStatus === 'success') { return ( - + + - {authProgress && ( - + + {phaseText} + + + {remainingSeconds !== null ? ( {t('screen5.auth.remaining', { - time: Math.round((authProgress.maxPollTime - authProgress.elapsed) / 1000), + time: remainingSeconds, })} - - - )} + ) : ( +
+ )} + + ); } @@ -283,13 +311,19 @@ const LoginStep = memo(({ onBack, onNext }) => { const renderSelfhostContent = () => { if (selfhostLoginStatus === 'success') { return ( - + + )} - +
); }); diff --git a/src/app/[variants]/(desktop)/desktop-onboarding/features/PermissionsStep.tsx b/src/app/[variants]/(desktop)/desktop-onboarding/features/PermissionsStep.tsx index fa054fcb08..b75e3be444 100644 --- a/src/app/[variants]/(desktop)/desktop-onboarding/features/PermissionsStep.tsx +++ b/src/app/[variants]/(desktop)/desktop-onboarding/features/PermissionsStep.tsx @@ -18,6 +18,7 @@ import { useTranslation } from 'react-i18next'; import { ensureElectronIpc } from '@/utils/electron/ipc'; import LobeMessage from '../components/LobeMessage'; +import OnboardingFooterActions from '../components/OnboardingFooterActions'; type PermissionMeta = { descriptionKey: string; @@ -154,7 +155,7 @@ const PermissionsStep = memo(({ onBack, onNext }) => { }; return ( - + {t('screen3.description')} @@ -207,19 +208,23 @@ const PermissionsStep = memo(({ onBack, onNext }) => { ))} - - - - + + {t('back')} + + } + right={ + + } + /> ); }); diff --git a/src/app/[variants]/(desktop)/desktop-onboarding/index.tsx b/src/app/[variants]/(desktop)/desktop-onboarding/index.tsx index 6797abf7ff..1e98099e59 100644 --- a/src/app/[variants]/(desktop)/desktop-onboarding/index.tsx +++ b/src/app/[variants]/(desktop)/desktop-onboarding/index.tsx @@ -1,5 +1,6 @@ 'use client'; +import { APP_WINDOW_MIN_SIZE } from '@lobechat/desktop-bridge'; import { Flexbox, Skeleton } from '@lobehub/ui'; import { Suspense, memo, useCallback, useEffect, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; @@ -50,12 +51,11 @@ const DesktopOnboardingPage = memo(() => { // 设置窗口大小和可调整性 useEffect(() => { - const fixedSize = { height: 900, width: 1400 }; + const minimumSize = { height: 900, width: 1200 }; const applyWindowSettings = async () => { try { - await electronSystemService.setWindowSize(fixedSize); - await electronSystemService.setWindowResizable({ resizable: false }); + await electronSystemService.setWindowMinimumSize(minimumSize); } catch (error) { console.error('[DesktopOnboarding] Failed to apply window settings:', error); } @@ -64,7 +64,8 @@ const DesktopOnboardingPage = memo(() => { applyWindowSettings(); return () => { - electronSystemService.setWindowResizable({ resizable: true }).catch((error) => { + // Restore to app-level default minimum size preset + electronSystemService.setWindowMinimumSize(APP_WINDOW_MIN_SIZE).catch((error) => { console.error('[DesktopOnboarding] Failed to restore window settings:', error); }); }; @@ -127,9 +128,9 @@ const DesktopOnboardingPage = memo(() => { // 如果是第4步(LoginStep),完成 onboarding setDesktopOnboardingCompleted(); clearDesktopOnboardingStep(); // Clear persisted step since onboarding is complete - // Restore window resizable before hard reload (cleanup won't run due to hard navigation) + // Restore window minimum size before hard reload (cleanup won't run due to hard navigation) electronSystemService - .setWindowResizable({ resizable: true }) + .setWindowMinimumSize(APP_WINDOW_MIN_SIZE) .catch(console.error) .finally(() => { // Use hard reload instead of SPA navigation to ensure the app boots with the new desktop state. @@ -196,7 +197,7 @@ const DesktopOnboardingPage = memo(() => { return ( - + diff --git a/src/services/electron/system.ts b/src/services/electron/system.ts index a4ce5b65ed..ad2be14f63 100644 --- a/src/services/electron/system.ts +++ b/src/services/electron/system.ts @@ -1,6 +1,6 @@ import type { ElectronAppState, - WindowResizableParams, + WindowMinimumSizeParams, WindowSizeParams, } from '@lobechat/electron-client-ipc'; @@ -36,14 +36,14 @@ class ElectronSystemService { return this.ipc.windows.minimizeWindow(); } - async setWindowResizable(params: WindowResizableParams): Promise { - return this.ipc.windows.setWindowResizable(params); - } - async setWindowSize(params: WindowSizeParams): Promise { return this.ipc.windows.setWindowSize(params); } + async setWindowMinimumSize(params: WindowMinimumSizeParams): Promise { + return this.ipc.windows.setWindowMinimumSize(params); + } + async openExternalLink(url: string): Promise { return this.ipc.system.openExternalLink(url); }