From c5625de6f17e50ca48ee0fbc97ec8d8a3ea9898a Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Mon, 1 Dec 2025 15:21:15 +0100 Subject: [PATCH] feat(customWindowTitleBar): add sexy custom window title bar <3 --- electron/ipc-handlers/app-control.ts | 11 ++++++- electron/ipc-handlers/exec.ts | 3 +- electron/main-window.ts | 30 ++++++++++++++++--- .../simple-store.const.ts | 6 ++++ electron/start-app.ts | 6 ++-- src/app/app.constants.ts | 2 +- .../magic-side-nav.component.scss | 4 +++ .../main-header/main-header.component.scss | 8 +++++ src/app/core/theme/global-theme.service.ts | 9 ++++++ .../form-cfgs/misc-settings-form.const.ts | 12 ++++++++ .../features/config/global-config.model.ts | 1 + .../focus-mode-overlay.component.html | 2 +- .../focus-mode-overlay.component.scss | 10 +++++-- src/app/t.const.ts | 2 ++ src/assets/i18n/en.json | 2 ++ src/styles/_css-variables.scss | 1 + src/styles/page.scss | 4 +++ 17 files changed, 99 insertions(+), 14 deletions(-) create mode 100644 electron/shared-with-frontend/simple-store.const.ts diff --git a/electron/ipc-handlers/app-control.ts b/electron/ipc-handlers/app-control.ts index 7c5c70221..2c4dc0d23 100644 --- a/electron/ipc-handlers/app-control.ts +++ b/electron/ipc-handlers/app-control.ts @@ -11,6 +11,8 @@ import { import { lockscreen } from '../lockscreen'; import { errorHandlerWithFrontendInform } from '../error-handler-with-frontend-inform'; import { GlobalConfigState } from '../../src/app/features/config/global-config.model'; +import { saveSimpleStore } from '../simple-store'; +import { SimpleStoreKey } from '../shared-with-frontend/simple-store.const'; export const initAppControlIpc = (): void => { ipcMain.on(IPC.SHUTDOWN_NOW, quitApp); @@ -19,10 +21,17 @@ export const initAppControlIpc = (): void => { ipcMain.on(IPC.OPEN_DEV_TOOLS, () => getWin().webContents.openDevTools()); ipcMain.on(IPC.RELOAD_MAIN_WIN, () => getWin().reload()); - ipcMain.on(IPC.TRANSFER_SETTINGS_TO_ELECTRON, (ev, cfg: GlobalConfigState) => { + ipcMain.on(IPC.TRANSFER_SETTINGS_TO_ELECTRON, async (ev, cfg: GlobalConfigState) => { setIsMinimizeToTray(cfg.misc.isMinimizeToTray); setIsTrayShowCurrentTask(cfg.misc.isTrayShowCurrentTask); setIsTrayShowCurrentCountdown(cfg.misc.isTrayShowCurrentCountdown); + + if (cfg.misc.isUseCustomWindowTitleBar !== undefined) { + await saveSimpleStore( + SimpleStoreKey.IS_USE_CUSTOM_WINDOW_TITLE_BAR, + cfg.misc.isUseCustomWindowTitleBar, + ); + } }); ipcMain.on(IPC.SHOW_OR_FOCUS, () => { diff --git a/electron/ipc-handlers/exec.ts b/electron/ipc-handlers/exec.ts index 6864462fc..609585f96 100644 --- a/electron/ipc-handlers/exec.ts +++ b/electron/ipc-handlers/exec.ts @@ -5,8 +5,9 @@ import { log } from 'electron-log/main'; import { loadSimpleStoreAll, saveSimpleStore } from '../simple-store'; import { getWin } from '../main-window'; import { errorHandlerWithFrontendInform } from '../error-handler-with-frontend-inform'; +import { SimpleStoreKey } from '../shared-with-frontend/simple-store.const'; -const COMMAND_MAP_PROP = 'allowedCommands'; +const COMMAND_MAP_PROP = SimpleStoreKey.ALLOWED_COMMANDS; export const initExecIpc = (): void => { ipcMain.on(IPC.EXEC, execWithFrontendErrorHandlerInform); diff --git a/electron/main-window.ts b/electron/main-window.ts index 46b6f4e70..fece7210b 100644 --- a/electron/main-window.ts +++ b/electron/main-window.ts @@ -2,6 +2,7 @@ import windowStateKeeper from 'electron-window-state'; import { App, BrowserWindow, + BrowserWindowConstructorOptions, ipcMain, Menu, MenuItem, @@ -22,6 +23,8 @@ import { showOverlayWindow, } from './overlay-indicator/overlay-indicator'; import { getIsMinimizeToTray, getIsQuiting, setIsQuiting } from './shared-state'; +import { loadSimpleStoreAll } from './simple-store'; +import { SimpleStoreKey } from './shared-with-frontend/simple-store.const'; let mainWin: BrowserWindow; @@ -44,7 +47,7 @@ export const getIsAppReady = (): boolean => { return mainWinModule.isAppReady; }; -export const createWindow = ({ +export const createWindow = async ({ IS_DEV, ICONS_FOLDER, quitApp, @@ -56,7 +59,7 @@ export const createWindow = ({ quitApp: () => void; app: App; customUrl?: string; -}): BrowserWindow => { +}): Promise => { // make sure the main window isn't already created if (mainWin) { errorHandlerWithFrontendInform('Main window already exists'); @@ -73,6 +76,24 @@ export const createWindow = ({ defaultHeight: 800, }); + const simpleStore = await loadSimpleStoreAll(); + const persistedIsUseCustomWindowTitleBar = + simpleStore[SimpleStoreKey.IS_USE_CUSTOM_WINDOW_TITLE_BAR]; + const legacyIsUseObsidianStyleHeader = + simpleStore[SimpleStoreKey.LEGACY_IS_USE_OBSIDIAN_STYLE_HEADER]; + const isUseCustomWindowTitleBar = + persistedIsUseCustomWindowTitleBar ?? legacyIsUseObsidianStyleHeader ?? true; + const titleBarStyle: BrowserWindowConstructorOptions['titleBarStyle'] = + isUseCustomWindowTitleBar || IS_MAC ? 'hidden' : 'default'; + const titleBarOverlay: BrowserWindowConstructorOptions['titleBarOverlay'] = + isUseCustomWindowTitleBar && !IS_MAC + ? { + color: '#00000000', + symbolColor: '#fff', + height: 44, + } + : undefined; + mainWin = new BrowserWindow({ x: mainWindowState.x, y: mainWindowState.y, @@ -81,7 +102,8 @@ export const createWindow = ({ minHeight: 240, minWidth: 300, title: IS_DEV ? 'Super Productivity D' : 'Super Productivity', - titleBarStyle: IS_MAC ? 'hidden' : 'default', + titleBarStyle, + titleBarOverlay, show: false, webPreferences: { scrollBounce: true, @@ -99,7 +121,7 @@ export const createWindow = ({ }, icon: ICONS_FOLDER + '/icon_256x256.png', // Wayland compatibility: disable transparent/frameless features that can cause issues - // transparent: false, + transparent: false, // frame: true, }); diff --git a/electron/shared-with-frontend/simple-store.const.ts b/electron/shared-with-frontend/simple-store.const.ts new file mode 100644 index 000000000..bb875b729 --- /dev/null +++ b/electron/shared-with-frontend/simple-store.const.ts @@ -0,0 +1,6 @@ +export enum SimpleStoreKey { + IS_USE_CUSTOM_WINDOW_TITLE_BAR = 'isUseCustomWindowTitleBar', + ALLOWED_COMMANDS = 'allowedCommands', + // Legacy key kept for backwards compatibility when reading persisted settings + LEGACY_IS_USE_OBSIDIAN_STYLE_HEADER = 'isUseObsidianStyleHeader', +} diff --git a/electron/start-app.ts b/electron/start-app.ts index f06ecffc3..fc501c074 100644 --- a/electron/start-app.ts +++ b/electron/start-app.ts @@ -149,7 +149,7 @@ export const startApp = (): void => { // APP EVENT LISTENERS // ------------------- - appIN.on('ready', createMainWin); + appIN.on('ready', () => createMainWin()); appIN.on('ready', () => initBackupAdapter()); appIN.on('ready', () => initLocalFileSyncAdapter()); appIN.on('ready', () => initFullScreenBlocker(IS_DEV)); @@ -353,8 +353,8 @@ export const startApp = (): void => { } // eslint-disable-next-line prefer-arrow/prefer-arrow-functions - function createMainWin(): void { - mainWin = createWindow({ + async function createMainWin(): Promise { + mainWin = await createWindow({ app, IS_DEV, ICONS_FOLDER, diff --git a/src/app/app.constants.ts b/src/app/app.constants.ts index 5df2e6f84..1beee0cca 100644 --- a/src/app/app.constants.ts +++ b/src/app/app.constants.ts @@ -37,8 +37,8 @@ export enum BodyClass { isLightTheme = 'isLightTheme', isDarkTheme = 'isDarkTheme', isDisableBackgroundTint = 'isDisableBackgroundTint', - isEnabledBackgroundGradient = 'isEnabledBackgroundGradient', isDisableAnimations = 'isDisableAnimations', + isObsidianStyleHeader = 'isObsidianStyleHeader', isDataImportInProgress = 'isDataImportInProgress', hasBgImage = 'hasBgImage', hasMobileBottomNav = 'hasMobileBottomNav', diff --git a/src/app/core-ui/magic-side-nav/magic-side-nav.component.scss b/src/app/core-ui/magic-side-nav/magic-side-nav.component.scss index 106720f97..8b49c55fa 100644 --- a/src/app/core-ui/magic-side-nav/magic-side-nav.component.scss +++ b/src/app/core-ui/magic-side-nav/magic-side-nav.component.scss @@ -88,6 +88,7 @@ z-index: 1002; overflow-y: auto; overflow-x: hidden; + -webkit-app-region: drag; // Slight elevation and clear background for overlay/mobile mode is defined in the mobile block below @@ -165,6 +166,7 @@ cursor: pointer; transition: var(--transition-standard); color: var(--sidenav-text-secondary); + -webkit-app-region: no-drag; :host-context(.isMac.isElectron) & { top: calc(var(--top) + var(--mac-title-bar-padding, 0px)); @@ -207,6 +209,7 @@ .nav-item { margin-bottom: 2px; + -webkit-app-region: no-drag; &.has-children { margin-bottom: 4px; @@ -221,6 +224,7 @@ border: none; opacity: 1; flex-shrink: 0; + -webkit-app-region: no-drag; } // Resize handle (now positioned relative to host component) diff --git a/src/app/core-ui/main-header/main-header.component.scss b/src/app/core-ui/main-header/main-header.component.scss index 47bc5d7df..37284a44c 100644 --- a/src/app/core-ui/main-header/main-header.component.scss +++ b/src/app/core-ui/main-header/main-header.component.scss @@ -21,6 +21,14 @@ :host-context(.isMac.isElectron) { padding-top: calc(var(--mac-title-bar-padding) - 8px); +} + +:host-context(.isObsidianStyleHeader.isElectron) { + padding-right: var(--window-controls-width); // Space for window controls +} + +:host-context(.isMac.isElectron), +:host-context(.isObsidianStyleHeader.isElectron) { -webkit-app-region: drag; cursor: grab; diff --git a/src/app/core/theme/global-theme.service.ts b/src/app/core/theme/global-theme.service.ts index 1d177e745..c436a6ea5 100644 --- a/src/app/core/theme/global-theme.service.ts +++ b/src/app/core/theme/global-theme.service.ts @@ -281,6 +281,15 @@ export class GlobalThemeService { } }); + effect(() => { + const misc = this._globalConfigService.misc(); + if (misc?.isUseCustomWindowTitleBar !== false) { + this.document.body.classList.add(BodyClass.isObsidianStyleHeader); + } else { + this.document.body.classList.remove(BodyClass.isObsidianStyleHeader); + } + }); + // Add/remove hasBgImage class to body when background image changes effect(() => { if (this.backgroundImg()) { diff --git a/src/app/features/config/form-cfgs/misc-settings-form.const.ts b/src/app/features/config/form-cfgs/misc-settings-form.const.ts index d5e26e7dd..7b1a7f8ed 100644 --- a/src/app/features/config/form-cfgs/misc-settings-form.const.ts +++ b/src/app/features/config/form-cfgs/misc-settings-form.const.ts @@ -138,6 +138,18 @@ export const MISC_SETTINGS_FORM_CFG: ConfigFormSection = { label: T.GCF.MISC.IS_OVERLAY_INDICATOR_ENABLED, }, }, + ...((IS_ELECTRON + ? [ + { + key: 'isUseCustomWindowTitleBar', + type: 'checkbox', + templateOptions: { + label: T.GCF.MISC.IS_USE_CUSTOM_WINDOW_TITLE_BAR, + description: T.GCF.MISC.IS_USE_CUSTOM_WINDOW_TITLE_BAR_HINT, + }, + }, + ] + : []) as LimitedFormlyFieldConfig[]), { key: 'customTheme', type: 'select', diff --git a/src/app/features/config/global-config.model.ts b/src/app/features/config/global-config.model.ts index 59dd1f69c..aa4ae8718 100644 --- a/src/app/features/config/global-config.model.ts +++ b/src/app/features/config/global-config.model.ts @@ -38,6 +38,7 @@ export type MiscConfig = Readonly<{ isShowProductivityTipLonger?: boolean; isTrayShowCurrentCountdown?: boolean; isOverlayIndicatorEnabled?: boolean; + isUseCustomWindowTitleBar?: boolean; customTheme?: string; defaultStartPage?: number; unsplashApiKey?: string | null; diff --git a/src/app/features/focus-mode/focus-mode-overlay/focus-mode-overlay.component.html b/src/app/features/focus-mode/focus-mode-overlay/focus-mode-overlay.component.html index a4591745f..0503d9b4f 100644 --- a/src/app/features/focus-mode/focus-mode-overlay/focus-mode-overlay.component.html +++ b/src/app/features/focus-mode/focus-mode-overlay/focus-mode-overlay.component.html @@ -1,4 +1,4 @@ -
+
@if (!isPomodoroEnabled()) {
diff --git a/src/app/features/focus-mode/focus-mode-overlay/focus-mode-overlay.component.scss b/src/app/features/focus-mode/focus-mode-overlay/focus-mode-overlay.component.scss index ed17fcf6a..72b05e8d2 100644 --- a/src/app/features/focus-mode/focus-mode-overlay/focus-mode-overlay.component.scss +++ b/src/app/features/focus-mode/focus-mode-overlay/focus-mode-overlay.component.scss @@ -22,17 +22,17 @@ --z-focus-mode-countdown: 11; } -.mac-os-drag-bar { +.electron-drag-bar { -webkit-app-region: drag; height: 50px; - width: 160px; + width: 100%; position: absolute; top: 0; left: 0; z-index: var(--z-focus-mode-drag-bar); display: none; - :host-context(.isMac) & { + :host-context(.isElectron) & { display: block; } } @@ -72,6 +72,10 @@ main { opacity: 1; } + :host-context(.isObsidianStyleHeader.isElectron.isNoMac) & { + top: calc(var(--s) + 40px); + } + mat-icon { font-size: 32px !important; //make it bigger, the default being 24px. width: 32px; diff --git a/src/app/t.const.ts b/src/app/t.const.ts index 517a488d5..48e24ced5 100644 --- a/src/app/t.const.ts +++ b/src/app/t.const.ts @@ -1930,6 +1930,8 @@ const T = { IS_TRAY_SHOW_CURRENT_COUNTDOWN: 'GCF.MISC.IS_TRAY_SHOW_CURRENT_COUNTDOWN', IS_TRAY_SHOW_CURRENT_TASK: 'GCF.MISC.IS_TRAY_SHOW_CURRENT_TASK', IS_TURN_OFF_MARKDOWN: 'GCF.MISC.IS_TURN_OFF_MARKDOWN', + IS_USE_CUSTOM_WINDOW_TITLE_BAR: 'GCF.MISC.IS_USE_CUSTOM_WINDOW_TITLE_BAR', + IS_USE_CUSTOM_WINDOW_TITLE_BAR_HINT: 'GCF.MISC.IS_USE_CUSTOM_WINDOW_TITLE_BAR_HINT', START_OF_NEXT_DAY: 'GCF.MISC.START_OF_NEXT_DAY', START_OF_NEXT_DAY_HINT: 'GCF.MISC.START_OF_NEXT_DAY_HINT', TASK_NOTES_TPL: 'GCF.MISC.TASK_NOTES_TPL', diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 51fb0a049..1f1dbbef4 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -1901,6 +1901,8 @@ "IS_TRAY_SHOW_CURRENT_COUNTDOWN": "Show current countdown in the tray / status menu (desktop mac only)", "IS_TRAY_SHOW_CURRENT_TASK": "Show current task in the tray / status menu (desktop mac/windows only)", "IS_TURN_OFF_MARKDOWN": "Turn off markdown parsing for task notes", + "IS_USE_CUSTOM_WINDOW_TITLE_BAR": "Use custom title bar (Windows/Linux only)", + "IS_USE_CUSTOM_WINDOW_TITLE_BAR_HINT": "Requires restart to take effect", "START_OF_NEXT_DAY": "Start time of the next day", "START_OF_NEXT_DAY_HINT": "from when (in hour) you want to count the next day has started. default is midnight which is 0.", "TASK_NOTES_TPL": "Task description template", diff --git a/src/styles/_css-variables.scss b/src/styles/_css-variables.scss index 42ac0fb22..78dffb374 100644 --- a/src/styles/_css-variables.scss +++ b/src/styles/_css-variables.scss @@ -46,6 +46,7 @@ --mat-mini-fab-size: 48px; --mac-title-bar-padding: 20px; --card-border-radius: 4px; + --window-controls-width: 96px; // Z-index layers --z-focus-mode-overlay: 101; diff --git a/src/styles/page.scss b/src/styles/page.scss index b16726175..b4c491009 100644 --- a/src/styles/page.scss +++ b/src/styles/page.scss @@ -94,6 +94,10 @@ body { display: none !important; } } + + &.isObsidianStyleHeader.isElectron { + border: 1px solid var(--divider-color); + } } .page-wrapper {