feat: custom title bar that is more compact

This commit is contained in:
2026-02-17 10:52:25 +01:00
parent 03cf6ae9e7
commit 7b5829e965
14 changed files with 756 additions and 113 deletions

View File

@@ -12,6 +12,7 @@ import { getGitEngine } from '../engine/GitEngine';
import { taskManager, TaskProgress } from '../engine/TaskManager';
import { getDatabase } from '../database';
import { media } from '../database/schema';
import { APP_MENU_ACTION_EVENT_MAP, APP_MENU_WEB_CONTENTS_ACTIONS, type AppMenuAction } from '../shared/menuCommands';
/**
* Wrap an IPC handler so that "Database is closing" errors during shutdown
@@ -46,6 +47,45 @@ function resolvePostCreatedAt(post: { createdAt: Date | string }): Date {
return Number.isNaN(parsed.getTime()) ? new Date() : parsed;
}
function runWebContentsMenuAction(sender: any, action: AppMenuAction): boolean {
if (!sender) {
return false;
}
if (!APP_MENU_WEB_CONTENTS_ACTIONS.has(action)) {
return false;
}
switch (action) {
case 'undo':
sender.undo?.();
return true;
case 'redo':
sender.redo?.();
return true;
case 'cut':
sender.cut?.();
return true;
case 'copy':
sender.copy?.();
return true;
case 'paste':
sender.paste?.();
return true;
case 'delete':
sender.delete?.();
return true;
case 'selectAll':
sender.selectAll?.();
return true;
case 'toggleDevTools':
sender.toggleDevTools?.();
return true;
default:
return false;
}
}
export function registerIpcHandlers(): void {
// ============ Git Handlers ============
@@ -673,6 +713,20 @@ export function registerIpcHandlers(): void {
}
});
safeHandle('app:triggerMenuAction', async (event, action: string) => {
const typedAction = action as AppMenuAction;
const handledByWebContents = runWebContentsMenuAction((event as any)?.sender, typedAction);
if (handledByWebContents) {
return;
}
const channel = APP_MENU_ACTION_EVENT_MAP[typedAction];
if (!channel) {
return;
}
event.sender.send(channel);
});
// ============ Meta Handlers ============
safeHandle('meta:getTags', async () => {

View File

@@ -8,6 +8,7 @@ import { eq } from 'drizzle-orm';
import { getMediaEngine } from './engine/MediaEngine';
import { getPostEngine } from './engine/PostEngine';
import { PreviewServer } from './engine/PreviewServer';
import { APP_MENU_ACTION_EVENT_MAP, APP_MENU_GROUPS, type AppMenuAction, type AppMenuItemDefinition } from './shared/menuCommands';
let mainWindow: BrowserWindow | null = null;
let previewServer: PreviewServer | null = null;
@@ -58,7 +59,7 @@ function createWindow(): void {
symbolColor: '#cccccc',
height: 34,
},
autoHideMenuBar: true,
autoHideMenuBar: false,
}),
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
@@ -169,32 +170,67 @@ async function startPreviewServerOnAppStart(): Promise<void> {
}
function createApplicationMenu(): Menu {
const commandDefinitions = APP_MENU_GROUPS
.flatMap(group => group.items)
.filter(item => !item.separator)
.reduce<Record<string, AppMenuItemDefinition>>((acc, item) => {
acc[item.action] = item;
return acc;
}, {});
const triggerMenuAction = (action: AppMenuAction): void => {
const channel = APP_MENU_ACTION_EVENT_MAP[action];
if (channel) {
mainWindow?.webContents.send(channel);
}
};
const buildSharedMenuItem = (action: AppMenuAction): MenuItemConstructorOptions => {
const definition = commandDefinitions[action];
if (!definition) {
throw new Error(`Unknown shared menu action: ${action}`);
}
if (definition.role) {
return {
label: definition.label,
role: definition.role,
accelerator: definition.accelerator,
};
}
return {
label: definition.label,
accelerator: definition.accelerator,
click: () => {
triggerMenuAction(action);
},
};
};
const buildSharedGroupMenuItems = (groupLabel: string): MenuItemConstructorOptions[] => {
const group = APP_MENU_GROUPS.find(item => item.label === groupLabel);
if (!group) {
return [];
}
return group.items.map((item) => {
if (item.separator) {
return { type: 'separator' };
}
return buildSharedMenuItem(item.action as AppMenuAction);
});
};
const template: MenuItemConstructorOptions[] = [
{
label: 'File',
submenu: [
{
label: 'New Post',
accelerator: 'CmdOrCtrl+N',
click: () => {
mainWindow?.webContents.send('menu:newPost');
},
},
{
label: 'Import Media...',
accelerator: 'CmdOrCtrl+I',
click: () => {
mainWindow?.webContents.send('menu:importMedia');
},
},
buildSharedMenuItem('newPost'),
buildSharedMenuItem('importMedia'),
{ type: 'separator' },
{
label: 'Save',
accelerator: 'CmdOrCtrl+S',
click: () => {
mainWindow?.webContents.send('menu:save');
},
},
buildSharedMenuItem('save'),
{ type: 'separator' },
{
label: 'Open in Browser',
@@ -226,65 +262,12 @@ function createApplicationMenu(): Menu {
},
{
label: 'Edit',
submenu: [
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' },
{ role: 'delete' },
{ type: 'separator' },
{ role: 'selectAll' },
{ type: 'separator' },
{
label: 'Find',
accelerator: 'CmdOrCtrl+F',
click: () => {
mainWindow?.webContents.send('menu:find');
},
},
{
label: 'Replace',
accelerator: 'CmdOrCtrl+H',
click: () => {
mainWindow?.webContents.send('menu:replace');
},
},
],
submenu: buildSharedGroupMenuItems('Edit'),
},
{
label: 'View',
submenu: [
{
label: 'Posts',
accelerator: 'CmdOrCtrl+1',
click: () => {
mainWindow?.webContents.send('menu:viewPosts');
},
},
{
label: 'Media',
accelerator: 'CmdOrCtrl+2',
click: () => {
mainWindow?.webContents.send('menu:viewMedia');
},
},
{ type: 'separator' },
{
label: 'Toggle Sidebar',
accelerator: 'CmdOrCtrl+B',
click: () => {
mainWindow?.webContents.send('menu:toggleSidebar');
},
},
{
label: 'Toggle Panel',
accelerator: 'CmdOrCtrl+J',
click: () => {
mainWindow?.webContents.send('menu:togglePanel');
},
},
...buildSharedGroupMenuItems('View'),
{ type: 'separator' },
{ role: 'reload' },
{ role: 'forceReload' },
@@ -306,13 +289,7 @@ function createApplicationMenu(): Menu {
{
label: 'Blog',
submenu: [
{
label: 'Publish Selected',
accelerator: 'CmdOrCtrl+Shift+P',
click: () => {
mainWindow?.webContents.send('menu:publishSelected');
},
},
buildSharedMenuItem('publishSelected'),
{ type: 'separator' },
{
label: 'Preview Post',
@@ -328,36 +305,16 @@ function createApplicationMenu(): Menu {
},
},
{ type: 'separator' },
{
label: 'Rebuild Database from Files',
click: () => {
mainWindow?.webContents.send('menu:rebuildDatabase');
},
},
{
label: 'Reindex Search Text',
click: () => {
mainWindow?.webContents.send('menu:reindexText');
},
},
buildSharedMenuItem('rebuildDatabase'),
buildSharedMenuItem('reindexText'),
{ type: 'separator' },
{
label: 'Metadata Diff Tool',
click: () => {
mainWindow?.webContents.send('menu:metadataDiff');
},
},
buildSharedMenuItem('metadataDiff'),
],
},
{
label: 'Help',
submenu: [
{
label: 'About Blogging Desktop Server',
click: () => {
mainWindow?.webContents.send('menu:about');
},
},
buildSharedMenuItem('about'),
{ type: 'separator' },
{
label: 'View on GitHub',

View File

@@ -142,6 +142,7 @@ export const electronAPI: ElectronAPI = {
getDefaultProjectPath: (projectId: string) => ipcRenderer.invoke('app:getDefaultProjectPath', projectId),
readProjectMetadata: (folderPath: string) => ipcRenderer.invoke('app:readProjectMetadata', folderPath),
setPreviewPostTarget: (postId: string | null) => ipcRenderer.invoke('app:setPreviewPostTarget', postId),
triggerMenuAction: (action: string) => ipcRenderer.invoke('app:triggerMenuAction', action),
},
// Meta (tags, categories, and project metadata)

View File

@@ -510,6 +510,7 @@ export interface ElectronAPI {
getDefaultProjectPath: (projectId: string) => Promise<string>;
readProjectMetadata: (folderPath: string) => Promise<{ name?: string; description?: string; mainLanguage?: string } | null>;
setPreviewPostTarget: (postId: string | null) => Promise<void>;
triggerMenuAction: (action: string) => Promise<void>;
};
meta: {
getTags: () => Promise<string[]>;

View File

@@ -0,0 +1,120 @@
export type AppMenuAction =
| 'newPost'
| 'importMedia'
| 'save'
| 'undo'
| 'redo'
| 'cut'
| 'copy'
| 'paste'
| 'delete'
| 'selectAll'
| 'find'
| 'replace'
| 'viewPosts'
| 'viewMedia'
| 'toggleSidebar'
| 'togglePanel'
| 'toggleDevTools'
| 'publishSelected'
| 'rebuildDatabase'
| 'reindexText'
| 'metadataDiff'
| 'about';
export type AppMenuRole = 'undo' | 'redo' | 'cut' | 'copy' | 'paste' | 'delete' | 'selectAll';
export interface AppMenuItemDefinition {
label: string;
action: AppMenuAction | `${string}-separator-${number}`;
accelerator?: string;
separator?: boolean;
role?: AppMenuRole;
}
export interface AppMenuGroupDefinition {
label: 'File' | 'Edit' | 'View' | 'Blog' | 'Help';
items: AppMenuItemDefinition[];
}
export const APP_MENU_GROUPS: AppMenuGroupDefinition[] = [
{
label: 'File',
items: [
{ label: 'New Post', action: 'newPost', accelerator: 'CmdOrCtrl+N' },
{ label: 'Import Media...', action: 'importMedia', accelerator: 'CmdOrCtrl+I' },
{ label: 'Save', action: 'save', accelerator: 'CmdOrCtrl+S' },
],
},
{
label: 'Edit',
items: [
{ label: 'Undo', action: 'undo', accelerator: 'CmdOrCtrl+Z', role: 'undo' },
{ label: 'Redo', action: 'redo', accelerator: 'CmdOrCtrl+Y', role: 'redo' },
{ label: '', action: 'edit-separator-1', separator: true },
{ label: 'Cut', action: 'cut', accelerator: 'CmdOrCtrl+X', role: 'cut' },
{ label: 'Copy', action: 'copy', accelerator: 'CmdOrCtrl+C', role: 'copy' },
{ label: 'Paste', action: 'paste', accelerator: 'CmdOrCtrl+V', role: 'paste' },
{ label: 'Delete', action: 'delete', role: 'delete' },
{ label: '', action: 'edit-separator-2', separator: true },
{ label: 'Select All', action: 'selectAll', accelerator: 'CmdOrCtrl+A', role: 'selectAll' },
{ label: '', action: 'edit-separator-3', separator: true },
{ label: 'Find', action: 'find', accelerator: 'CmdOrCtrl+F' },
{ label: 'Replace', action: 'replace', accelerator: 'CmdOrCtrl+H' },
],
},
{
label: 'View',
items: [
{ label: 'Posts', action: 'viewPosts', accelerator: 'CmdOrCtrl+1' },
{ label: 'Media', action: 'viewMedia', accelerator: 'CmdOrCtrl+2' },
{ label: 'Toggle Sidebar', action: 'toggleSidebar', accelerator: 'CmdOrCtrl+B' },
{ label: 'Toggle Panel', action: 'togglePanel', accelerator: 'CmdOrCtrl+J' },
{ label: 'Toggle Developer Tools', action: 'toggleDevTools', accelerator: 'CmdOrCtrl+Shift+I' },
],
},
{
label: 'Blog',
items: [
{ label: 'Publish Selected', action: 'publishSelected', accelerator: 'CmdOrCtrl+Shift+P' },
{ label: 'Rebuild Database from Files', action: 'rebuildDatabase' },
{ label: 'Reindex Search Text', action: 'reindexText' },
{ label: 'Metadata Diff Tool', action: 'metadataDiff' },
],
},
{
label: 'Help',
items: [
{ label: 'About Blogging Desktop Server', action: 'about' },
],
},
];
export const APP_MENU_ACTION_EVENT_MAP: Partial<Record<AppMenuAction, string>> = {
newPost: 'menu:newPost',
importMedia: 'menu:importMedia',
save: 'menu:save',
find: 'menu:find',
replace: 'menu:replace',
viewPosts: 'menu:viewPosts',
viewMedia: 'menu:viewMedia',
toggleSidebar: 'menu:toggleSidebar',
togglePanel: 'menu:togglePanel',
toggleDevTools: 'menu:toggleDevTools',
publishSelected: 'menu:publishSelected',
rebuildDatabase: 'menu:rebuildDatabase',
reindexText: 'menu:reindexText',
metadataDiff: 'menu:metadataDiff',
about: 'menu:about',
};
export const APP_MENU_WEB_CONTENTS_ACTIONS: ReadonlySet<AppMenuAction> = new Set([
'undo',
'redo',
'cut',
'copy',
'paste',
'delete',
'selectAll',
'toggleDevTools',
]);