mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 05:14:54 +00:00
feat: implement tray and minimize behaviors (#13851)
This PR introduces new window behaviors, which can be enabled when the menubar setting is active: New Features: - Quick open from tray icon - Minimize to tray - Exit to tray - Start minimized These changes have not yet been tested on macOS. <img width="645" height="479" alt="image" src="https://github.com/user-attachments/assets/7bdd13d0-5322-45a4-8e71-85c081aa0c86" /> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Configurable menubar/tray behaviors: open on left-click, minimize to tray, close to tray (exit to tray), and start minimized. * **UI** * Appearance settings add a Menubar → Window Behavior group with four toggles; group shows only when menubar/tray is enabled (hidden on macOS). * **Settings** * Tray settings persisted and exposed via the settings API with getters and setters for each option. * **Localization** * Added translation keys and English strings for the new controls and descriptions. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Peng Xiao <pengxiao@outlook.com>
This commit is contained in:
@@ -58,6 +58,10 @@ export type SpellCheckStateSchema = z.infer<typeof SpellCheckStateSchema>;
|
||||
export const MenubarStateKey = 'menubarState' as const;
|
||||
export const MenubarStateSchema = z.object({
|
||||
enabled: z.boolean().default(true),
|
||||
openOnLeftClick: z.boolean().default(false),
|
||||
minimizeToTray: z.boolean().default(false),
|
||||
closeToTray: z.boolean().default(false),
|
||||
startMinimized: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export type MenubarStateSchema = z.infer<typeof MenubarStateSchema>;
|
||||
|
||||
@@ -317,7 +317,14 @@ class TrayState implements Disposable {
|
||||
logger.debug('User clicked on tray icon');
|
||||
this.update();
|
||||
if (!isMacOS()) {
|
||||
this.tray?.popUpContextMenu();
|
||||
if (
|
||||
TraySettingsState.value.enabled &&
|
||||
TraySettingsState.value.openOnLeftClick
|
||||
) {
|
||||
showMainWindow();
|
||||
} else {
|
||||
this.tray?.popUpContextMenu();
|
||||
}
|
||||
}
|
||||
updateApplicationsPing$.next(Date.now());
|
||||
};
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { app, clipboard, nativeImage, nativeTheme } from 'electron';
|
||||
import { getLinkPreview } from 'link-preview-js';
|
||||
import { map, shareReplay } from 'rxjs';
|
||||
|
||||
import { isMacOS } from '../../shared/utils';
|
||||
import { persistentConfig } from '../config-storage/persist';
|
||||
import { logger } from '../logger';
|
||||
import { openExternalSafely } from '../security/open-external';
|
||||
import type { WorkbenchViewMeta } from '../shared-state-schema';
|
||||
import { MenubarStateKey, MenubarStateSchema } from '../shared-state-schema';
|
||||
import { globalStateStorage } from '../shared-storage/storage';
|
||||
import type { NamespaceHandlers } from '../type';
|
||||
import {
|
||||
activateView,
|
||||
@@ -34,6 +37,19 @@ import { getOrCreateCustomThemeWindow } from '../windows-manager/custom-theme-wi
|
||||
import { getChallengeResponse } from './challenge';
|
||||
import { uiSubjects } from './subject';
|
||||
|
||||
const TraySettingsState = {
|
||||
$: globalStateStorage.watch<MenubarStateSchema>(MenubarStateKey).pipe(
|
||||
map(v => MenubarStateSchema.parse(v ?? {})),
|
||||
shareReplay(1)
|
||||
),
|
||||
|
||||
get value() {
|
||||
return MenubarStateSchema.parse(
|
||||
globalStateStorage.get(MenubarStateKey) ?? {}
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const uiHandlers = {
|
||||
isMaximized: async () => {
|
||||
const window = await getMainWindow();
|
||||
@@ -48,7 +64,14 @@ export const uiHandlers = {
|
||||
},
|
||||
handleMinimizeApp: async () => {
|
||||
const window = await getMainWindow();
|
||||
window?.minimize();
|
||||
if (
|
||||
TraySettingsState.value.enabled &&
|
||||
TraySettingsState.value.minimizeToTray
|
||||
) {
|
||||
window?.hide();
|
||||
} else {
|
||||
window?.minimize();
|
||||
}
|
||||
},
|
||||
handleMaximizeApp: async () => {
|
||||
const window = await getMainWindow();
|
||||
@@ -69,7 +92,15 @@ export const uiHandlers = {
|
||||
await handleWebContentsResize(e.sender);
|
||||
},
|
||||
handleCloseApp: async () => {
|
||||
app.quit();
|
||||
if (
|
||||
TraySettingsState.value.enabled &&
|
||||
TraySettingsState.value.closeToTray
|
||||
) {
|
||||
const window = await getMainWindow();
|
||||
window?.hide();
|
||||
} else {
|
||||
app.quit();
|
||||
}
|
||||
},
|
||||
handleHideApp: async () => {
|
||||
const window = await getMainWindow();
|
||||
|
||||
@@ -2,7 +2,7 @@ import { join } from 'node:path';
|
||||
|
||||
import { BrowserWindow, nativeTheme } from 'electron';
|
||||
import electronWindowState from 'electron-window-state';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { BehaviorSubject, map, shareReplay } from 'rxjs';
|
||||
|
||||
import { isLinux, isMacOS, isWindows, resourcesPath } from '../../shared/utils';
|
||||
import { beforeAppQuit } from '../cleanup';
|
||||
@@ -10,11 +10,26 @@ import { buildType } from '../config';
|
||||
import { mainWindowOrigin } from '../constants';
|
||||
import { ensureHelperProcess } from '../helper-process';
|
||||
import { logger } from '../logger';
|
||||
import { MenubarStateKey, MenubarStateSchema } from '../shared-state-schema';
|
||||
import { globalStateStorage } from '../shared-storage/storage';
|
||||
import { uiSubjects } from '../ui/subject';
|
||||
|
||||
const IS_DEV: boolean =
|
||||
process.env.NODE_ENV === 'development' && !process.env.CI;
|
||||
|
||||
const TraySettingsState = {
|
||||
$: globalStateStorage.watch<MenubarStateSchema>(MenubarStateKey).pipe(
|
||||
map(v => MenubarStateSchema.parse(v ?? {})),
|
||||
shareReplay(1)
|
||||
),
|
||||
|
||||
get value() {
|
||||
return MenubarStateSchema.parse(
|
||||
globalStateStorage.get(MenubarStateKey) ?? {}
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
function closeAllWindows() {
|
||||
BrowserWindow.getAllWindows().forEach(w => {
|
||||
if (!w.isDestroyed()) {
|
||||
@@ -125,9 +140,16 @@ export class MainWindowManager {
|
||||
// TODO(@pengx17): gracefully close the app, for example, ask user to save unsaved changes
|
||||
e.preventDefault();
|
||||
if (!isMacOS()) {
|
||||
closeAllWindows();
|
||||
this.mainWindowReady = undefined;
|
||||
this.mainWindow$.next(undefined);
|
||||
if (
|
||||
TraySettingsState.value.enabled &&
|
||||
TraySettingsState.value.closeToTray
|
||||
) {
|
||||
mainWindow.hide();
|
||||
} else {
|
||||
closeAllWindows();
|
||||
this.mainWindowReady = undefined;
|
||||
this.mainWindow$.next(undefined);
|
||||
}
|
||||
} else {
|
||||
// hide window on macOS
|
||||
// application quit will be handled by closing the hidden window
|
||||
@@ -209,7 +231,10 @@ export class MainWindowManager {
|
||||
if (IS_DEV) {
|
||||
// do not gain focus in dev mode
|
||||
mainWindow.showInactive();
|
||||
} else {
|
||||
} else if (
|
||||
!TraySettingsState.value.enabled ||
|
||||
!TraySettingsState.value.startMinimized
|
||||
) {
|
||||
mainWindow.show();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user