mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-27 13:29:15 +07:00
✨ feat(desktop): Linux window specialization (#13059)
* ✨ feat(desktop): Linux window specialization - Add minimize/maximize/close buttons for Linux (WinControl) - Linux: no tray, close main window quits app - Linux: native window shadow and opaque background - i18n for window control tooltips Made-with: Cursor * 🌐 i18n: add window control translations for all locales Made-with: Cursor * 🐛 fix(desktop): show WinControl in SimpleTitleBar only on Linux Made-with: Cursor * 🐛 fix(desktop): limit custom titlebar controls to Linux Avoid rendering duplicate window controls on Windows and keep the Linux maximize button in sync with the current window state. Made-with: Cursor --------- Co-authored-by: LiJian <onlyyoulove3@gmail.com>
This commit is contained in:
@@ -27,7 +27,7 @@ export default class BrowserWindowsCtr extends ControllerModule {
|
|||||||
? { tab: typeof options === 'string' ? options : undefined }
|
? { tab: typeof options === 'string' ? options : undefined }
|
||||||
: options;
|
: options;
|
||||||
|
|
||||||
console.log('[BrowserWindowsCtr] Received request to open settings', normalizedOptions);
|
console.info('[BrowserWindowsCtr] Received request to open settings', normalizedOptions);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let fullPath: string;
|
let fullPath: string;
|
||||||
@@ -73,6 +73,13 @@ export default class BrowserWindowsCtr extends ControllerModule {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@IpcMethod()
|
||||||
|
isWindowMaximized() {
|
||||||
|
return this.withSenderIdentifier((identifier) => {
|
||||||
|
return this.app.browserManager.isWindowMaximized(identifier);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@IpcMethod()
|
@IpcMethod()
|
||||||
setWindowSize(params: WindowSizeParams) {
|
setWindowSize(params: WindowSizeParams) {
|
||||||
this.withSenderIdentifier((identifier) => {
|
this.withSenderIdentifier((identifier) => {
|
||||||
@@ -106,7 +113,7 @@ export default class BrowserWindowsCtr extends ControllerModule {
|
|||||||
@IpcMethod()
|
@IpcMethod()
|
||||||
async interceptRoute(params: InterceptRouteParams) {
|
async interceptRoute(params: InterceptRouteParams) {
|
||||||
const { path, source } = params;
|
const { path, source } = params;
|
||||||
console.log(
|
console.info(
|
||||||
`[BrowserWindowsCtr] Received route interception request: ${path}, source: ${source}`,
|
`[BrowserWindowsCtr] Received route interception request: ${path}, source: ${source}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -115,11 +122,11 @@ export default class BrowserWindowsCtr extends ControllerModule {
|
|||||||
|
|
||||||
// If no matching route found, return not intercepted
|
// If no matching route found, return not intercepted
|
||||||
if (!matchedRoute) {
|
if (!matchedRoute) {
|
||||||
console.log(`[BrowserWindowsCtr] No matching route configuration found: ${path}`);
|
console.info(`[BrowserWindowsCtr] No matching route configuration found: ${path}`);
|
||||||
return { intercepted: false, path, source };
|
return { intercepted: false, path, source };
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.info(
|
||||||
`[BrowserWindowsCtr] Intercepted route: ${path}, target window: ${matchedRoute.targetWindow}`,
|
`[BrowserWindowsCtr] Intercepted route: ${path}, target window: ${matchedRoute.targetWindow}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -153,7 +160,7 @@ export default class BrowserWindowsCtr extends ControllerModule {
|
|||||||
uniqueId?: string;
|
uniqueId?: string;
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
console.log('[BrowserWindowsCtr] Creating multi-instance window:', params);
|
console.info('[BrowserWindowsCtr] Creating multi-instance window:', params);
|
||||||
|
|
||||||
const result = this.app.browserManager.createMultiInstanceWindow(
|
const result = this.app.browserManager.createMultiInstanceWindow(
|
||||||
params.templateId,
|
params.templateId,
|
||||||
@@ -223,11 +230,11 @@ export default class BrowserWindowsCtr extends ControllerModule {
|
|||||||
browser.show();
|
browser.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private withSenderIdentifier(fn: (identifier: string) => void) {
|
private withSenderIdentifier<T>(fn: (identifier: string) => T): T | undefined {
|
||||||
const context = getIpcContext();
|
const context = getIpcContext();
|
||||||
if (!context) return;
|
if (!context) return undefined;
|
||||||
const identifier = this.app.browserManager.getIdentifierByWebContents(context.sender);
|
const identifier = this.app.browserManager.getIdentifierByWebContents(context.sender);
|
||||||
if (!identifier) return;
|
if (!identifier) return undefined;
|
||||||
fn(identifier);
|
return fn(identifier);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { InterceptRouteParams } from '@lobechat/electron-client-ipc';
|
import type { InterceptRouteParams } from '@lobechat/electron-client-ipc';
|
||||||
import type { Mock} from 'vitest';
|
import type { Mock } from 'vitest';
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import type { AppBrowsersIdentifiers} from '@/appBrowsers';
|
import type { AppBrowsersIdentifiers } from '@/appBrowsers';
|
||||||
import type { App } from '@/core/App';
|
import type { App } from '@/core/App';
|
||||||
import type { IpcContext } from '@/utils/ipc';
|
import type { IpcContext } from '@/utils/ipc';
|
||||||
import { runWithIpcContext } from '@/utils/ipc';
|
import { runWithIpcContext } from '@/utils/ipc';
|
||||||
@@ -28,6 +28,7 @@ const mockRedirectToPage = vi.fn();
|
|||||||
const mockCloseWindow = vi.fn();
|
const mockCloseWindow = vi.fn();
|
||||||
const mockMinimizeWindow = vi.fn();
|
const mockMinimizeWindow = vi.fn();
|
||||||
const mockMaximizeWindow = vi.fn();
|
const mockMaximizeWindow = vi.fn();
|
||||||
|
const mockIsWindowMaximized = vi.fn();
|
||||||
const mockRetrieveByIdentifier = vi.fn();
|
const mockRetrieveByIdentifier = vi.fn();
|
||||||
const testSenderIdentifierString: string = 'test-window-event-id';
|
const testSenderIdentifierString: string = 'test-window-event-id';
|
||||||
|
|
||||||
@@ -55,6 +56,7 @@ const mockApp = {
|
|||||||
closeWindow: mockCloseWindow,
|
closeWindow: mockCloseWindow,
|
||||||
minimizeWindow: mockMinimizeWindow,
|
minimizeWindow: mockMinimizeWindow,
|
||||||
maximizeWindow: mockMaximizeWindow,
|
maximizeWindow: mockMaximizeWindow,
|
||||||
|
isWindowMaximized: mockIsWindowMaximized,
|
||||||
retrieveByIdentifier: mockRetrieveByIdentifier.mockImplementation(
|
retrieveByIdentifier: mockRetrieveByIdentifier.mockImplementation(
|
||||||
(identifier: AppBrowsersIdentifiers | string) => {
|
(identifier: AppBrowsersIdentifiers | string) => {
|
||||||
if (identifier === 'some-other-window') {
|
if (identifier === 'some-other-window') {
|
||||||
@@ -135,6 +137,20 @@ describe('BrowserWindowsCtr', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('isWindowMaximized', () => {
|
||||||
|
it('should return maximized state for the sender window', () => {
|
||||||
|
mockIsWindowMaximized.mockReturnValueOnce(true);
|
||||||
|
|
||||||
|
const sender = {} as any;
|
||||||
|
const context = { sender, event: { sender } as any } as IpcContext;
|
||||||
|
const result = runWithIpcContext(context, () => browserWindowsCtr.isWindowMaximized());
|
||||||
|
|
||||||
|
expect(mockGetIdentifierByWebContents).toHaveBeenCalledWith(context.sender);
|
||||||
|
expect(mockIsWindowMaximized).toHaveBeenCalledWith(testSenderIdentifierString);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('interceptRoute', () => {
|
describe('interceptRoute', () => {
|
||||||
const baseParams = { source: 'link-click' as const };
|
const baseParams = { source: 'link-click' as const };
|
||||||
|
|
||||||
|
|||||||
@@ -250,8 +250,8 @@ export class App {
|
|||||||
this.isQuiting = false;
|
this.isQuiting = false;
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
app.on('window-all-closed', () => {
|
||||||
if (windows()) {
|
if (windows() || process.platform === 'linux') {
|
||||||
logger.info('All windows closed, quitting application (Windows)');
|
logger.info(`All windows closed, quitting application (${process.platform})`);
|
||||||
app.quit();
|
app.quit();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
import type { MainBroadcastEventKey, MainBroadcastParams } from '@lobechat/electron-client-ipc';
|
import type { MainBroadcastEventKey, MainBroadcastParams } from '@lobechat/electron-client-ipc';
|
||||||
import type { WebContents } from 'electron';
|
import type { WebContents } from 'electron';
|
||||||
|
|
||||||
|
import { isLinux } from '@/const/env';
|
||||||
import RemoteServerConfigCtr from '@/controllers/RemoteServerConfigCtr';
|
import RemoteServerConfigCtr from '@/controllers/RemoteServerConfigCtr';
|
||||||
import { createLogger } from '@/utils/logger';
|
import { createLogger } from '@/utils/logger';
|
||||||
|
|
||||||
import type {
|
import type { AppBrowsersIdentifiers, WindowTemplateIdentifiers } from '../../appBrowsers';
|
||||||
AppBrowsersIdentifiers,
|
import { appBrowsers, BrowsersIdentifiers, windowTemplates } from '../../appBrowsers';
|
||||||
WindowTemplateIdentifiers} from '../../appBrowsers';
|
|
||||||
import {
|
|
||||||
appBrowsers,
|
|
||||||
BrowsersIdentifiers,
|
|
||||||
windowTemplates,
|
|
||||||
} from '../../appBrowsers';
|
|
||||||
import type { App } from '../App';
|
import type { App } from '../App';
|
||||||
import type { BrowserWindowOpts } from './Browser';
|
import type { BrowserWindowOpts } from './Browser';
|
||||||
import Browser from './Browser';
|
import Browser from './Browser';
|
||||||
@@ -196,11 +191,15 @@ export class BrowserManager {
|
|||||||
// Dynamically determine initial path for main window
|
// Dynamically determine initial path for main window
|
||||||
if (browser.identifier === BrowsersIdentifiers.app) {
|
if (browser.identifier === BrowsersIdentifiers.app) {
|
||||||
const initialPath = isOnboardingCompleted ? '/' : '/desktop-onboarding';
|
const initialPath = isOnboardingCompleted ? '/' : '/desktop-onboarding';
|
||||||
browser = { ...browser, path: initialPath };
|
browser = {
|
||||||
|
...browser,
|
||||||
|
keepAlive: isLinux ? false : browser.keepAlive,
|
||||||
|
path: initialPath,
|
||||||
|
};
|
||||||
logger.debug(`Main window initial path: ${initialPath}`);
|
logger.debug(`Main window initial path: ${initialPath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (browser.keepAlive) {
|
if (browser.keepAlive || browser.identifier === BrowsersIdentifiers.app) {
|
||||||
this.retrieveOrInitialize(browser);
|
this.retrieveOrInitialize(browser);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -259,6 +258,11 @@ export class BrowserManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isWindowMaximized(identifier: string) {
|
||||||
|
const browser = this.browsers.get(identifier);
|
||||||
|
return browser?.browserWindow.isMaximized() ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
setWindowSize(identifier: string, size: { height?: number; width?: number }) {
|
setWindowSize(identifier: string, size: { height?: number; width?: number }) {
|
||||||
const browser = this.browsers.get(identifier);
|
const browser = this.browsers.get(identifier);
|
||||||
browser?.setWindowSize(size);
|
browser?.setWindowSize(size);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { TITLE_BAR_HEIGHT } from '@lobechat/desktop-bridge';
|
|||||||
import { type BrowserWindow, type BrowserWindowConstructorOptions, nativeTheme } from 'electron';
|
import { type BrowserWindow, type BrowserWindowConstructorOptions, nativeTheme } from 'electron';
|
||||||
|
|
||||||
import { buildDir } from '@/const/dir';
|
import { buildDir } from '@/const/dir';
|
||||||
import { isDev, isMac, isMacTahoe, isWindows } from '@/const/env';
|
import { isDev, isLinux, isMac, isMacTahoe, isWindows } from '@/const/env';
|
||||||
import { createLogger } from '@/utils/logger';
|
import { createLogger } from '@/utils/logger';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -28,9 +28,15 @@ interface WindowsThemeConfig {
|
|||||||
titleBarStyle: 'hidden';
|
titleBarStyle: 'hidden';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface LinuxThemeConfig {
|
||||||
|
backgroundColor: string;
|
||||||
|
hasShadow: true;
|
||||||
|
}
|
||||||
|
|
||||||
// Lazy-load liquid glass only on macOS Tahoe to avoid import errors on other platforms.
|
// Lazy-load liquid glass only on macOS Tahoe to avoid import errors on other platforms.
|
||||||
// Dynamic require is intentional: native .node addons cannot be loaded via
|
// Dynamic require is intentional: native .node addons cannot be loaded via
|
||||||
// async import() and must be synchronously required at module init time.
|
// async import() and must be synchronously required at module init time.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-imports -- dynamic require, type from module
|
||||||
let liquidGlass: typeof import('electron-liquid-glass').default | undefined;
|
let liquidGlass: typeof import('electron-liquid-glass').default | undefined;
|
||||||
if (isMacTahoe) {
|
if (isMacTahoe) {
|
||||||
try {
|
try {
|
||||||
@@ -139,6 +145,9 @@ export class WindowThemeManager {
|
|||||||
visualEffectState: 'active',
|
visualEffectState: 'active',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (isLinux) {
|
||||||
|
return this.getLinuxConfig();
|
||||||
|
}
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,6 +163,13 @@ export class WindowThemeManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getLinuxConfig(): LinuxThemeConfig {
|
||||||
|
return {
|
||||||
|
backgroundColor: this.resolveIsDarkMode() ? BACKGROUND_DARK : BACKGROUND_LIGHT,
|
||||||
|
hasShadow: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== Theme Listener ====================
|
// ==================== Theme Listener ====================
|
||||||
|
|
||||||
private setupThemeListener(): void {
|
private setupThemeListener(): void {
|
||||||
@@ -206,6 +222,8 @@ export class WindowThemeManager {
|
|||||||
try {
|
try {
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
this.applyWindowsVisualEffects(isDarkMode);
|
this.applyWindowsVisualEffects(isDarkMode);
|
||||||
|
} else if (isLinux) {
|
||||||
|
this.applyLinuxVisualEffects();
|
||||||
} else if (isMac) {
|
} else if (isMac) {
|
||||||
this.applyMacVisualEffects();
|
this.applyMacVisualEffects();
|
||||||
}
|
}
|
||||||
@@ -230,6 +248,18 @@ export class WindowThemeManager {
|
|||||||
this.browserWindow.setTitleBarOverlay(config.titleBarOverlay);
|
this.browserWindow.setTitleBarOverlay(config.titleBarOverlay);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private applyLinuxVisualEffects(): void {
|
||||||
|
if (!this.browserWindow) return;
|
||||||
|
|
||||||
|
const config = this.getLinuxConfig();
|
||||||
|
const browserWindow = this.browserWindow as BrowserWindow & {
|
||||||
|
setHasShadow?: (hasShadow: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
browserWindow.setBackgroundColor(config.backgroundColor);
|
||||||
|
browserWindow.setHasShadow?.(true);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply macOS visual effects.
|
* Apply macOS visual effects.
|
||||||
* - Tahoe+: liquid glass auto-adapts to dark mode; ensure it's applied if not yet.
|
* - Tahoe+: liquid glass auto-adapts to dark mode; ensure it's applied if not yet.
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ vi.mock('@/const/dir', () => ({
|
|||||||
|
|
||||||
vi.mock('@/const/env', () => ({
|
vi.mock('@/const/env', () => ({
|
||||||
isDev: false,
|
isDev: false,
|
||||||
|
isLinux: false,
|
||||||
isMac: false,
|
isMac: false,
|
||||||
isMacTahoe: false,
|
isMacTahoe: false,
|
||||||
isWindows: true,
|
isWindows: true,
|
||||||
@@ -240,7 +241,7 @@ describe('Browser', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Create new browser to trigger initialization with saved state
|
// Create new browser to trigger initialization with saved state
|
||||||
const newBrowser = new Browser(defaultOptions, mockApp);
|
const _newBrowser = new Browser(defaultOptions, mockApp);
|
||||||
|
|
||||||
expect(MockBrowserWindow).toHaveBeenCalledWith(
|
expect(MockBrowserWindow).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@@ -328,7 +329,7 @@ describe('Browser', () => {
|
|||||||
mockNativeTheme.shouldUseDarkColors = true;
|
mockNativeTheme.shouldUseDarkColors = true;
|
||||||
|
|
||||||
// Create browser with dark mode
|
// Create browser with dark mode
|
||||||
const darkBrowser = new Browser(defaultOptions, mockApp);
|
const _darkBrowser = new Browser(defaultOptions, mockApp);
|
||||||
|
|
||||||
expect(MockBrowserWindow).toHaveBeenCalledWith(
|
expect(MockBrowserWindow).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@@ -603,7 +604,7 @@ describe('Browser', () => {
|
|||||||
...defaultOptions,
|
...defaultOptions,
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
};
|
};
|
||||||
const keepAliveBrowser = new Browser(keepAliveOptions, mockApp);
|
const _keepAliveBrowser = new Browser(keepAliveOptions, mockApp);
|
||||||
|
|
||||||
// Get the new close handler
|
// Get the new close handler
|
||||||
const keepAliveCloseHandler = mockBrowserWindow.on.mock.calls.findLast(
|
const keepAliveCloseHandler = mockBrowserWindow.on.mock.calls.findLast(
|
||||||
|
|||||||
@@ -88,6 +88,10 @@ vi.mock('@/controllers/RemoteServerConfigCtr', () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/const/env', () => ({
|
||||||
|
isLinux: false,
|
||||||
|
}));
|
||||||
|
|
||||||
describe('BrowserManager', () => {
|
describe('BrowserManager', () => {
|
||||||
let manager: BrowserManager;
|
let manager: BrowserManager;
|
||||||
let mockApp: AppCore;
|
let mockApp: AppCore;
|
||||||
@@ -394,6 +398,24 @@ describe('BrowserManager', () => {
|
|||||||
expect(browser?.browserWindow.maximize).not.toHaveBeenCalled();
|
expect(browser?.browserWindow.maximize).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('isWindowMaximized', () => {
|
||||||
|
it('should return false when window is not maximized', () => {
|
||||||
|
manager.retrieveByIdentifier('app');
|
||||||
|
const browser = manager.browsers.get('app');
|
||||||
|
browser!.browserWindow.isMaximized = vi.fn().mockReturnValue(false);
|
||||||
|
|
||||||
|
expect(manager.isWindowMaximized('app')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when window is maximized', () => {
|
||||||
|
manager.retrieveByIdentifier('app');
|
||||||
|
const browser = manager.browsers.get('app');
|
||||||
|
browser!.browserWindow.isMaximized = vi.fn().mockReturnValue(true);
|
||||||
|
|
||||||
|
expect(manager.isWindowMaximized('app')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getIdentifierByWebContents', () => {
|
describe('getIdentifierByWebContents', () => {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ vi.mock('@/const/dir', () => ({
|
|||||||
|
|
||||||
vi.mock('@/const/env', () => ({
|
vi.mock('@/const/env', () => ({
|
||||||
isDev: false,
|
isDev: false,
|
||||||
|
isLinux: false,
|
||||||
isMac: false,
|
isMac: false,
|
||||||
isMacTahoe: false,
|
isMacTahoe: false,
|
||||||
isWindows: true,
|
isWindows: true,
|
||||||
|
|||||||
@@ -120,5 +120,8 @@
|
|||||||
"waitingOAuth.errorTitle": "فشل الاتصال بالتفويض",
|
"waitingOAuth.errorTitle": "فشل الاتصال بالتفويض",
|
||||||
"waitingOAuth.helpText": "إذا لم يفتح المتصفح تلقائياً، يرجى النقر على إلغاء والمحاولة مرة أخرى",
|
"waitingOAuth.helpText": "إذا لم يفتح المتصفح تلقائياً، يرجى النقر على إلغاء والمحاولة مرة أخرى",
|
||||||
"waitingOAuth.retry": "إعادة المحاولة",
|
"waitingOAuth.retry": "إعادة المحاولة",
|
||||||
"waitingOAuth.title": "في انتظار الاتصال بالتفويض"
|
"waitingOAuth.title": "في انتظار الاتصال بالتفويض",
|
||||||
|
"window.close": "إغلاق النافذة",
|
||||||
|
"window.maximize": "تكبير النافذة",
|
||||||
|
"window.minimize": "تصغير النافذة"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,5 +120,8 @@
|
|||||||
"waitingOAuth.errorTitle": "Неуспешна връзка за удостоверяване",
|
"waitingOAuth.errorTitle": "Неуспешна връзка за удостоверяване",
|
||||||
"waitingOAuth.helpText": "Ако браузърът не се е отворил автоматично, моля натиснете отказ и опитайте отново",
|
"waitingOAuth.helpText": "Ако браузърът не се е отворил автоматично, моля натиснете отказ и опитайте отново",
|
||||||
"waitingOAuth.retry": "Опитай отново",
|
"waitingOAuth.retry": "Опитай отново",
|
||||||
"waitingOAuth.title": "Изчакване на връзка за удостоверяване"
|
"waitingOAuth.title": "Изчакване на връзка за удостоверяване",
|
||||||
|
"window.close": "Затвори прозореца",
|
||||||
|
"window.maximize": "Максимизирай прозореца",
|
||||||
|
"window.minimize": "Минимизирай прозореца"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,5 +120,8 @@
|
|||||||
"waitingOAuth.errorTitle": "Autorisierungsverbindung fehlgeschlagen",
|
"waitingOAuth.errorTitle": "Autorisierungsverbindung fehlgeschlagen",
|
||||||
"waitingOAuth.helpText": "Falls sich der Browser nicht automatisch geöffnet hat, klicken Sie bitte auf Abbrechen und versuchen Sie es erneut",
|
"waitingOAuth.helpText": "Falls sich der Browser nicht automatisch geöffnet hat, klicken Sie bitte auf Abbrechen und versuchen Sie es erneut",
|
||||||
"waitingOAuth.retry": "Erneut versuchen",
|
"waitingOAuth.retry": "Erneut versuchen",
|
||||||
"waitingOAuth.title": "Warten auf Autorisierungsverbindung"
|
"waitingOAuth.title": "Warten auf Autorisierungsverbindung",
|
||||||
|
"window.close": "Fenster schließen",
|
||||||
|
"window.maximize": "Fenster maximieren",
|
||||||
|
"window.minimize": "Fenster minimieren"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,5 +120,8 @@
|
|||||||
"waitingOAuth.errorTitle": "Authorization Connection Failed",
|
"waitingOAuth.errorTitle": "Authorization Connection Failed",
|
||||||
"waitingOAuth.helpText": "If the browser did not open automatically, please click cancel and try again",
|
"waitingOAuth.helpText": "If the browser did not open automatically, please click cancel and try again",
|
||||||
"waitingOAuth.retry": "Retry",
|
"waitingOAuth.retry": "Retry",
|
||||||
"waitingOAuth.title": "Waiting for Authorization Connection"
|
"waitingOAuth.title": "Waiting for Authorization Connection",
|
||||||
|
"window.close": "Close window",
|
||||||
|
"window.maximize": "Maximize window",
|
||||||
|
"window.minimize": "Minimize window"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,5 +120,8 @@
|
|||||||
"waitingOAuth.errorTitle": "Fallo en la conexión de autorización",
|
"waitingOAuth.errorTitle": "Fallo en la conexión de autorización",
|
||||||
"waitingOAuth.helpText": "Si el navegador no se abrió automáticamente, por favor haz clic en cancelar e inténtalo de nuevo",
|
"waitingOAuth.helpText": "Si el navegador no se abrió automáticamente, por favor haz clic en cancelar e inténtalo de nuevo",
|
||||||
"waitingOAuth.retry": "Reintentar",
|
"waitingOAuth.retry": "Reintentar",
|
||||||
"waitingOAuth.title": "Esperando conexión de autorización"
|
"waitingOAuth.title": "Esperando conexión de autorización",
|
||||||
|
"window.close": "Cerrar ventana",
|
||||||
|
"window.maximize": "Maximizar ventana",
|
||||||
|
"window.minimize": "Minimizar ventana"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,5 +120,8 @@
|
|||||||
"waitingOAuth.errorTitle": "اتصال احراز هویت ناموفق بود",
|
"waitingOAuth.errorTitle": "اتصال احراز هویت ناموفق بود",
|
||||||
"waitingOAuth.helpText": "اگر مرورگر بهطور خودکار باز نشد، لطفاً لغو را بزنید و دوباره تلاش کنید",
|
"waitingOAuth.helpText": "اگر مرورگر بهطور خودکار باز نشد، لطفاً لغو را بزنید و دوباره تلاش کنید",
|
||||||
"waitingOAuth.retry": "تلاش مجدد",
|
"waitingOAuth.retry": "تلاش مجدد",
|
||||||
"waitingOAuth.title": "در انتظار اتصال احراز هویت"
|
"waitingOAuth.title": "در انتظار اتصال احراز هویت",
|
||||||
|
"window.close": "بستن پنجره",
|
||||||
|
"window.maximize": "حداکثر کردن پنجره",
|
||||||
|
"window.minimize": "حداقل کردن پنجره"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,5 +120,8 @@
|
|||||||
"waitingOAuth.errorTitle": "Échec de la connexion d'autorisation",
|
"waitingOAuth.errorTitle": "Échec de la connexion d'autorisation",
|
||||||
"waitingOAuth.helpText": "Si le navigateur ne s'est pas ouvert automatiquement, veuillez cliquer sur annuler et réessayer",
|
"waitingOAuth.helpText": "Si le navigateur ne s'est pas ouvert automatiquement, veuillez cliquer sur annuler et réessayer",
|
||||||
"waitingOAuth.retry": "Réessayer",
|
"waitingOAuth.retry": "Réessayer",
|
||||||
"waitingOAuth.title": "En attente de la connexion d'autorisation"
|
"waitingOAuth.title": "En attente de la connexion d'autorisation",
|
||||||
|
"window.close": "Fermer la fenêtre",
|
||||||
|
"window.maximize": "Maximiser la fenêtre",
|
||||||
|
"window.minimize": "Minimiser la fenêtre"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,5 +120,8 @@
|
|||||||
"waitingOAuth.errorTitle": "Connessione di autorizzazione fallita",
|
"waitingOAuth.errorTitle": "Connessione di autorizzazione fallita",
|
||||||
"waitingOAuth.helpText": "Se il browser non si è aperto automaticamente, fai clic su annulla e riprova",
|
"waitingOAuth.helpText": "Se il browser non si è aperto automaticamente, fai clic su annulla e riprova",
|
||||||
"waitingOAuth.retry": "Riprova",
|
"waitingOAuth.retry": "Riprova",
|
||||||
"waitingOAuth.title": "In attesa della connessione di autorizzazione"
|
"waitingOAuth.title": "In attesa della connessione di autorizzazione",
|
||||||
|
"window.close": "Chiudi finestra",
|
||||||
|
"window.maximize": "Massimizza finestra",
|
||||||
|
"window.minimize": "Minimizza finestra"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,5 +120,8 @@
|
|||||||
"waitingOAuth.errorTitle": "認証接続に失敗しました",
|
"waitingOAuth.errorTitle": "認証接続に失敗しました",
|
||||||
"waitingOAuth.helpText": "ブラウザが自動的に開かない場合は、キャンセルをクリックして再試行してください",
|
"waitingOAuth.helpText": "ブラウザが自動的に開かない場合は、キャンセルをクリックして再試行してください",
|
||||||
"waitingOAuth.retry": "再試行",
|
"waitingOAuth.retry": "再試行",
|
||||||
"waitingOAuth.title": "認証接続を待機中"
|
"waitingOAuth.title": "認証接続を待機中",
|
||||||
|
"window.close": "ウィンドウを閉じる",
|
||||||
|
"window.maximize": "ウィンドウを最大化",
|
||||||
|
"window.minimize": "ウィンドウを最小化"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,5 +120,8 @@
|
|||||||
"waitingOAuth.errorTitle": "권한 연결 실패",
|
"waitingOAuth.errorTitle": "권한 연결 실패",
|
||||||
"waitingOAuth.helpText": "브라우저가 자동으로 열리지 않으면, 취소를 클릭한 후 다시 시도하세요.",
|
"waitingOAuth.helpText": "브라우저가 자동으로 열리지 않으면, 취소를 클릭한 후 다시 시도하세요.",
|
||||||
"waitingOAuth.retry": "다시 시도",
|
"waitingOAuth.retry": "다시 시도",
|
||||||
"waitingOAuth.title": "인증 연결 대기 중"
|
"waitingOAuth.title": "인증 연결 대기 중",
|
||||||
|
"window.close": "창 닫기",
|
||||||
|
"window.maximize": "창 최대화",
|
||||||
|
"window.minimize": "창 최소화"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,5 +120,8 @@
|
|||||||
"waitingOAuth.errorTitle": "Autorisatieverbinding mislukt",
|
"waitingOAuth.errorTitle": "Autorisatieverbinding mislukt",
|
||||||
"waitingOAuth.helpText": "Als de browser niet automatisch is geopend, klik dan op annuleren en probeer het opnieuw",
|
"waitingOAuth.helpText": "Als de browser niet automatisch is geopend, klik dan op annuleren en probeer het opnieuw",
|
||||||
"waitingOAuth.retry": "Opnieuw proberen",
|
"waitingOAuth.retry": "Opnieuw proberen",
|
||||||
"waitingOAuth.title": "Wachten op autorisatieverbinding"
|
"waitingOAuth.title": "Wachten op autorisatieverbinding",
|
||||||
|
"window.close": "Venster sluiten",
|
||||||
|
"window.maximize": "Venster maximaliseren",
|
||||||
|
"window.minimize": "Venster minimaliseren"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,5 +120,8 @@
|
|||||||
"waitingOAuth.errorTitle": "Nie udało się nawiązać połączenia autoryzacyjnego",
|
"waitingOAuth.errorTitle": "Nie udało się nawiązać połączenia autoryzacyjnego",
|
||||||
"waitingOAuth.helpText": "Jeśli przeglądarka nie otworzyła się automatycznie, kliknij Anuluj i spróbuj ponownie",
|
"waitingOAuth.helpText": "Jeśli przeglądarka nie otworzyła się automatycznie, kliknij Anuluj i spróbuj ponownie",
|
||||||
"waitingOAuth.retry": "Spróbuj ponownie",
|
"waitingOAuth.retry": "Spróbuj ponownie",
|
||||||
"waitingOAuth.title": "Oczekiwanie na połączenie autoryzacyjne"
|
"waitingOAuth.title": "Oczekiwanie na połączenie autoryzacyjne",
|
||||||
|
"window.close": "Zamknij okno",
|
||||||
|
"window.maximize": "Maksymalizuj okno",
|
||||||
|
"window.minimize": "Minimalizuj okno"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,5 +120,8 @@
|
|||||||
"waitingOAuth.errorTitle": "Falha na conexão de autorização",
|
"waitingOAuth.errorTitle": "Falha na conexão de autorização",
|
||||||
"waitingOAuth.helpText": "Se o navegador não abriu automaticamente, clique em cancelar e tente novamente",
|
"waitingOAuth.helpText": "Se o navegador não abriu automaticamente, clique em cancelar e tente novamente",
|
||||||
"waitingOAuth.retry": "Tentar novamente",
|
"waitingOAuth.retry": "Tentar novamente",
|
||||||
"waitingOAuth.title": "Aguardando conexão de autorização"
|
"waitingOAuth.title": "Aguardando conexão de autorização",
|
||||||
|
"window.close": "Fechar janela",
|
||||||
|
"window.maximize": "Maximizar janela",
|
||||||
|
"window.minimize": "Minimizar janela"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,5 +120,8 @@
|
|||||||
"waitingOAuth.errorTitle": "Не удалось подключиться к авторизации",
|
"waitingOAuth.errorTitle": "Не удалось подключиться к авторизации",
|
||||||
"waitingOAuth.helpText": "Если браузер не открылся автоматически, нажмите «Отмена» и попробуйте снова",
|
"waitingOAuth.helpText": "Если браузер не открылся автоматически, нажмите «Отмена» и попробуйте снова",
|
||||||
"waitingOAuth.retry": "Повторить",
|
"waitingOAuth.retry": "Повторить",
|
||||||
"waitingOAuth.title": "Ожидание подключения к авторизации"
|
"waitingOAuth.title": "Ожидание подключения к авторизации",
|
||||||
|
"window.close": "Закрыть окно",
|
||||||
|
"window.maximize": "Развернуть окно",
|
||||||
|
"window.minimize": "Свернуть окно"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,5 +120,8 @@
|
|||||||
"waitingOAuth.errorTitle": "Yetkilendirme Bağlantısı Başarısız",
|
"waitingOAuth.errorTitle": "Yetkilendirme Bağlantısı Başarısız",
|
||||||
"waitingOAuth.helpText": "Tarayıcı otomatik olarak açılmadıysa, lütfen iptal edip tekrar deneyin",
|
"waitingOAuth.helpText": "Tarayıcı otomatik olarak açılmadıysa, lütfen iptal edip tekrar deneyin",
|
||||||
"waitingOAuth.retry": "Tekrar Dene",
|
"waitingOAuth.retry": "Tekrar Dene",
|
||||||
"waitingOAuth.title": "Yetkilendirme Bağlantısı Bekleniyor"
|
"waitingOAuth.title": "Yetkilendirme Bağlantısı Bekleniyor",
|
||||||
|
"window.close": "Pencereyi kapat",
|
||||||
|
"window.maximize": "Pencereyi büyüt",
|
||||||
|
"window.minimize": "Pencereyi küçült"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,5 +120,8 @@
|
|||||||
"waitingOAuth.errorTitle": "Kết nối xác thực thất bại",
|
"waitingOAuth.errorTitle": "Kết nối xác thực thất bại",
|
||||||
"waitingOAuth.helpText": "Nếu trình duyệt không tự động mở, vui lòng nhấn hủy và thử lại",
|
"waitingOAuth.helpText": "Nếu trình duyệt không tự động mở, vui lòng nhấn hủy và thử lại",
|
||||||
"waitingOAuth.retry": "Thử lại",
|
"waitingOAuth.retry": "Thử lại",
|
||||||
"waitingOAuth.title": "Đang chờ kết nối xác thực"
|
"waitingOAuth.title": "Đang chờ kết nối xác thực",
|
||||||
|
"window.close": "Đóng cửa sổ",
|
||||||
|
"window.maximize": "Phóng to cửa sổ",
|
||||||
|
"window.minimize": "Thu nhỏ cửa sổ"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,5 +120,8 @@
|
|||||||
"waitingOAuth.errorTitle": "授权连接失败",
|
"waitingOAuth.errorTitle": "授权连接失败",
|
||||||
"waitingOAuth.helpText": "如果浏览器没有自动打开,请点击取消后重新尝试",
|
"waitingOAuth.helpText": "如果浏览器没有自动打开,请点击取消后重新尝试",
|
||||||
"waitingOAuth.retry": "重试",
|
"waitingOAuth.retry": "重试",
|
||||||
"waitingOAuth.title": "等待授权连接"
|
"waitingOAuth.title": "等待授权连接",
|
||||||
|
"window.close": "关闭窗口",
|
||||||
|
"window.maximize": "最大化窗口",
|
||||||
|
"window.minimize": "最小化窗口"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,5 +120,8 @@
|
|||||||
"waitingOAuth.errorTitle": "授權連接失敗",
|
"waitingOAuth.errorTitle": "授權連接失敗",
|
||||||
"waitingOAuth.helpText": "如果瀏覽器沒有自動打開,請點擊取消後重新嘗試",
|
"waitingOAuth.helpText": "如果瀏覽器沒有自動打開,請點擊取消後重新嘗試",
|
||||||
"waitingOAuth.retry": "重試",
|
"waitingOAuth.retry": "重試",
|
||||||
"waitingOAuth.title": "等待授權連接"
|
"waitingOAuth.title": "等待授權連接",
|
||||||
|
"window.close": "關閉視窗",
|
||||||
|
"window.maximize": "最大化視窗",
|
||||||
|
"window.minimize": "最小化視窗"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,13 @@ import { type FC } from 'react';
|
|||||||
|
|
||||||
import { ProductLogo } from '@/components/Branding/ProductLogo';
|
import { ProductLogo } from '@/components/Branding/ProductLogo';
|
||||||
import { electronStylish } from '@/styles/electron';
|
import { electronStylish } from '@/styles/electron';
|
||||||
|
import { getPlatform, isMacOS } from '@/utils/platform';
|
||||||
|
|
||||||
import { useWatchThemeUpdate } from '../system/useWatchThemeUpdate';
|
import { useWatchThemeUpdate } from '../system/useWatchThemeUpdate';
|
||||||
|
import WinControl, { WINDOW_CONTROL_WIDTH } from './WinControl';
|
||||||
|
|
||||||
|
const isMac = isMacOS();
|
||||||
|
const isLinux = getPlatform() === 'Linux';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple, minimal TitleBar for Electron windows.
|
* A simple, minimal TitleBar for Electron windows.
|
||||||
@@ -16,16 +21,22 @@ import { useWatchThemeUpdate } from '../system/useWatchThemeUpdate';
|
|||||||
*/
|
*/
|
||||||
const SimpleTitleBar: FC = () => {
|
const SimpleTitleBar: FC = () => {
|
||||||
useWatchThemeUpdate();
|
useWatchThemeUpdate();
|
||||||
|
|
||||||
|
const showWinControl = isLinux && !isMac;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flexbox
|
<Flexbox
|
||||||
horizontal
|
horizontal
|
||||||
align={'center'}
|
align={'center'}
|
||||||
className={electronStylish.draggable}
|
className={electronStylish.draggable}
|
||||||
height={TITLE_BAR_HEIGHT}
|
height={TITLE_BAR_HEIGHT}
|
||||||
justify={'center'}
|
justify={showWinControl ? 'space-between' : 'center'}
|
||||||
|
style={{ minHeight: TITLE_BAR_HEIGHT, padding: '0 12px' }}
|
||||||
width={'100%'}
|
width={'100%'}
|
||||||
>
|
>
|
||||||
|
{showWinControl && <div style={{ width: WINDOW_CONTROL_WIDTH }} />}
|
||||||
<ProductLogo size={16} type={'text'} />
|
<ProductLogo size={16} type={'text'} />
|
||||||
|
{showWinControl && <WinControl />}
|
||||||
</Flexbox>
|
</Flexbox>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ import { Flexbox } from '@lobehub/ui';
|
|||||||
import { Divider } from 'antd';
|
import { Divider } from 'antd';
|
||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
|
|
||||||
import { useElectronStore } from '@/store/electron';
|
|
||||||
import { electronStylish } from '@/styles/electron';
|
import { electronStylish } from '@/styles/electron';
|
||||||
import { isMacOS } from '@/utils/platform';
|
import { getPlatform, isMacOS } from '@/utils/platform';
|
||||||
|
|
||||||
import Connection from '../connection/Connection';
|
import Connection from '../connection/Connection';
|
||||||
import { useTabNavigation } from '../navigation/useTabNavigation';
|
import { useTabNavigation } from '../navigation/useTabNavigation';
|
||||||
@@ -16,18 +15,13 @@ import TabBar from './TabBar';
|
|||||||
import WinControl from './WinControl';
|
import WinControl from './WinControl';
|
||||||
|
|
||||||
const isMac = isMacOS();
|
const isMac = isMacOS();
|
||||||
|
const isLinux = getPlatform() === 'Linux';
|
||||||
|
|
||||||
const TitleBar = memo(() => {
|
const TitleBar = memo(() => {
|
||||||
const [isAppStateInit, initElectronAppState] = useElectronStore((s) => [
|
|
||||||
s.isAppStateInit,
|
|
||||||
s.useInitElectronAppState,
|
|
||||||
]);
|
|
||||||
|
|
||||||
initElectronAppState();
|
|
||||||
useWatchThemeUpdate();
|
useWatchThemeUpdate();
|
||||||
useTabNavigation();
|
useTabNavigation();
|
||||||
|
|
||||||
const showWinControl = isAppStateInit && !isMac;
|
const showWinControl = isLinux && !isMac;
|
||||||
|
|
||||||
const padding = useMemo(() => {
|
const padding = useMemo(() => {
|
||||||
if (showWinControl) {
|
if (showWinControl) {
|
||||||
@@ -35,7 +29,7 @@ const TitleBar = memo(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return '0 12px';
|
return '0 12px';
|
||||||
}, [showWinControl, isMac]);
|
}, [showWinControl]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flexbox
|
<Flexbox
|
||||||
|
|||||||
@@ -1,5 +1,108 @@
|
|||||||
const WinControl = () => {
|
'use client';
|
||||||
return <div style={{ width: 132 }} />;
|
|
||||||
};
|
import { ActionIcon, Flexbox } from '@lobehub/ui';
|
||||||
|
import { createStaticStyles, cx } from 'antd-style';
|
||||||
|
import { Maximize2Icon, Minimize2Icon, MinusIcon, XIcon } from 'lucide-react';
|
||||||
|
import { memo, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { electronSystemService } from '@/services/electron/system';
|
||||||
|
import { electronStylish } from '@/styles/electron';
|
||||||
|
|
||||||
|
export const WINDOW_CONTROL_WIDTH = 112;
|
||||||
|
|
||||||
|
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||||
|
closeButton: css`
|
||||||
|
border-radius: 8px;
|
||||||
|
color: ${cssVar.colorTextSecondary};
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: ${cssVar.colorBgBase};
|
||||||
|
background: ${cssVar.colorError};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
container: css`
|
||||||
|
width: ${WINDOW_CONTROL_WIDTH}px;
|
||||||
|
min-width: ${WINDOW_CONTROL_WIDTH}px;
|
||||||
|
`,
|
||||||
|
controlButton: css`
|
||||||
|
border-radius: 8px;
|
||||||
|
color: ${cssVar.colorTextSecondary};
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: ${cssVar.colorText};
|
||||||
|
background: ${cssVar.colorFillTertiary};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const WinControl = memo(() => {
|
||||||
|
const { t } = useTranslation('electron');
|
||||||
|
const [isMaximized, setIsMaximized] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let mounted = true;
|
||||||
|
|
||||||
|
const syncWindowState = async () => {
|
||||||
|
const nextState = await electronSystemService.isWindowMaximized();
|
||||||
|
if (mounted) setIsMaximized(nextState);
|
||||||
|
};
|
||||||
|
|
||||||
|
void syncWindowState();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mounted = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const controls = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
icon: MinusIcon,
|
||||||
|
key: 'minimize',
|
||||||
|
label: t('window.minimize'),
|
||||||
|
onClick: () => void electronSystemService.minimizeWindow(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: isMaximized ? Minimize2Icon : Maximize2Icon,
|
||||||
|
key: 'maximize',
|
||||||
|
label: t(isMaximized ? 'window.restore' : 'window.maximize'),
|
||||||
|
onClick: async () => {
|
||||||
|
await electronSystemService.maximizeWindow();
|
||||||
|
setIsMaximized(await electronSystemService.isWindowMaximized());
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: XIcon,
|
||||||
|
key: 'close',
|
||||||
|
label: t('window.close'),
|
||||||
|
onClick: () => void electronSystemService.closeWindow(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[isMaximized, t],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flexbox
|
||||||
|
horizontal
|
||||||
|
align={'center'}
|
||||||
|
className={cx(styles.container, electronStylish.nodrag)}
|
||||||
|
gap={4}
|
||||||
|
justify={'flex-end'}
|
||||||
|
>
|
||||||
|
{controls.map((control) => (
|
||||||
|
<ActionIcon
|
||||||
|
className={control.key === 'close' ? styles.closeButton : styles.controlButton}
|
||||||
|
icon={control.icon}
|
||||||
|
key={control.key}
|
||||||
|
size={{ blockSize: 28, size: 14 }}
|
||||||
|
title={control.label}
|
||||||
|
tooltipProps={{ placement: 'bottom' }}
|
||||||
|
onClick={control.onClick}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Flexbox>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export default WinControl;
|
export default WinControl;
|
||||||
|
|||||||
@@ -127,4 +127,8 @@ export default {
|
|||||||
'If the browser did not open automatically, please click cancel and try again',
|
'If the browser did not open automatically, please click cancel and try again',
|
||||||
'waitingOAuth.retry': 'Retry',
|
'waitingOAuth.retry': 'Retry',
|
||||||
'waitingOAuth.title': 'Waiting for Authorization Connection',
|
'waitingOAuth.title': 'Waiting for Authorization Connection',
|
||||||
|
'window.close': 'Close window',
|
||||||
|
'window.maximize': 'Maximize window',
|
||||||
|
'window.minimize': 'Minimize window',
|
||||||
|
'window.restore': 'Restore window',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ class ElectronSystemService {
|
|||||||
return this.ipc.windows.maximizeWindow();
|
return this.ipc.windows.maximizeWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async isWindowMaximized(): Promise<boolean> {
|
||||||
|
return this.ipc.windows.isWindowMaximized();
|
||||||
|
}
|
||||||
|
|
||||||
async minimizeWindow(): Promise<void> {
|
async minimizeWindow(): Promise<void> {
|
||||||
return this.ipc.windows.minimizeWindow();
|
return this.ipc.windows.minimizeWindow();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user