fix: macosx UI cleanup
This commit is contained in:
@@ -83,6 +83,33 @@ function runWebContentsMenuAction(sender: any, action: AppMenuAction): boolean {
|
|||||||
case 'toggleDevTools':
|
case 'toggleDevTools':
|
||||||
sender.toggleDevTools?.();
|
sender.toggleDevTools?.();
|
||||||
return true;
|
return true;
|
||||||
|
case 'reload':
|
||||||
|
sender.reload?.();
|
||||||
|
return true;
|
||||||
|
case 'forceReload':
|
||||||
|
sender.reloadIgnoringCache?.();
|
||||||
|
return true;
|
||||||
|
case 'resetZoom':
|
||||||
|
sender.setZoomLevel?.(0);
|
||||||
|
return true;
|
||||||
|
case 'zoomIn': {
|
||||||
|
const currentZoomLevel = sender.getZoomLevel?.() ?? 0;
|
||||||
|
sender.setZoomLevel?.(currentZoomLevel + 0.5);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case 'zoomOut': {
|
||||||
|
const currentZoomLevel = sender.getZoomLevel?.() ?? 0;
|
||||||
|
sender.setZoomLevel?.(currentZoomLevel - 0.5);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case 'toggleFullScreen': {
|
||||||
|
const ownerWindow = BrowserWindow.fromWebContents(sender);
|
||||||
|
if (!ownerWindow) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ownerWindow.setFullScreen(!ownerWindow.isFullScreen());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -748,6 +775,17 @@ export function registerIpcHandlers(): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typedAction === 'openInBrowser') {
|
||||||
|
await shell.openExternal('http://localhost:4123/');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typedAction === 'openDataFolder') {
|
||||||
|
const paths = getDatabase().getDataPaths();
|
||||||
|
await shell.openPath(path.dirname(paths.database));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (typedAction === 'reportIssue') {
|
if (typedAction === 'reportIssue') {
|
||||||
await shell.openExternal('https://github.com/rfc1437/bDS/issues');
|
await shell.openExternal('https://github.com/rfc1437/bDS/issues');
|
||||||
return;
|
return;
|
||||||
|
|||||||
146
src/main/main.ts
146
src/main/main.ts
@@ -8,13 +8,13 @@ import { eq } from 'drizzle-orm';
|
|||||||
import { getMediaEngine } from './engine/MediaEngine';
|
import { getMediaEngine } from './engine/MediaEngine';
|
||||||
import { getPostEngine } from './engine/PostEngine';
|
import { getPostEngine } from './engine/PostEngine';
|
||||||
import { PreviewServer } from './engine/PreviewServer';
|
import { PreviewServer } from './engine/PreviewServer';
|
||||||
import { APP_MENU_ACTION_EVENT_MAP, APP_MENU_GROUPS, type AppMenuAction, type AppMenuItemDefinition } from './shared/menuCommands';
|
import { APP_MENU_ACTION_EVENT_MAP, APP_MENU_GROUPS, APP_MENU_ITEM_IDS, type AppMenuAction, type AppMenuItemDefinition } from './shared/menuCommands';
|
||||||
|
|
||||||
let mainWindow: BrowserWindow | null = null;
|
let mainWindow: BrowserWindow | null = null;
|
||||||
let previewServer: PreviewServer | null = null;
|
let previewServer: PreviewServer | null = null;
|
||||||
let activePreviewPostId: string | null = null;
|
let activePreviewPostId: string | null = null;
|
||||||
const PREVIEW_SERVER_PORT = 4123;
|
const PREVIEW_SERVER_PORT = 4123;
|
||||||
const BLOG_PREVIEW_POST_MENU_ID = 'blog.previewPost';
|
const BLOG_PREVIEW_POST_MENU_ID = APP_MENU_ITEM_IDS.previewPost;
|
||||||
|
|
||||||
// Check if dev server is likely running (only in development)
|
// Check if dev server is likely running (only in development)
|
||||||
const isDev = process.env.NODE_ENV === 'development';
|
const isDev = process.env.NODE_ENV === 'development';
|
||||||
@@ -178,12 +178,70 @@ function createApplicationMenu(): Menu {
|
|||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
const triggerMenuAction = (action: AppMenuAction): void => {
|
const triggerMenuAction = async (action: AppMenuAction): Promise<void> => {
|
||||||
if (action === 'quit') {
|
if (action === 'quit') {
|
||||||
app.quit();
|
app.quit();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action === 'openInBrowser') {
|
||||||
|
await openPreviewInBrowser().catch((error) => {
|
||||||
|
console.error('Failed to open preview in browser:', error);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'openDataFolder') {
|
||||||
|
const paths = getDatabase().getDataPaths();
|
||||||
|
void shell.openPath(path.dirname(paths.database));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'previewPost') {
|
||||||
|
await openActivePostPreviewInBrowser().catch((error) => {
|
||||||
|
console.error('Failed to preview active post in browser:', error);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'reload') {
|
||||||
|
mainWindow?.webContents.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'forceReload') {
|
||||||
|
mainWindow?.webContents.reloadIgnoringCache();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'resetZoom') {
|
||||||
|
mainWindow?.webContents.setZoomLevel(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'zoomIn') {
|
||||||
|
if (mainWindow) {
|
||||||
|
const zoomLevel = mainWindow.webContents.getZoomLevel();
|
||||||
|
mainWindow.webContents.setZoomLevel(zoomLevel + 0.5);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'zoomOut') {
|
||||||
|
if (mainWindow) {
|
||||||
|
const zoomLevel = mainWindow.webContents.getZoomLevel();
|
||||||
|
mainWindow.webContents.setZoomLevel(zoomLevel - 0.5);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'toggleFullScreen') {
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.setFullScreen(!mainWindow.isFullScreen());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (action === 'viewOnGitHub') {
|
if (action === 'viewOnGitHub') {
|
||||||
void shell.openExternal('https://github.com/rfc1437/bDS');
|
void shell.openExternal('https://github.com/rfc1437/bDS');
|
||||||
return;
|
return;
|
||||||
@@ -217,8 +275,10 @@ function createApplicationMenu(): Menu {
|
|||||||
return {
|
return {
|
||||||
label: definition.label,
|
label: definition.label,
|
||||||
accelerator: definition.accelerator,
|
accelerator: definition.accelerator,
|
||||||
click: () => {
|
id: definition.id,
|
||||||
triggerMenuAction(action);
|
enabled: definition.enabled,
|
||||||
|
click: async () => {
|
||||||
|
await triggerMenuAction(action);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -229,7 +289,9 @@ function createApplicationMenu(): Menu {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return group.items.map((item) => {
|
const filteredItems = group.items.filter(item => isDev || item.action !== 'toggleDevTools');
|
||||||
|
|
||||||
|
return filteredItems.map((item) => {
|
||||||
if (item.separator) {
|
if (item.separator) {
|
||||||
return { type: 'separator' };
|
return { type: 'separator' };
|
||||||
}
|
}
|
||||||
@@ -241,33 +303,7 @@ function createApplicationMenu(): Menu {
|
|||||||
const template: MenuItemConstructorOptions[] = [
|
const template: MenuItemConstructorOptions[] = [
|
||||||
{
|
{
|
||||||
label: 'File',
|
label: 'File',
|
||||||
submenu: [
|
submenu: buildSharedGroupMenuItems('File'),
|
||||||
buildSharedMenuItem('newPost'),
|
|
||||||
buildSharedMenuItem('importMedia'),
|
|
||||||
{ type: 'separator' },
|
|
||||||
buildSharedMenuItem('save'),
|
|
||||||
{ type: 'separator' },
|
|
||||||
{
|
|
||||||
label: 'Open in Browser',
|
|
||||||
click: async () => {
|
|
||||||
try {
|
|
||||||
await openPreviewInBrowser();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to open preview in browser:', error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ type: 'separator' },
|
|
||||||
{
|
|
||||||
label: 'Open Data Folder',
|
|
||||||
click: async () => {
|
|
||||||
const paths = getDatabase().getDataPaths();
|
|
||||||
shell.openPath(path.dirname(paths.database));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ type: 'separator' },
|
|
||||||
buildSharedMenuItem('quit'),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Edit',
|
label: 'Edit',
|
||||||
@@ -275,51 +311,11 @@ function createApplicationMenu(): Menu {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'View',
|
label: 'View',
|
||||||
submenu: [
|
submenu: buildSharedGroupMenuItems('View'),
|
||||||
...buildSharedGroupMenuItems('View'),
|
|
||||||
{ type: 'separator' },
|
|
||||||
{ role: 'reload' },
|
|
||||||
{ role: 'forceReload' },
|
|
||||||
{
|
|
||||||
label: 'Toggle Developer Tools',
|
|
||||||
accelerator: process.platform === 'darwin' ? 'Cmd+Option+I' : 'Ctrl+Shift+I',
|
|
||||||
click: () => {
|
|
||||||
mainWindow?.webContents.toggleDevTools();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ type: 'separator' },
|
|
||||||
{ role: 'resetZoom' },
|
|
||||||
{ role: 'zoomIn' },
|
|
||||||
{ role: 'zoomOut' },
|
|
||||||
{ type: 'separator' },
|
|
||||||
{ role: 'togglefullscreen' },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Blog',
|
label: 'Blog',
|
||||||
submenu: [
|
submenu: buildSharedGroupMenuItems('Blog'),
|
||||||
buildSharedMenuItem('publishSelected'),
|
|
||||||
{ type: 'separator' },
|
|
||||||
{
|
|
||||||
label: 'Preview Post',
|
|
||||||
id: BLOG_PREVIEW_POST_MENU_ID,
|
|
||||||
enabled: false,
|
|
||||||
accelerator: 'CmdOrCtrl+Shift+V',
|
|
||||||
click: async () => {
|
|
||||||
try {
|
|
||||||
await openActivePostPreviewInBrowser();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to preview active post in browser:', error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ type: 'separator' },
|
|
||||||
buildSharedMenuItem('rebuildDatabase'),
|
|
||||||
buildSharedMenuItem('reindexText'),
|
|
||||||
{ type: 'separator' },
|
|
||||||
buildSharedMenuItem('metadataDiff'),
|
|
||||||
buildSharedMenuItem('generateSitemap'),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Help',
|
label: 'Help',
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ export type AppMenuAction =
|
|||||||
| 'newPost'
|
| 'newPost'
|
||||||
| 'importMedia'
|
| 'importMedia'
|
||||||
| 'save'
|
| 'save'
|
||||||
|
| 'openInBrowser'
|
||||||
|
| 'openDataFolder'
|
||||||
| 'quit'
|
| 'quit'
|
||||||
| 'undo'
|
| 'undo'
|
||||||
| 'redo'
|
| 'redo'
|
||||||
@@ -17,7 +19,14 @@ export type AppMenuAction =
|
|||||||
| 'toggleSidebar'
|
| 'toggleSidebar'
|
||||||
| 'togglePanel'
|
| 'togglePanel'
|
||||||
| 'toggleDevTools'
|
| 'toggleDevTools'
|
||||||
|
| 'reload'
|
||||||
|
| 'forceReload'
|
||||||
|
| 'resetZoom'
|
||||||
|
| 'zoomIn'
|
||||||
|
| 'zoomOut'
|
||||||
|
| 'toggleFullScreen'
|
||||||
| 'publishSelected'
|
| 'publishSelected'
|
||||||
|
| 'previewPost'
|
||||||
| 'rebuildDatabase'
|
| 'rebuildDatabase'
|
||||||
| 'reindexText'
|
| 'reindexText'
|
||||||
| 'metadataDiff'
|
| 'metadataDiff'
|
||||||
@@ -35,6 +44,8 @@ export interface AppMenuItemDefinition {
|
|||||||
accelerator?: string;
|
accelerator?: string;
|
||||||
separator?: boolean;
|
separator?: boolean;
|
||||||
role?: AppMenuRole;
|
role?: AppMenuRole;
|
||||||
|
id?: string;
|
||||||
|
enabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppMenuGroupDefinition {
|
export interface AppMenuGroupDefinition {
|
||||||
@@ -42,14 +53,23 @@ export interface AppMenuGroupDefinition {
|
|||||||
items: AppMenuItemDefinition[];
|
items: AppMenuItemDefinition[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const APP_MENU_ITEM_IDS = {
|
||||||
|
previewPost: 'blog.previewPost',
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const APP_MENU_GROUPS: AppMenuGroupDefinition[] = [
|
export const APP_MENU_GROUPS: AppMenuGroupDefinition[] = [
|
||||||
{
|
{
|
||||||
label: 'File',
|
label: 'File',
|
||||||
items: [
|
items: [
|
||||||
{ label: 'New Post', action: 'newPost', accelerator: 'CmdOrCtrl+N' },
|
{ label: 'New Post', action: 'newPost', accelerator: 'CmdOrCtrl+N' },
|
||||||
{ label: 'Import Media...', action: 'importMedia', accelerator: 'CmdOrCtrl+I' },
|
{ label: 'Import Media...', action: 'importMedia', accelerator: 'CmdOrCtrl+I' },
|
||||||
|
{ label: '', action: 'file-separator-0', separator: true },
|
||||||
{ label: 'Save', action: 'save', accelerator: 'CmdOrCtrl+S' },
|
{ label: 'Save', action: 'save', accelerator: 'CmdOrCtrl+S' },
|
||||||
{ label: '', action: 'file-separator-1', separator: true },
|
{ label: '', action: 'file-separator-1', separator: true },
|
||||||
|
{ label: 'Open in Browser', action: 'openInBrowser' },
|
||||||
|
{ label: '', action: 'file-separator-2', separator: true },
|
||||||
|
{ label: 'Open Data Folder', action: 'openDataFolder' },
|
||||||
|
{ label: '', action: 'file-separator-3', separator: true },
|
||||||
{ label: 'Quit', action: 'quit', accelerator: 'CmdOrCtrl+Q' },
|
{ label: 'Quit', action: 'quit', accelerator: 'CmdOrCtrl+Q' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -78,14 +98,27 @@ export const APP_MENU_GROUPS: AppMenuGroupDefinition[] = [
|
|||||||
{ label: 'Toggle Sidebar', action: 'toggleSidebar', accelerator: 'CmdOrCtrl+B' },
|
{ label: 'Toggle Sidebar', action: 'toggleSidebar', accelerator: 'CmdOrCtrl+B' },
|
||||||
{ label: 'Toggle Panel', action: 'togglePanel', accelerator: 'CmdOrCtrl+J' },
|
{ label: 'Toggle Panel', action: 'togglePanel', accelerator: 'CmdOrCtrl+J' },
|
||||||
{ label: 'Toggle Developer Tools', action: 'toggleDevTools', accelerator: 'CmdOrCtrl+Shift+I' },
|
{ label: 'Toggle Developer Tools', action: 'toggleDevTools', accelerator: 'CmdOrCtrl+Shift+I' },
|
||||||
|
{ label: '', action: 'view-separator-1', separator: true },
|
||||||
|
{ label: 'Reload', action: 'reload' },
|
||||||
|
{ label: 'Force Reload', action: 'forceReload' },
|
||||||
|
{ label: '', action: 'view-separator-2', separator: true },
|
||||||
|
{ label: 'Actual Size', action: 'resetZoom' },
|
||||||
|
{ label: 'Zoom In', action: 'zoomIn' },
|
||||||
|
{ label: 'Zoom Out', action: 'zoomOut' },
|
||||||
|
{ label: '', action: 'view-separator-3', separator: true },
|
||||||
|
{ label: 'Toggle Full Screen', action: 'toggleFullScreen' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Blog',
|
label: 'Blog',
|
||||||
items: [
|
items: [
|
||||||
{ label: 'Publish Selected', action: 'publishSelected', accelerator: 'CmdOrCtrl+Shift+P' },
|
{ label: 'Publish Selected', action: 'publishSelected', accelerator: 'CmdOrCtrl+Shift+P' },
|
||||||
|
{ label: '', action: 'blog-separator-1', separator: true },
|
||||||
|
{ label: 'Preview Post', action: 'previewPost', id: APP_MENU_ITEM_IDS.previewPost, enabled: false, accelerator: 'CmdOrCtrl+Shift+V' },
|
||||||
|
{ label: '', action: 'blog-separator-2', separator: true },
|
||||||
{ label: 'Rebuild Database from Files', action: 'rebuildDatabase' },
|
{ label: 'Rebuild Database from Files', action: 'rebuildDatabase' },
|
||||||
{ label: 'Reindex Search Text', action: 'reindexText' },
|
{ label: 'Reindex Search Text', action: 'reindexText' },
|
||||||
|
{ label: '', action: 'blog-separator-3', separator: true },
|
||||||
{ label: 'Metadata Diff Tool', action: 'metadataDiff' },
|
{ label: 'Metadata Diff Tool', action: 'metadataDiff' },
|
||||||
{ label: 'Generate Sitemap', action: 'generateSitemap' },
|
{ label: 'Generate Sitemap', action: 'generateSitemap' },
|
||||||
],
|
],
|
||||||
@@ -113,6 +146,7 @@ export const APP_MENU_ACTION_EVENT_MAP: Partial<Record<AppMenuAction, string>> =
|
|||||||
toggleSidebar: 'menu:toggleSidebar',
|
toggleSidebar: 'menu:toggleSidebar',
|
||||||
togglePanel: 'menu:togglePanel',
|
togglePanel: 'menu:togglePanel',
|
||||||
toggleDevTools: 'menu:toggleDevTools',
|
toggleDevTools: 'menu:toggleDevTools',
|
||||||
|
previewPost: 'menu:previewPost',
|
||||||
publishSelected: 'menu:publishSelected',
|
publishSelected: 'menu:publishSelected',
|
||||||
rebuildDatabase: 'menu:rebuildDatabase',
|
rebuildDatabase: 'menu:rebuildDatabase',
|
||||||
reindexText: 'menu:reindexText',
|
reindexText: 'menu:reindexText',
|
||||||
@@ -131,4 +165,10 @@ export const APP_MENU_WEB_CONTENTS_ACTIONS: ReadonlySet<AppMenuAction> = new Set
|
|||||||
'delete',
|
'delete',
|
||||||
'selectAll',
|
'selectAll',
|
||||||
'toggleDevTools',
|
'toggleDevTools',
|
||||||
|
'reload',
|
||||||
|
'forceReload',
|
||||||
|
'resetZoom',
|
||||||
|
'zoomIn',
|
||||||
|
'zoomOut',
|
||||||
|
'toggleFullScreen',
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -288,6 +288,25 @@ const App: React.FC = () => {
|
|||||||
}) || (() => {})
|
}) || (() => {})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
unsubscribers.push(
|
||||||
|
window.electronAPI?.on('menu:previewPost', async () => {
|
||||||
|
try {
|
||||||
|
const selectedPostId = useAppStore.getState().selectedPostId;
|
||||||
|
if (!selectedPostId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const previewUrl = await window.electronAPI?.posts.getPreviewUrl(selectedPostId);
|
||||||
|
if (typeof previewUrl === 'string' && previewUrl.length > 0) {
|
||||||
|
window.open(previewUrl, '_blank', 'noopener');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to open selected post preview:', error);
|
||||||
|
showToast.error('Failed to open selected post preview');
|
||||||
|
}
|
||||||
|
}) || (() => {})
|
||||||
|
);
|
||||||
|
|
||||||
unsubscribers.push(
|
unsubscribers.push(
|
||||||
window.electronAPI?.on('menu:openDocumentation', () => {
|
window.electronAPI?.on('menu:openDocumentation', () => {
|
||||||
openTab({ id: 'documentation', type: 'documentation', isTransient: false });
|
openTab({ id: 'documentation', type: 'documentation', isTransient: false });
|
||||||
|
|||||||
@@ -133,83 +133,6 @@ export const WindowTitleBar: React.FC = () => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isMac) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rootStyle = document.documentElement.style;
|
|
||||||
let isDisposed = false;
|
|
||||||
let resolutionQuery: MediaQueryList | null = null;
|
|
||||||
|
|
||||||
const syncMacInset = async () => {
|
|
||||||
const metrics = await window.electronAPI?.app?.getTitleBarMetrics?.();
|
|
||||||
if (isDisposed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (metrics && Number.isFinite(metrics.macosLeftInset)) {
|
|
||||||
rootStyle.setProperty('--bds-titlebar-macos-left-inset', `${Math.max(0, Math.round(metrics.macosLeftInset))}px`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const bindResolutionListener = () => {
|
|
||||||
if (typeof window.matchMedia !== 'function') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resolutionQuery && typeof resolutionQuery.removeEventListener === 'function') {
|
|
||||||
resolutionQuery.removeEventListener('change', onResolutionChange);
|
|
||||||
}
|
|
||||||
|
|
||||||
resolutionQuery = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
|
|
||||||
if (typeof resolutionQuery.addEventListener === 'function') {
|
|
||||||
resolutionQuery.addEventListener('change', onResolutionChange);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onResolutionChange = () => {
|
|
||||||
void syncMacInset();
|
|
||||||
bindResolutionListener();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onResize = () => {
|
|
||||||
void syncMacInset();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onVisibilityChange = () => {
|
|
||||||
if (!document.hidden) {
|
|
||||||
void syncMacInset();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const canListenToWindowEvents = typeof window.addEventListener === 'function';
|
|
||||||
const canListenToDocumentEvents = typeof document.addEventListener === 'function';
|
|
||||||
|
|
||||||
void syncMacInset();
|
|
||||||
bindResolutionListener();
|
|
||||||
if (canListenToWindowEvents) {
|
|
||||||
window.addEventListener('resize', onResize);
|
|
||||||
}
|
|
||||||
if (canListenToDocumentEvents) {
|
|
||||||
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
isDisposed = true;
|
|
||||||
if (canListenToWindowEvents && typeof window.removeEventListener === 'function') {
|
|
||||||
window.removeEventListener('resize', onResize);
|
|
||||||
}
|
|
||||||
if (canListenToDocumentEvents && typeof document.removeEventListener === 'function') {
|
|
||||||
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resolutionQuery && typeof resolutionQuery.removeEventListener === 'function') {
|
|
||||||
resolutionQuery.removeEventListener('change', onResolutionChange);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [isMac]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updateTitle = () => {
|
const updateTitle = () => {
|
||||||
setWindowTitle(document.title || 'Blogging Desktop Server');
|
setWindowTitle(document.title || 'Blogging Desktop Server');
|
||||||
@@ -363,6 +286,10 @@ export const WindowTitleBar: React.FC = () => {
|
|||||||
}, [openMenu?.label]);
|
}, [openMenu?.label]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isMac) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const onKeyDown = (event: KeyboardEvent) => {
|
const onKeyDown = (event: KeyboardEvent) => {
|
||||||
if (event.defaultPrevented || event.metaKey || event.ctrlKey) {
|
if (event.defaultPrevented || event.metaKey || event.ctrlKey) {
|
||||||
return;
|
return;
|
||||||
@@ -401,7 +328,7 @@ export const WindowTitleBar: React.FC = () => {
|
|||||||
document.removeEventListener('keydown', onKeyDown);
|
document.removeEventListener('keydown', onKeyDown);
|
||||||
document.removeEventListener('mousedown', onDocumentMouseDown);
|
document.removeEventListener('mousedown', onDocumentMouseDown);
|
||||||
};
|
};
|
||||||
}, [mnemonicByKey, showMnemonics]);
|
}, [isMac, mnemonicByKey, showMnemonics]);
|
||||||
|
|
||||||
const handleMenuButtonClick = (event: React.MouseEvent<HTMLButtonElement>, label: string) => {
|
const handleMenuButtonClick = (event: React.MouseEvent<HTMLButtonElement>, label: string) => {
|
||||||
const left = getMenuLeft(label);
|
const left = getMenuLeft(label);
|
||||||
@@ -461,6 +388,7 @@ export const WindowTitleBar: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`window-titlebar${isMac ? ' is-mac' : ''}`} data-testid="window-titlebar" ref={menuRootRef}>
|
<div className={`window-titlebar${isMac ? ' is-mac' : ''}`} data-testid="window-titlebar" ref={menuRootRef}>
|
||||||
|
{!isMac && (
|
||||||
<div className="window-titlebar-menu-bar" data-testid="window-titlebar-menu-bar">
|
<div className="window-titlebar-menu-bar" data-testid="window-titlebar-menu-bar">
|
||||||
{visibleMenuGroups.map(group => (
|
{visibleMenuGroups.map(group => (
|
||||||
<button
|
<button
|
||||||
@@ -478,6 +406,7 @@ export const WindowTitleBar: React.FC = () => {
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<div className="window-titlebar-drag-region" />
|
<div className="window-titlebar-drag-region" />
|
||||||
<div className="window-titlebar-title" data-testid="window-titlebar-title" title={windowTitle}>
|
<div className="window-titlebar-title" data-testid="window-titlebar-title" title={windowTitle}>
|
||||||
{windowTitle}
|
{windowTitle}
|
||||||
@@ -512,7 +441,7 @@ export const WindowTitleBar: React.FC = () => {
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{openMenu && activeMenu && (
|
{!isMac && openMenu && activeMenu && (
|
||||||
<div
|
<div
|
||||||
className="window-titlebar-menu-dropdown"
|
className="window-titlebar-menu-dropdown"
|
||||||
data-testid="window-titlebar-menu-dropdown"
|
data-testid="window-titlebar-menu-dropdown"
|
||||||
|
|||||||
@@ -1461,6 +1461,110 @@ describe('IPC Handlers', () => {
|
|||||||
expect(shell.openExternal).toHaveBeenCalledWith('https://github.com/rfc1437/bDS');
|
expect(shell.openExternal).toHaveBeenCalledWith('https://github.com/rfc1437/bDS');
|
||||||
expect(send).not.toHaveBeenCalled();
|
expect(send).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should open preview root URL when action is openInBrowser', async () => {
|
||||||
|
const { shell } = await import('electron');
|
||||||
|
const send = vi.fn();
|
||||||
|
const event = { sender: { send } };
|
||||||
|
|
||||||
|
await invokeHandlerWithEvent(event, 'app:triggerMenuAction', 'openInBrowser');
|
||||||
|
|
||||||
|
expect(shell.openExternal).toHaveBeenCalledWith('http://localhost:4123/');
|
||||||
|
expect(send).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open the data folder when action is openDataFolder', async () => {
|
||||||
|
const { shell } = await import('electron');
|
||||||
|
const send = vi.fn();
|
||||||
|
const event = { sender: { send } };
|
||||||
|
|
||||||
|
await invokeHandlerWithEvent(event, 'app:triggerMenuAction', 'openDataFolder');
|
||||||
|
|
||||||
|
expect(shell.openPath).toHaveBeenCalledWith('/mock/data');
|
||||||
|
expect(send).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should forward previewPost to renderer menu channel', async () => {
|
||||||
|
const send = vi.fn();
|
||||||
|
const event = { sender: { send } };
|
||||||
|
|
||||||
|
await invokeHandlerWithEvent(event, 'app:triggerMenuAction', 'previewPost');
|
||||||
|
|
||||||
|
expect(send).toHaveBeenCalledWith('menu:previewPost');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reload sender when action is reload', async () => {
|
||||||
|
const reload = vi.fn();
|
||||||
|
const send = vi.fn();
|
||||||
|
const event = { sender: { reload, send } };
|
||||||
|
|
||||||
|
await invokeHandlerWithEvent(event, 'app:triggerMenuAction', 'reload');
|
||||||
|
|
||||||
|
expect(reload).toHaveBeenCalled();
|
||||||
|
expect(send).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should force reload sender when action is forceReload', async () => {
|
||||||
|
const reloadIgnoringCache = vi.fn();
|
||||||
|
const send = vi.fn();
|
||||||
|
const event = { sender: { reloadIgnoringCache, send } };
|
||||||
|
|
||||||
|
await invokeHandlerWithEvent(event, 'app:triggerMenuAction', 'forceReload');
|
||||||
|
|
||||||
|
expect(reloadIgnoringCache).toHaveBeenCalled();
|
||||||
|
expect(send).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset zoom level when action is resetZoom', async () => {
|
||||||
|
const setZoomLevel = vi.fn();
|
||||||
|
const send = vi.fn();
|
||||||
|
const event = { sender: { setZoomLevel, send } };
|
||||||
|
|
||||||
|
await invokeHandlerWithEvent(event, 'app:triggerMenuAction', 'resetZoom');
|
||||||
|
|
||||||
|
expect(setZoomLevel).toHaveBeenCalledWith(0);
|
||||||
|
expect(send).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should zoom in when action is zoomIn', async () => {
|
||||||
|
const getZoomLevel = vi.fn(() => 0);
|
||||||
|
const setZoomLevel = vi.fn();
|
||||||
|
const send = vi.fn();
|
||||||
|
const event = { sender: { getZoomLevel, setZoomLevel, send } };
|
||||||
|
|
||||||
|
await invokeHandlerWithEvent(event, 'app:triggerMenuAction', 'zoomIn');
|
||||||
|
|
||||||
|
expect(setZoomLevel).toHaveBeenCalledWith(0.5);
|
||||||
|
expect(send).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should zoom out when action is zoomOut', async () => {
|
||||||
|
const getZoomLevel = vi.fn(() => 0.5);
|
||||||
|
const setZoomLevel = vi.fn();
|
||||||
|
const send = vi.fn();
|
||||||
|
const event = { sender: { getZoomLevel, setZoomLevel, send } };
|
||||||
|
|
||||||
|
await invokeHandlerWithEvent(event, 'app:triggerMenuAction', 'zoomOut');
|
||||||
|
|
||||||
|
expect(setZoomLevel).toHaveBeenCalledWith(0);
|
||||||
|
expect(send).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should toggle fullscreen on owner window when action is toggleFullScreen', async () => {
|
||||||
|
const { BrowserWindow } = await import('electron');
|
||||||
|
const sender = { send: vi.fn() };
|
||||||
|
const ownerWindow = {
|
||||||
|
isFullScreen: vi.fn(() => false),
|
||||||
|
setFullScreen: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mocked(BrowserWindow.fromWebContents).mockReturnValue(ownerWindow as unknown as ReturnType<typeof BrowserWindow.fromWebContents>);
|
||||||
|
|
||||||
|
await invokeHandlerWithEvent({ sender }, 'app:triggerMenuAction', 'toggleFullScreen');
|
||||||
|
|
||||||
|
expect(BrowserWindow.fromWebContents).toHaveBeenCalledWith(sender);
|
||||||
|
expect(ownerWindow.setFullScreen).toHaveBeenCalledWith(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ describe('WindowTitleBar', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('applies a macOS class to the title bar root for platform-specific spacing', () => {
|
it('renders title bar on macOS but hides simulated menu buttons', () => {
|
||||||
Object.defineProperty(navigator, 'platform', {
|
Object.defineProperty(navigator, 'platform', {
|
||||||
value: 'MacIntel',
|
value: 'MacIntel',
|
||||||
configurable: true,
|
configurable: true,
|
||||||
@@ -27,10 +27,14 @@ describe('WindowTitleBar', () => {
|
|||||||
|
|
||||||
render(<WindowTitleBar />);
|
render(<WindowTitleBar />);
|
||||||
|
|
||||||
expect(screen.getByTestId('window-titlebar')).toHaveClass('is-mac');
|
expect(screen.getByTestId('window-titlebar')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole('button', { name: 'File' })).toBeNull();
|
||||||
|
expect(screen.queryByRole('button', { name: 'Edit' })).toBeNull();
|
||||||
|
expect(screen.getByLabelText('Toggle Sidebar')).toBeInTheDocument();
|
||||||
|
expect(screen.getByLabelText('Toggle Panel')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets macOS title bar inset CSS variable from dynamic native metrics', async () => {
|
it('does not request macOS title bar metrics when simulated title bar is disabled', async () => {
|
||||||
Object.defineProperty(navigator, 'platform', {
|
Object.defineProperty(navigator, 'platform', {
|
||||||
value: 'MacIntel',
|
value: 'MacIntel',
|
||||||
configurable: true,
|
configurable: true,
|
||||||
@@ -48,8 +52,8 @@ describe('WindowTitleBar', () => {
|
|||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(getTitleBarMetrics).toHaveBeenCalled();
|
expect(getTitleBarMetrics).not.toHaveBeenCalled();
|
||||||
expect(document.documentElement.style.getPropertyValue('--bds-titlebar-macos-left-inset')).toBe('102px');
|
expect(document.documentElement.style.getPropertyValue('--bds-titlebar-macos-left-inset')).toBe('');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a right-side sidebar toggle button and toggles store state', () => {
|
it('renders a right-side sidebar toggle button and toggles store state', () => {
|
||||||
|
|||||||
@@ -12,4 +12,31 @@ describe('Help menu documentation entry', () => {
|
|||||||
it('maps Open Documentation to a renderer menu event', () => {
|
it('maps Open Documentation to a renderer menu event', () => {
|
||||||
expect(APP_MENU_ACTION_EVENT_MAP.openDocumentation).toBe('menu:openDocumentation');
|
expect(APP_MENU_ACTION_EVENT_MAP.openDocumentation).toBe('menu:openDocumentation');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('includes Open in Browser and Open Data Folder actions in File menu', () => {
|
||||||
|
const fileGroup = APP_MENU_GROUPS.find((group) => group.label === 'File');
|
||||||
|
|
||||||
|
expect(fileGroup).toBeDefined();
|
||||||
|
expect(fileGroup?.items.some((item) => item.action === 'openInBrowser')).toBe(true);
|
||||||
|
expect(fileGroup?.items.some((item) => item.action === 'openDataFolder')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes Preview Post action in Blog menu', () => {
|
||||||
|
const blogGroup = APP_MENU_GROUPS.find((group) => group.label === 'Blog');
|
||||||
|
|
||||||
|
expect(blogGroup).toBeDefined();
|
||||||
|
expect(blogGroup?.items.some((item) => item.action === 'previewPost')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes shared View actions for reload, zoom and fullscreen controls', () => {
|
||||||
|
const viewGroup = APP_MENU_GROUPS.find((group) => group.label === 'View');
|
||||||
|
|
||||||
|
expect(viewGroup).toBeDefined();
|
||||||
|
expect(viewGroup?.items.some((item) => item.action === 'reload')).toBe(true);
|
||||||
|
expect(viewGroup?.items.some((item) => item.action === 'forceReload')).toBe(true);
|
||||||
|
expect(viewGroup?.items.some((item) => item.action === 'resetZoom')).toBe(true);
|
||||||
|
expect(viewGroup?.items.some((item) => item.action === 'zoomIn')).toBe(true);
|
||||||
|
expect(viewGroup?.items.some((item) => item.action === 'zoomOut')).toBe(true);
|
||||||
|
expect(viewGroup?.items.some((item) => item.action === 'toggleFullScreen')).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user