feat(customWindowTitleBar): add sexy custom window title bar <3

This commit is contained in:
Johannes Millan 2025-12-01 15:21:15 +01:00
parent b070233f29
commit c5625de6f1
17 changed files with 99 additions and 14 deletions

View file

@ -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, () => {

View file

@ -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);

View file

@ -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<BrowserWindow> => {
// 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,
});

View file

@ -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',
}

View file

@ -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<void> {
mainWin = await createWindow({
app,
IS_DEV,
ICONS_FOLDER,

View file

@ -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',

View file

@ -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)

View file

@ -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;

View file

@ -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()) {

View file

@ -138,6 +138,18 @@ export const MISC_SETTINGS_FORM_CFG: ConfigFormSection<MiscConfig> = {
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<MiscConfig>[]),
{
key: 'customTheme',
type: 'select',

View file

@ -38,6 +38,7 @@ export type MiscConfig = Readonly<{
isShowProductivityTipLonger?: boolean;
isTrayShowCurrentCountdown?: boolean;
isOverlayIndicatorEnabled?: boolean;
isUseCustomWindowTitleBar?: boolean;
customTheme?: string;
defaultStartPage?: number;
unsplashApiKey?: string | null;

View file

@ -1,4 +1,4 @@
<div class="mac-os-drag-bar"></div>
<div class="electron-drag-bar"></div>
@if (!isPomodoroEnabled()) {
<header>

View file

@ -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;

View file

@ -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',

View file

@ -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",

View file

@ -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;

View file

@ -94,6 +94,10 @@ body {
display: none !important;
}
}
&.isObsidianStyleHeader.isElectron {
border: 1px solid var(--divider-color);
}
}
.page-wrapper {