TidGi-Desktop/src/services/view/handleNewWindow.ts
lin onetwo 19ef74a4a6
Feat/mini window (#642)
* feat: new config for tidgi mini window

* chore: upgrade electron-forge

* fix: use 汉语 和 漢語

* feat: shortcut to open mini window

* test: TidGiMenubarWindow

* feat: allow updateWindowProperties on the fly

* fix: wrong icon path

* fix: log not showing error message and stack

* refactor: directly log error when using logger.error

* feat: shortcut to open window

* fix: menubar not closed

* test: e2e for menubar

* test: keyboard shortcut

* test: wiki web content, and refactor to files

* test: update command

* Update Testing.md

* test: menubar settings about menubarSyncWorkspaceWithMainWindow, menubarFixedWorkspaceId

* test: simplify menubar test and cleanup test config

* fix: view missing when execute several test all together

* refactor: use hook to cleanup menubar setting

* refactor: I clear test ai settings to before hook

* Add option to show title bar on menubar window

Introduces a new preference 'showMenubarWindowTitleBar' allowing users to toggle the title bar visibility on the menubar window. Updates related services, interfaces, and UI components to support this feature, and adds corresponding localization strings for English and Chinese.

* refactor: tidgiminiwindow

* refactor: preference keys to right order

* Refactor window dimension checks to use constants

Replaces hardcoded window dimensions with values from windowDimension and WindowNames constants for improved maintainability and consistency in window identification and checks.

* I cleanup test wiki

* Update defaultPreferences.ts

* test: mini window workspace switch

* fix: image broken by ai, and lint

* fix: can't switch to mini window

* refactor: useless todo

* Update index.ts

* refactor: reuse serialize-error

* Update index.ts

* Update testKeyboardShortcuts.ts

* refactor: dup logic

* Update ui.ts

* fix: electron-ipc-cat
2025-10-21 20:07:04 +08:00

163 lines
7.3 KiB
TypeScript

import { BrowserWindowConstructorOptions, shell, WebContentsView } from 'electron';
import windowStateKeeper from 'electron-window-state';
import { SETTINGS_FOLDER } from '@/constants/appPaths';
import { MetaDataChannel } from '@/constants/channels';
import { extractDomain, isInternalUrl } from '@/helpers/url';
import { container } from '@services/container';
import { logger } from '@services/libs/log';
import type { IMenuService } from '@services/menu/interface';
import serviceIdentifier from '@services/serviceIdentifier';
import { getPreloadPath } from '@services/windows/viteEntry';
import type { IBrowserViewMetaData } from '@services/windows/WindowProperties';
import { windowDimension, WindowNames } from '@services/windows/WindowProperties';
import type { IWorkspace, IWorkspaceService } from '@services/workspaces/interface';
import { isWikiWorkspace } from '@services/workspaces/interface';
import type { INewWindowAction } from './interface';
import type { IViewMeta } from './setupViewEventHandlers';
import { handleOpenFileExternalLink } from './setupViewFileProtocol';
export interface INewWindowContext {
meta: IViewMeta;
sharedWebPreferences: BrowserWindowConstructorOptions['webPreferences'];
view: WebContentsView;
workspace: IWorkspace;
}
export function handleNewWindow(
details: Electron.HandlerDetails,
newWindowContext: INewWindowContext,
parentWebContents: Electron.WebContents,
): INewWindowAction {
const { url: nextUrl, disposition, frameName } = details;
/**
* Guess from tiddlywiki's `core/modules/startup/windows.js`, it will open with details {
url: 'about:blank#blocked',
frameName: 'external-XXXSomeTiddlerTitle',
features: 'scrollbars,width=700,height=600',
disposition: 'new-window',
referrer: { url: '', policy: 'strict-origin-when-cross-origin' },
postBody: undefined
}
*/
const mightFromTiddlywikiOpenNewWindow = frameName.startsWith('external-');
logger.debug('Getting url that will open externally', { ...details, fromTW: mightFromTiddlywikiOpenNewWindow });
// don't show useless blank page
if (nextUrl.startsWith('about:blank') && !mightFromTiddlywikiOpenNewWindow) {
logger.debug('ignore about:blank');
return { action: 'deny' };
}
const workspaceService = container.get<IWorkspaceService>(serviceIdentifier.Workspace);
const nextDomain = extractDomain(nextUrl);
const handleOpenFileExternalLinkAction = handleOpenFileExternalLink(nextUrl, newWindowContext);
if (handleOpenFileExternalLinkAction !== undefined) return handleOpenFileExternalLinkAction;
// open external url in browser
if (nextDomain !== undefined && (disposition === 'foreground-tab' || disposition === 'background-tab')) {
logger.debug('openExternal', { nextUrl, nextDomain, disposition, function: 'handleNewWindow' });
void shell.openExternal(nextUrl).catch((error_: unknown) => {
const error = error_ as Error;
logger.error(
`handleNewWindow() openExternal error ${error.message}`,
{ error },
);
});
return {
action: 'deny',
};
}
logger.debug('Allowing creating new window', { newWindowContext, function: 'handleNewWindow' });
const { view: parentWindowView, workspace, sharedWebPreferences, meta } = newWindowContext;
const currentUrl = parentWindowView.webContents.getURL();
/** Conditions are listed by order of priority
if global.forceNewWindow = true
or regular new-window event
or if in Google Drive app, open Google Docs files internally https://github.com/atomery/webcatalog/issues/800
the next external link request will be opened in new window */
const clickOpenNewWindow = meta.forceNewWindow || disposition === 'new-window' || disposition === 'default';
/** App tries to open external link using JS
nextURL === 'about:blank' but then window will redirect to the external URL
https://github.com/quanglam2807/webcatalog/issues/467#issuecomment-569857721 */
const isExternalLinkUsingJS = nextDomain === null && (disposition === 'foreground-tab' || disposition === 'background-tab');
if (clickOpenNewWindow || isExternalLinkUsingJS) {
// https://gist.github.com/Gvozd/2cec0c8c510a707854e439fb15c561b0
// if 'new-window' is triggered with Cmd+Click
// options is undefined
// https://github.com/atomery/webcatalog/issues/842
const browserViewMetaData: IBrowserViewMetaData = {
isPopup: true,
...(JSON.parse(
decodeURIComponent(sharedWebPreferences?.additionalArguments?.[1]?.replace(MetaDataChannel.browserViewMetaData, '') ?? '{}'),
) as IBrowserViewMetaData),
};
logger.debug('open new window request', {
browserViewMetaData,
disposition,
nextUrl,
nextDomain,
function: 'handleNewWindow',
});
meta.forceNewWindow = false;
const webPreferences = {
additionalArguments: [
`${MetaDataChannel.browserViewMetaData}${WindowNames.view}`,
`${MetaDataChannel.browserViewMetaData}${encodeURIComponent(JSON.stringify(browserViewMetaData))}`,
'--unsafely-disable-devtools-self-xss-warnings',
],
preload: getPreloadPath(),
};
const windowWithBrowserViewState = windowStateKeeper({
file: 'window-state-open-in-new-window.json',
path: SETTINGS_FOLDER,
defaultWidth: windowDimension[WindowNames.main].width,
defaultHeight: windowDimension[WindowNames.main].height,
});
let newOptions: BrowserWindowConstructorOptions = {
x: windowWithBrowserViewState.x,
y: windowWithBrowserViewState.y,
width: windowWithBrowserViewState.width,
height: windowWithBrowserViewState.height,
webPreferences,
autoHideMenuBar: true,
};
if (isExternalLinkUsingJS) {
newOptions = { ...newOptions, show: false };
}
parentWebContents.once('did-create-window', (childWindow) => {
childWindow.setMenuBarVisibility(false);
childWindow.webContents.setWindowOpenHandler((details: Electron.HandlerDetails) => handleNewWindow(details, newWindowContext, childWindow.webContents));
childWindow.webContents.once('will-navigate', async (_event, url) => {
// if the window is used for the current app, then use default behavior
const currentWorkspace = await workspaceService.get(workspace.id);
const appUrl = currentWorkspace && isWikiWorkspace(currentWorkspace) ? currentWorkspace.homeUrl : undefined;
if (appUrl === undefined) {
throw new Error(`Workspace ${workspace.id} not existed, or don't have homeUrl setting`);
}
if (isInternalUrl(url, [appUrl, currentUrl])) {
childWindow.show();
} else {
// if not, open in browser
_event.preventDefault();
void shell.openExternal(url);
childWindow.close();
}
});
windowWithBrowserViewState.manage(childWindow);
const menuService = container.get<IMenuService>(serviceIdentifier.MenuService);
childWindow.webContents.once('dom-ready', async () => {
await menuService.initContextMenuForWindowWebContents(childWindow.webContents).then((unregisterContextMenu) => {
childWindow.webContents.on('destroyed', () => {
unregisterContextMenu();
});
});
});
});
return {
action: 'allow',
overrideBrowserWindowOptions: newOptions,
};
}
return { action: 'allow' };
}