refactor: move some private method out

This commit is contained in:
lin onetwo 2023-12-30 18:48:15 +08:00
parent 9b6d9a83d7
commit 32485a1bee
10 changed files with 658 additions and 554 deletions

View file

@ -0,0 +1,147 @@
import { MENUBAR_ICON_PATH } from '@/constants/paths';
import { isMac } from '@/helpers/system';
import { container } from '@services/container';
import { i18n } from '@services/libs/i18n';
import { logger } from '@services/libs/log';
import { IMenuService } from '@services/menu/interface';
import serviceIdentifier from '@services/serviceIdentifier';
import { BrowserWindow, BrowserWindowConstructorOptions, Menu, nativeImage, Tray } from 'electron';
import windowStateKeeper from 'electron-window-state';
import { debounce, merge as mergeDeep } from 'lodash';
import { Menubar, menubar } from 'menubar';
import { IWindowService } from './interface';
import { WindowNames } from './WindowProperties';
export async function handleAttachToMenuBar(windowConfig: BrowserWindowConstructorOptions, windowWithBrowserViewState: windowStateKeeper.State | undefined): Promise<Menubar> {
const menuService = container.get<IMenuService>(serviceIdentifier.MenuService);
const windowService = container.get<IWindowService>(serviceIdentifier.Window);
// setImage after Tray instance is created to avoid
// "Segmentation fault (core dumped)" bug on Linux
// https://github.com/electron/electron/issues/22137#issuecomment-586105622
// https://github.com/atomery/translatium/issues/164
const tray = new Tray(nativeImage.createEmpty());
// icon template is not supported on Windows & Linux
tray.setImage(MENUBAR_ICON_PATH);
const menuBar = menubar({
index: MAIN_WINDOW_WEBPACK_ENTRY,
tray,
activateWithApp: false,
preloadWindow: true,
tooltip: i18n.t('Menu.TidGiMenuBar'),
browserWindow: mergeDeep(windowConfig, {
show: false,
minHeight: 100,
minWidth: 250,
}),
});
menuBar.on('after-create-window', () => {
if (menuBar.window !== undefined) {
menuBar.window.on('focus', () => {
logger.debug('restore window position');
if (windowWithBrowserViewState === undefined) {
logger.debug('windowWithBrowserViewState is undefined for menuBar');
} else {
if (menuBar.window === undefined) {
logger.debug('menuBar.window is undefined');
} else {
menuBar.window.setPosition(windowWithBrowserViewState.x, windowWithBrowserViewState.y, false);
menuBar.window.setSize(windowWithBrowserViewState.width, windowWithBrowserViewState.height, false);
}
}
const view = menuBar.window?.getBrowserView();
if (view?.webContents !== undefined) {
view.webContents.focus();
}
});
menuBar.window.removeAllListeners('close');
menuBar.window.on('close', (event) => {
event.preventDefault();
menuBar.hideWindow();
});
}
});
// https://github.com/maxogden/menubar/issues/120
menuBar.on('after-hide', () => {
if (isMac) {
menuBar.app.hide();
}
});
// manually save window state https://github.com/mawie81/electron-window-state/issues/64
const debouncedSaveWindowState = debounce(
(event: { sender: BrowserWindow }) => {
windowWithBrowserViewState?.saveState(event.sender);
},
500,
);
// menubar is hide, not close, so not managed by windowStateKeeper, need to save manually
menuBar.window?.on('resize', debouncedSaveWindowState);
menuBar.window?.on('move', debouncedSaveWindowState);
return await new Promise<Menubar>((resolve) => {
menuBar.on('ready', async () => {
// right on tray icon
menuBar.tray.on('right-click', () => {
// TODO: restore updater options here
const contextMenu = Menu.buildFromTemplate([
{
label: i18n.t('ContextMenu.OpenTidGi'),
click: async () => {
await windowService.open(WindowNames.main);
},
},
{
label: i18n.t('ContextMenu.OpenTidGiMenuBar'),
click: async () => {
await menuBar.showWindow();
},
},
{
type: 'separator',
},
{
label: i18n.t('ContextMenu.About'),
click: async () => {
await windowService.open(WindowNames.about);
},
},
{ type: 'separator' },
{
label: i18n.t('ContextMenu.Preferences'),
click: async () => {
await windowService.open(WindowNames.preferences);
},
},
{
label: i18n.t('ContextMenu.Notifications'),
click: async () => {
await windowService.open(WindowNames.notifications);
},
},
{ type: 'separator' },
{
label: i18n.t('ContextMenu.Quit'),
click: () => {
menuBar.app.quit();
},
},
]);
menuBar.tray.popUpContextMenu(contextMenu);
});
// right click on window content
if (menuBar.window?.webContents !== undefined) {
const unregisterContextMenu = await menuService.initContextMenuForWindowWebContents(menuBar.window.webContents);
menuBar.on('after-close', () => {
unregisterContextMenu();
});
}
resolve(menuBar);
});
});
}

View file

@ -0,0 +1,72 @@
/* eslint-disable @typescript-eslint/consistent-type-assertions */
import { container } from '@services/container';
import { IMenuService } from '@services/menu/interface';
import serviceIdentifier from '@services/serviceIdentifier';
import { IThemeService } from '@services/theme/interface';
import { app, BrowserWindow, BrowserWindowConstructorOptions } from 'electron';
import { IWindowOpenConfig, IWindowService } from './interface';
import { WindowMeta, WindowNames } from './WindowProperties';
export async function handleCreateBasicWindow<N extends WindowNames>(
windowName: N,
windowConfig: BrowserWindowConstructorOptions,
windowMeta: WindowMeta[N] = {} as WindowMeta[N],
config?: IWindowOpenConfig<N>,
): Promise<BrowserWindow> {
const menuService = container.get<IMenuService>(serviceIdentifier.MenuService);
const windowService = container.get<IWindowService>(serviceIdentifier.Window);
const newWindow = new BrowserWindow(windowConfig);
const newWindowURL = (windowMeta !== undefined && 'uri' in windowMeta ? windowMeta.uri : undefined) ?? MAIN_WINDOW_WEBPACK_ENTRY;
if (config?.multiple !== true) {
windowService.set(windowName, newWindow);
}
const unregisterContextMenu = await menuService.initContextMenuForWindowWebContents(newWindow.webContents);
newWindow.on('closed', () => {
windowService.set(windowName, undefined);
unregisterContextMenu();
});
let webContentLoadingPromise: Promise<void> | undefined;
if (windowName === WindowNames.main) {
// handle window show and Webview/browserView show
webContentLoadingPromise = new Promise<void>((resolve, reject) => {
newWindow.once('ready-to-show', () => {
const mainWindow = windowService.get(WindowNames.main);
if (mainWindow === undefined) {
reject(new Error("Main window is undefined in newWindow.once('ready-to-show'"));
return;
}
const { wasOpenedAsHidden } = app.getLoginItemSettings();
if (!wasOpenedAsHidden) {
mainWindow.show();
}
// ensure redux is loaded first
// if not, redux might not be able catch changes sent from ipcMain
if (!mainWindow.webContents.isLoading()) {
resolve();
return;
}
mainWindow.webContents.once('did-stop-loading', () => {
resolve();
});
});
});
}
await updateWindowBackground(newWindow);
// Not loading main window (like sidebar and background) here. Only load wiki in browserView in the secondary window.
const isWindowToLoadURL = windowName !== WindowNames.secondary;
if (isWindowToLoadURL) {
// This loading will wait for a while
await newWindow.loadURL(newWindowURL);
}
await webContentLoadingPromise;
return newWindow;
}
async function updateWindowBackground(newWindow: BrowserWindow): Promise<void> {
const themeService = container.get<IThemeService>(serviceIdentifier.ThemeService);
if (await themeService.shouldUseDarkColors()) {
newWindow.setBackgroundColor('#000000');
}
}

View file

@ -1,14 +1,13 @@
/* eslint-disable @typescript-eslint/require-await */
/* eslint-disable @typescript-eslint/no-misused-promises */
/* eslint-disable @typescript-eslint/consistent-type-assertions */
import { app, BrowserWindow, BrowserWindowConstructorOptions, ipcMain, Menu, nativeImage, Tray } from 'electron';
import { app, BrowserWindow, BrowserWindowConstructorOptions } from 'electron';
import windowStateKeeper, { State as windowStateKeeperState } from 'electron-window-state';
import { injectable } from 'inversify';
import mergeDeep from 'lodash/merge';
import { Menubar, menubar } from 'menubar';
import { Menubar } from 'menubar';
import serviceIdentifier from '@services/serviceIdentifier';
import { IBrowserViewMetaData, windowDimension, WindowMeta, WindowNames } from '@services/windows/WindowProperties';
import { windowDimension, WindowMeta, WindowNames } from '@services/windows/WindowProperties';
import { Channels, MetaDataChannel, ViewChannel, WindowChannel } from '@/constants/channels';
import type { IMenuService } from '@services/menu/interface';
@ -18,22 +17,19 @@ import type { IWorkspaceViewService } from '@services/workspacesView/interface';
import { SETTINGS_FOLDER } from '@/constants/appPaths';
import { isTest } from '@/constants/environment';
import { MENUBAR_ICON_PATH } from '@/constants/paths';
import { getDefaultTidGiUrl } from '@/constants/urls';
import { isMac } from '@/helpers/system';
import { lazyInject } from '@services/container';
import getFromRenderer from '@services/libs/getFromRenderer';
import getViewBounds from '@services/libs/getViewBounds';
import { i18n } from '@services/libs/i18n';
import { logger } from '@services/libs/log';
import { IThemeService } from '@services/theme/interface';
import { debounce } from 'lodash';
import { handleAttachToMenuBar } from './handleAttachToMenuBar';
import { handleCreateBasicWindow } from './handleCreateBasicWindow';
import { IWindowOpenConfig, IWindowService } from './interface';
import { registerBrowserViewWindowListeners } from './registerBrowserViewWindowListeners';
import { registerMenu } from './registerMenu';
@injectable()
export class Window implements IWindowService {
// TODO: use WeakMap instead
private windows = {} as Partial<Record<WindowNames, BrowserWindow | undefined>>;
private readonly windows = new Map<WindowNames, BrowserWindow>();
private windowMeta = {} as Partial<WindowMeta>;
/** menubar version of main window, if user set openInMenubar to true in preferences */
private mainWindowMenuBar?: Menubar;
@ -54,7 +50,7 @@ export class Window implements IWindowService {
private readonly themeService!: IThemeService;
constructor() {
void this.registerMenu();
void registerMenu();
}
public async findInPage(text: string, forward?: boolean, windowName: WindowNames = WindowNames.main): Promise<void> {
@ -94,7 +90,15 @@ export class Window implements IWindowService {
if (windowName === WindowNames.menuBar) {
return this.mainWindowMenuBar?.window;
}
return this.windows[windowName];
return this.windows.get(windowName);
}
public set(windowName: WindowNames = WindowNames.main, win: BrowserWindow | undefined): void {
if (win === undefined) {
this.windows.delete(windowName);
} else {
this.windows.set(windowName, win);
}
}
public async close(windowName: WindowNames): Promise<void> {
@ -182,15 +186,15 @@ export class Window implements IWindowService {
};
let newWindow: BrowserWindow;
if (windowName === WindowNames.menuBar) {
this.mainWindowMenuBar = await this.handleAttachToMenuBar(windowConfig, windowWithBrowserViewState);
this.mainWindowMenuBar = await handleAttachToMenuBar(windowConfig, windowWithBrowserViewState);
if (this.mainWindowMenuBar.window === undefined) {
throw new Error('MenuBar failed to create window.');
}
newWindow = this.mainWindowMenuBar.window;
} else {
newWindow = await this.handleCreateBasicWindow(windowName, windowConfig, meta, config);
newWindow = await handleCreateBasicWindow(windowName, windowConfig, meta, config);
if (isWindowWithBrowserView) {
this.registerBrowserViewWindowListeners(newWindow, windowName);
registerBrowserViewWindowListeners(newWindow, windowName);
// calling this to redundantly setBounds BrowserView
// after the UI is fully loaded
// if not, BrowserView mouseover event won't work correctly
@ -206,121 +210,8 @@ export class Window implements IWindowService {
}
}
private async handleCreateBasicWindow<N extends WindowNames>(
windowName: N,
windowConfig: BrowserWindowConstructorOptions,
windowMeta: WindowMeta[N] = {} as WindowMeta[N],
config?: IWindowOpenConfig<N>,
): Promise<BrowserWindow> {
const newWindow = new BrowserWindow(windowConfig);
const newWindowURL = (windowMeta !== undefined && 'uri' in windowMeta ? windowMeta.uri : undefined) ?? MAIN_WINDOW_WEBPACK_ENTRY;
if (config?.multiple !== true) {
this.windows[windowName] = newWindow;
}
const unregisterContextMenu = await this.menuService.initContextMenuForWindowWebContents(newWindow.webContents);
newWindow.on('closed', () => {
this.windows[windowName] = undefined;
unregisterContextMenu();
});
let webContentLoadingPromise: Promise<void> | undefined;
if (windowName === WindowNames.main) {
// handle window show and Webview/browserView show
webContentLoadingPromise = new Promise<void>((resolve, reject) => {
newWindow.once('ready-to-show', async () => {
const mainWindow = this.get(WindowNames.main);
if (mainWindow === undefined) {
reject(new Error("Main window is undefined in newWindow.once('ready-to-show'"));
return;
}
const { wasOpenedAsHidden } = app.getLoginItemSettings();
if (!wasOpenedAsHidden) {
mainWindow.show();
}
// ensure redux is loaded first
// if not, redux might not be able catch changes sent from ipcMain
if (!mainWindow.webContents.isLoading()) {
resolve();
return;
}
mainWindow.webContents.once('did-stop-loading', () => {
resolve();
});
});
});
}
await this.updateWindowBackground(newWindow);
// Not loading main window (like sidebar and background) here. Only load wiki in browserView in the secondary window.
const isWindowToLoadURL = windowName !== WindowNames.secondary;
if (isWindowToLoadURL) {
// This loading will wait for a while
await newWindow.loadURL(newWindowURL);
}
await webContentLoadingPromise;
return newWindow;
}
private registerBrowserViewWindowListeners(newWindow: BrowserWindow, windowName: WindowNames): void {
// Enable swipe to navigate
void this.preferenceService.get('swipeToNavigate').then((swipeToNavigate) => {
if (swipeToNavigate) {
if (newWindow === undefined) return;
newWindow.on('swipe', (_event, direction) => {
const view = newWindow?.getBrowserView();
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (view) {
if (direction === 'left') {
view.webContents.goBack();
} else if (direction === 'right') {
view.webContents.goForward();
}
}
});
}
});
// Hide window instead closing on macos
newWindow.on('close', async (event) => {
const windowMeta = await this.getWindowMeta(WindowNames.main);
if (newWindow === undefined) return;
if (isMac && windowMeta?.forceClose !== true) {
event.preventDefault();
// https://github.com/electron/electron/issues/6033#issuecomment-242023295
if (newWindow.isFullScreen()) {
newWindow.once('leave-full-screen', () => {
if (newWindow !== undefined) {
newWindow.hide();
}
});
newWindow.setFullScreen(false);
} else {
newWindow.hide();
}
}
});
newWindow.on('focus', () => {
if (newWindow === undefined) return;
const view = newWindow?.getBrowserView();
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
view?.webContents?.focus();
});
newWindow.on('enter-full-screen', async () => {
const mainWindow = this.get(windowName);
if (mainWindow === undefined) return;
mainWindow?.webContents?.send?.('is-fullscreen-updated', true);
await this.workspaceViewService.realignActiveWorkspace();
});
newWindow.on('leave-full-screen', async () => {
const mainWindow = this.get(windowName);
if (mainWindow === undefined) return;
mainWindow?.webContents?.send?.('is-fullscreen-updated', false);
await this.workspaceViewService.realignActiveWorkspace();
});
}
public async isFullScreen(windowName = WindowNames.main): Promise<boolean | undefined> {
return this.windows[windowName]?.isFullScreen();
return this.windows.get(windowName)?.isFullScreen();
}
public async setWindowMeta<N extends WindowNames>(windowName: N, meta: WindowMeta[N]): Promise<void> {
@ -411,253 +302,6 @@ export class Window implements IWindowService {
}
}
private async registerMenu(): Promise<void> {
await this.menuService.insertMenu('Window', [
// `role: 'zoom'` is only supported on macOS
isMac
? {
role: 'zoom',
}
: {
label: 'Zoom',
click: async () => {
await this.maximize();
},
},
{ role: 'resetZoom' },
{ role: 'togglefullscreen' },
{ role: 'close' },
]);
await this.menuService.insertMenu(
'View',
[
{
label: () => i18n.t('Menu.Find'),
accelerator: 'CmdOrCtrl+F',
click: async () => {
const mainWindow = this.get(WindowNames.main);
if (mainWindow !== undefined) {
mainWindow.webContents.focus();
mainWindow.webContents.send(WindowChannel.openFindInPage);
const contentSize = mainWindow.getContentSize();
const view = mainWindow.getBrowserView();
view?.setBounds(await getViewBounds(contentSize as [number, number], { findInPage: true }));
}
},
enabled: async () => (await this.workspaceService.countWorkspaces()) > 0,
},
{
label: () => i18n.t('Menu.FindNext'),
accelerator: 'CmdOrCtrl+G',
click: () => {
const mainWindow = this.get(WindowNames.main);
mainWindow?.webContents?.send('request-back-find-in-page', true);
},
enabled: async () => (await this.workspaceService.countWorkspaces()) > 0,
},
{
label: () => i18n.t('Menu.FindPrevious'),
accelerator: 'Shift+CmdOrCtrl+G',
click: () => {
const mainWindow = this.get(WindowNames.main);
mainWindow?.webContents?.send('request-back-find-in-page', false);
},
enabled: async () => (await this.workspaceService.countWorkspaces()) > 0,
},
{
label: () => `${i18n.t('Preference.AlwaysOnTop')} (${i18n.t('Preference.RequireRestart')})`,
checked: async () => await this.preferenceService.get('alwaysOnTop'),
click: async () => {
const alwaysOnTop = await this.preferenceService.get('alwaysOnTop');
await this.preferenceService.set('alwaysOnTop', !alwaysOnTop);
await this.requestRestart();
},
},
],
// eslint-disable-next-line unicorn/no-null
null,
true,
);
await this.menuService.insertMenu('History', [
{
label: () => i18n.t('Menu.Home'),
accelerator: 'Shift+CmdOrCtrl+H',
click: async () => {
await this.goHome();
},
enabled: async () => (await this.workspaceService.countWorkspaces()) > 0,
},
{
label: () => i18n.t('ContextMenu.Back'),
accelerator: 'CmdOrCtrl+[',
click: async (_menuItem, browserWindow) => {
// if back is called in popup window
// navigate in the popup window instead
if (browserWindow !== undefined) {
// TODO: test if we really can get this isPopup value
const { isPopup = false } = await getFromRenderer<IBrowserViewMetaData>(MetaDataChannel.getViewMetaData, browserWindow);
await this.goBack(isPopup ? WindowNames.menuBar : WindowNames.main);
}
ipcMain.emit('request-go-back');
},
enabled: async () => (await this.workspaceService.countWorkspaces()) > 0,
},
{
label: () => i18n.t('ContextMenu.Forward'),
accelerator: 'CmdOrCtrl+]',
click: async (_menuItem, browserWindow) => {
// if back is called in popup window
// navigate in the popup window instead
if (browserWindow !== undefined) {
const { isPopup = false } = await getFromRenderer<IBrowserViewMetaData>(MetaDataChannel.getViewMetaData, browserWindow);
await this.goForward(isPopup ? WindowNames.menuBar : WindowNames.main);
}
ipcMain.emit('request-go-forward');
},
enabled: async () => (await this.workspaceService.countWorkspaces()) > 0,
},
]);
}
private async handleAttachToMenuBar(windowConfig: BrowserWindowConstructorOptions, windowWithBrowserViewState: windowStateKeeper.State | undefined): Promise<Menubar> {
// setImage after Tray instance is created to avoid
// "Segmentation fault (core dumped)" bug on Linux
// https://github.com/electron/electron/issues/22137#issuecomment-586105622
// https://github.com/atomery/translatium/issues/164
const tray = new Tray(nativeImage.createEmpty());
// icon template is not supported on Windows & Linux
tray.setImage(MENUBAR_ICON_PATH);
const menuBar = menubar({
index: MAIN_WINDOW_WEBPACK_ENTRY,
tray,
activateWithApp: false,
preloadWindow: true,
tooltip: i18n.t('Menu.TidGiMenuBar'),
browserWindow: mergeDeep(windowConfig, {
show: false,
minHeight: 100,
minWidth: 250,
}),
});
menuBar.on('after-create-window', () => {
if (menuBar.window !== undefined) {
menuBar.window.on('focus', () => {
logger.debug('restore window position');
if (windowWithBrowserViewState === undefined) {
logger.debug('windowWithBrowserViewState is undefined for menuBar');
} else {
if (menuBar.window === undefined) {
logger.debug('menuBar.window is undefined');
} else {
menuBar.window.setPosition(windowWithBrowserViewState.x, windowWithBrowserViewState.y, false);
menuBar.window.setSize(windowWithBrowserViewState.width, windowWithBrowserViewState.height, false);
}
}
const view = menuBar.window?.getBrowserView();
if (view?.webContents !== undefined) {
view.webContents.focus();
}
});
menuBar.window.removeAllListeners('close');
menuBar.window.on('close', (event) => {
event.preventDefault();
menuBar.hideWindow();
});
}
});
// https://github.com/maxogden/menubar/issues/120
menuBar.on('after-hide', () => {
if (isMac) {
menuBar.app.hide();
}
});
// manually save window state https://github.com/mawie81/electron-window-state/issues/64
const debouncedSaveWindowState = debounce(
(event: { sender: BrowserWindow }) => {
windowWithBrowserViewState?.saveState(event.sender);
},
500,
);
// menubar is hide, not close, so not managed by windowStateKeeper, need to save manually
menuBar.window?.on('resize', debouncedSaveWindowState);
menuBar.window?.on('move', debouncedSaveWindowState);
return await new Promise<Menubar>((resolve) => {
menuBar.on('ready', async () => {
// right on tray icon
menuBar.tray.on('right-click', () => {
// TODO: restore updater options here
const contextMenu = Menu.buildFromTemplate([
{
label: i18n.t('ContextMenu.OpenTidGi'),
click: async () => {
await this.open(WindowNames.main);
},
},
{
label: i18n.t('ContextMenu.OpenTidGiMenuBar'),
click: async () => {
await menuBar.showWindow();
},
},
{
type: 'separator',
},
{
label: i18n.t('ContextMenu.About'),
click: async () => {
await this.open(WindowNames.about);
},
},
{ type: 'separator' },
{
label: i18n.t('ContextMenu.Preferences'),
click: async () => {
await this.open(WindowNames.preferences);
},
},
{
label: i18n.t('ContextMenu.Notifications'),
click: async () => {
await this.open(WindowNames.notifications);
},
},
{ type: 'separator' },
{
label: i18n.t('ContextMenu.Quit'),
click: () => {
menuBar.app.quit();
},
},
]);
menuBar.tray.popUpContextMenu(contextMenu);
});
// right click on window content
if (menuBar.window?.webContents !== undefined) {
const unregisterContextMenu = await this.menuService.initContextMenuForWindowWebContents(menuBar.window.webContents);
menuBar.on('after-close', () => {
unregisterContextMenu();
});
}
resolve(menuBar);
});
});
}
private async updateWindowBackground(newWindow: BrowserWindow): Promise<void> {
if (await this.themeService.shouldUseDarkColors()) {
newWindow.setBackgroundColor('#000000');
}
}
public async maximize(): Promise<void> {
const mainWindow = this.get(WindowNames.main);
if (mainWindow !== undefined) {

View file

@ -24,9 +24,9 @@ export interface IWindowService {
/** get window, this should not be called in renderer side */
get(windowName: WindowNames): BrowserWindow | undefined;
getWindowMeta<N extends WindowNames>(windowName: N): Promise<WindowMeta[N] | undefined>;
goBack(windowName: WindowNames): Promise<void>;
goForward(windowName: WindowNames): Promise<void>;
goHome(windowName: WindowNames): Promise<void>;
goBack(windowName?: WindowNames): Promise<void>;
goForward(windowName?: WindowNames): Promise<void>;
goHome(windowName?: WindowNames): Promise<void>;
isFullScreen(windowName?: WindowNames): Promise<boolean | undefined>;
isMenubarOpen(): Promise<boolean>;
loadURL(windowName: WindowNames, newUrl?: string): Promise<void>;
@ -41,6 +41,8 @@ export interface IWindowService {
reload(windowName: WindowNames): Promise<void>;
requestRestart(): Promise<void>;
sendToAllWindows: (channel: Channels, ...arguments_: unknown[]) => Promise<void>;
/** set window or delete window object by passing undefined (will not close it, only remove reference), this should not be called in renderer side */
set(windowName: WindowNames, win: BrowserWindow | undefined): void;
setWindowMeta<N extends WindowNames>(windowName: N, meta?: WindowMeta[N]): Promise<void>;
stopFindInPage(close?: boolean | undefined, windowName?: WindowNames): Promise<void>;
updateWindowMeta<N extends WindowNames>(windowName: N, meta?: WindowMeta[N]): Promise<void>;

View file

@ -0,0 +1,71 @@
import { isMac } from '@/helpers/system';
import { container } from '@services/container';
import { IPreferenceService } from '@services/preferences/interface';
import serviceIdentifier from '@services/serviceIdentifier';
import { IWorkspaceViewService } from '@services/workspacesView/interface';
import { BrowserWindow } from 'electron';
import { IWindowService } from './interface';
import { WindowNames } from './WindowProperties';
export function registerBrowserViewWindowListeners(newWindow: BrowserWindow, windowName: WindowNames): void {
const preferenceService = container.get<IPreferenceService>(serviceIdentifier.Preference);
const windowService = container.get<IWindowService>(serviceIdentifier.Window);
const workspaceViewService = container.get<IWorkspaceViewService>(serviceIdentifier.WorkspaceView);
// Enable swipe to navigate
void preferenceService.get('swipeToNavigate').then((swipeToNavigate) => {
if (swipeToNavigate) {
if (newWindow === undefined) return;
newWindow.on('swipe', (_event, direction) => {
const view = newWindow?.getBrowserView();
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (view) {
if (direction === 'left') {
view.webContents.goBack();
} else if (direction === 'right') {
view.webContents.goForward();
}
}
});
}
});
// Hide window instead closing on macos
newWindow.on('close', async (event) => {
const windowMeta = await windowService.getWindowMeta(WindowNames.main);
if (newWindow === undefined) return;
if (isMac && windowMeta?.forceClose !== true) {
event.preventDefault();
// https://github.com/electron/electron/issues/6033#issuecomment-242023295
if (newWindow.isFullScreen()) {
newWindow.once('leave-full-screen', () => {
if (newWindow !== undefined) {
newWindow.hide();
}
});
newWindow.setFullScreen(false);
} else {
newWindow.hide();
}
}
});
newWindow.on('focus', () => {
if (newWindow === undefined) return;
const view = newWindow?.getBrowserView();
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
view?.webContents?.focus();
});
newWindow.on('enter-full-screen', async () => {
const mainWindow = windowService.get(windowName);
if (mainWindow === undefined) return;
mainWindow?.webContents?.send?.('is-fullscreen-updated', true);
await workspaceViewService.realignActiveWorkspace();
});
newWindow.on('leave-full-screen', async () => {
const mainWindow = windowService.get(windowName);
if (mainWindow === undefined) return;
mainWindow?.webContents?.send?.('is-fullscreen-updated', false);
await workspaceViewService.realignActiveWorkspace();
});
}

View file

@ -0,0 +1,128 @@
import { MetaDataChannel, WindowChannel } from '@/constants/channels';
import { isMac } from '@/helpers/system';
import { container } from '@services/container';
import getFromRenderer from '@services/libs/getFromRenderer';
import getViewBounds from '@services/libs/getViewBounds';
import { i18n } from '@services/libs/i18n';
import { IMenuService } from '@services/menu/interface';
import { IPreferenceService } from '@services/preferences/interface';
import serviceIdentifier from '@services/serviceIdentifier';
import { IWorkspaceService } from '@services/workspaces/interface';
import { ipcMain } from 'electron';
import { IWindowService } from './interface';
import { IBrowserViewMetaData, WindowNames } from './WindowProperties';
export async function registerMenu(): Promise<void> {
const menuService = container.get<IMenuService>(serviceIdentifier.MenuService);
const windowService = container.get<IWindowService>(serviceIdentifier.Window);
const workspaceService = container.get<IWorkspaceService>(serviceIdentifier.Workspace);
const preferenceService = container.get<IPreferenceService>(serviceIdentifier.Preference);
await menuService.insertMenu('Window', [
// `role: 'zoom'` is only supported on macOS
isMac
? {
role: 'zoom',
}
: {
label: 'Zoom',
click: async () => {
await windowService.maximize();
},
},
{ role: 'resetZoom' },
{ role: 'togglefullscreen' },
{ role: 'close' },
]);
await menuService.insertMenu(
'View',
[
{
label: () => i18n.t('Menu.Find'),
accelerator: 'CmdOrCtrl+F',
click: async () => {
const mainWindow = windowService.get(WindowNames.main);
if (mainWindow !== undefined) {
mainWindow.webContents.focus();
mainWindow.webContents.send(WindowChannel.openFindInPage);
const contentSize = mainWindow.getContentSize();
const view = mainWindow.getBrowserView();
view?.setBounds(await getViewBounds(contentSize as [number, number], { findInPage: true }));
}
},
enabled: async () => (await workspaceService.countWorkspaces()) > 0,
},
{
label: () => i18n.t('Menu.FindNext'),
accelerator: 'CmdOrCtrl+G',
click: () => {
const mainWindow = windowService.get(WindowNames.main);
mainWindow?.webContents?.send('request-back-find-in-page', true);
},
enabled: async () => (await workspaceService.countWorkspaces()) > 0,
},
{
label: () => i18n.t('Menu.FindPrevious'),
accelerator: 'Shift+CmdOrCtrl+G',
click: () => {
const mainWindow = windowService.get(WindowNames.main);
mainWindow?.webContents?.send('request-back-find-in-page', false);
},
enabled: async () => (await workspaceService.countWorkspaces()) > 0,
},
{
label: () => `${i18n.t('Preference.AlwaysOnTop')} (${i18n.t('Preference.RequireRestart')})`,
checked: async () => await preferenceService.get('alwaysOnTop'),
click: async () => {
const alwaysOnTop = await preferenceService.get('alwaysOnTop');
await preferenceService.set('alwaysOnTop', !alwaysOnTop);
await windowService.requestRestart();
},
},
],
// eslint-disable-next-line unicorn/no-null
null,
true,
);
await menuService.insertMenu('History', [
{
label: () => i18n.t('Menu.Home'),
accelerator: 'Shift+CmdOrCtrl+H',
click: async () => {
await windowService.goHome();
},
enabled: async () => (await workspaceService.countWorkspaces()) > 0,
},
{
label: () => i18n.t('ContextMenu.Back'),
accelerator: 'CmdOrCtrl+[',
click: async (_menuItem, browserWindow) => {
// if back is called in popup window
// navigate in the popup window instead
if (browserWindow !== undefined) {
// TODO: test if we really can get this isPopup value
const { isPopup = false } = await getFromRenderer<IBrowserViewMetaData>(MetaDataChannel.getViewMetaData, browserWindow);
await windowService.goBack(isPopup ? WindowNames.menuBar : WindowNames.main);
}
ipcMain.emit('request-go-back');
},
enabled: async () => (await workspaceService.countWorkspaces()) > 0,
},
{
label: () => i18n.t('ContextMenu.Forward'),
accelerator: 'CmdOrCtrl+]',
click: async (_menuItem, browserWindow) => {
// if back is called in popup window
// navigate in the popup window instead
if (browserWindow !== undefined) {
const { isPopup = false } = await getFromRenderer<IBrowserViewMetaData>(MetaDataChannel.getViewMetaData, browserWindow);
await windowService.goForward(isPopup ? WindowNames.menuBar : WindowNames.main);
}
ipcMain.emit('request-go-forward');
},
enabled: async () => (await workspaceService.countWorkspaces()) > 0,
},
]);
}

View file

@ -31,6 +31,7 @@ import { WindowNames } from '@services/windows/WindowProperties';
import type { IWorkspaceViewService } from '@services/workspacesView/interface';
import { debouncedSetSettingFile } from './debouncedSetSettingFile';
import type { INewWorkspaceConfig, IWorkspace, IWorkspaceMetaData, IWorkspaceService, IWorkspaceWithMetadata } from './interface';
import { registerMenu } from './registerMenu';
import { workspaceSorter } from './utils';
@injectable()
@ -67,7 +68,7 @@ export class Workspace implements IWorkspaceService {
constructor() {
this.workspaces = this.getInitWorkspacesForCache();
void this.registerMenu();
void registerMenu();
this.workspaces$ = new BehaviorSubject<Record<string, IWorkspaceWithMetadata>>(this.getWorkspacesWithMetadata());
}
@ -79,71 +80,6 @@ export class Workspace implements IWorkspaceService {
this.workspaces$.next(this.getWorkspacesWithMetadata());
}
private async registerMenu(): Promise<void> {
/* eslint-disable @typescript-eslint/no-misused-promises */
await this.menuService.insertMenu('Workspaces', [
{
label: () => i18n.t('Menu.SelectNextWorkspace'),
click: async () => {
const currentActiveWorkspace = await this.getActiveWorkspace();
if (currentActiveWorkspace === undefined) return;
const nextWorkspace = await this.getNextWorkspace(currentActiveWorkspace.id);
if (nextWorkspace === undefined) return;
await this.workspaceViewService.setActiveWorkspaceView(nextWorkspace.id);
},
accelerator: 'CmdOrCtrl+Shift+]',
enabled: async () => (await this.countWorkspaces()) > 1,
},
{
label: () => i18n.t('Menu.SelectPreviousWorkspace'),
click: async () => {
const currentActiveWorkspace = await this.getActiveWorkspace();
if (currentActiveWorkspace === undefined) return;
const previousWorkspace = await this.getPreviousWorkspace(currentActiveWorkspace.id);
if (previousWorkspace === undefined) return;
await this.workspaceViewService.setActiveWorkspaceView(previousWorkspace.id);
},
accelerator: 'CmdOrCtrl+Shift+[',
enabled: async () => (await this.countWorkspaces()) > 1,
},
{ type: 'separator' },
{
label: () => i18n.t('WorkspaceSelector.EditCurrentWorkspace'),
click: async () => {
const currentActiveWorkspace = await this.getActiveWorkspace();
if (currentActiveWorkspace === undefined) return;
await this.windowService.open(WindowNames.editWorkspace, { workspaceID: currentActiveWorkspace.id });
},
enabled: async () => (await this.countWorkspaces()) > 0,
},
{
label: () => i18n.t('WorkspaceSelector.ReloadCurrentWorkspace'),
click: async () => {
const currentActiveWorkspace = await this.getActiveWorkspace();
if (currentActiveWorkspace === undefined) return;
await this.viewService.reloadActiveBrowserView();
},
enabled: async () => (await this.countWorkspaces()) > 0,
},
{
label: () => i18n.t('WorkspaceSelector.RemoveCurrentWorkspace'),
click: async () => {
const currentActiveWorkspace = await this.getActiveWorkspace();
if (currentActiveWorkspace === undefined) return;
await this.wikiGitWorkspaceService.removeWorkspace(currentActiveWorkspace.id);
},
enabled: async () => (await this.countWorkspaces()) > 0,
},
{ type: 'separator' },
{
label: () => i18n.t('AddWorkspace.AddWorkspace'),
click: async () => {
await this.windowService.open(WindowNames.addWorkspace);
},
},
]);
}
/**
* Update items like "activate workspace1" or "open devtool in workspace1" in the menu
*/

View file

@ -0,0 +1,82 @@
import { container } from '@services/container';
import { i18n } from '@services/libs/i18n';
import { IMenuService } from '@services/menu/interface';
import serviceIdentifier from '@services/serviceIdentifier';
import { IViewService } from '@services/view/interface';
import { IWikiGitWorkspaceService } from '@services/wikiGitWorkspace/interface';
import { IWindowService } from '@services/windows/interface';
import { WindowNames } from '@services/windows/WindowProperties';
import { IWorkspaceViewService } from '@services/workspacesView/interface';
import { IWorkspaceService } from './interface';
export async function registerMenu(): Promise<void> {
const menuService = container.get<IMenuService>(serviceIdentifier.MenuService);
const windowService = container.get<IWindowService>(serviceIdentifier.Window);
const viewService = container.get<IViewService>(serviceIdentifier.View);
const workspaceService = container.get<IWorkspaceService>(serviceIdentifier.Workspace);
const wikiGitWorkspaceService = container.get<IWikiGitWorkspaceService>(serviceIdentifier.WikiGitWorkspace);
const workspaceViewService = container.get<IWorkspaceViewService>(serviceIdentifier.WorkspaceView);
/* eslint-disable @typescript-eslint/no-misused-promises */
await menuService.insertMenu('Workspaces', [
{
label: () => i18n.t('Menu.SelectNextWorkspace'),
click: async () => {
const currentActiveWorkspace = await workspaceService.getActiveWorkspace();
if (currentActiveWorkspace === undefined) return;
const nextWorkspace = await workspaceService.getNextWorkspace(currentActiveWorkspace.id);
if (nextWorkspace === undefined) return;
await workspaceViewService.setActiveWorkspaceView(nextWorkspace.id);
},
accelerator: 'CmdOrCtrl+Shift+]',
enabled: async () => (await workspaceService.countWorkspaces()) > 1,
},
{
label: () => i18n.t('Menu.SelectPreviousWorkspace'),
click: async () => {
const currentActiveWorkspace = await workspaceService.getActiveWorkspace();
if (currentActiveWorkspace === undefined) return;
const previousWorkspace = await workspaceService.getPreviousWorkspace(currentActiveWorkspace.id);
if (previousWorkspace === undefined) return;
await workspaceViewService.setActiveWorkspaceView(previousWorkspace.id);
},
accelerator: 'CmdOrCtrl+Shift+[',
enabled: async () => (await workspaceService.countWorkspaces()) > 1,
},
{ type: 'separator' },
{
label: () => i18n.t('WorkspaceSelector.EditCurrentWorkspace'),
click: async () => {
const currentActiveWorkspace = await workspaceService.getActiveWorkspace();
if (currentActiveWorkspace === undefined) return;
await windowService.open(WindowNames.editWorkspace, { workspaceID: currentActiveWorkspace.id });
},
enabled: async () => (await workspaceService.countWorkspaces()) > 0,
},
{
label: () => i18n.t('WorkspaceSelector.ReloadCurrentWorkspace'),
click: async () => {
const currentActiveWorkspace = await workspaceService.getActiveWorkspace();
if (currentActiveWorkspace === undefined) return;
await viewService.reloadActiveBrowserView();
},
enabled: async () => (await workspaceService.countWorkspaces()) > 0,
},
{
label: () => i18n.t('WorkspaceSelector.RemoveCurrentWorkspace'),
click: async () => {
const currentActiveWorkspace = await workspaceService.getActiveWorkspace();
if (currentActiveWorkspace === undefined) return;
await wikiGitWorkspaceService.removeWorkspace(currentActiveWorkspace.id);
},
enabled: async () => (await workspaceService.countWorkspaces()) > 0,
},
{ type: 'separator' },
{
label: () => i18n.t('AddWorkspace.AddWorkspace'),
click: async () => {
await windowService.open(WindowNames.addWorkspace);
},
},
]);
}

View file

@ -2,19 +2,16 @@
/* eslint-disable @typescript-eslint/require-await */
/* eslint-disable unicorn/consistent-destructuring */
import { mapSeries } from 'bluebird';
import { app, clipboard, dialog, session } from 'electron';
import { app, dialog, session } from 'electron';
import { injectable } from 'inversify';
import { DEFAULT_DOWNLOADS_PATH } from '@/constants/appPaths';
import { MetaDataChannel, WikiChannel } from '@/constants/channels';
import { isHtmlWiki, wikiHtmlExtensions } from '@/constants/fileNames';
import { WikiChannel } from '@/constants/channels';
import { tiddlywikiLanguagesMap } from '@/constants/languages';
import { WikiCreationMethod } from '@/constants/wikiCreation';
import type { IAuthenticationService } from '@services/auth/interface';
import { lazyInject } from '@services/container';
import { IDatabaseService } from '@services/database/interface';
import type { IGitService } from '@services/git/interface';
import getFromRenderer from '@services/libs/getFromRenderer';
import { i18n } from '@services/libs/i18n';
import { logger } from '@services/libs/log';
import type { IMenuService } from '@services/menu/interface';
@ -25,11 +22,11 @@ import { SupportedStorageServices } from '@services/types';
import type { IViewService } from '@services/view/interface';
import type { IWikiService } from '@services/wiki/interface';
import type { IWindowService } from '@services/windows/interface';
import { IBrowserViewMetaData, WindowNames } from '@services/windows/WindowProperties';
import { WindowNames } from '@services/windows/WindowProperties';
import type { IWorkspace, IWorkspaceService } from '@services/workspaces/interface';
import { CancelError as DownloadCancelError, download } from 'electron-dl';
import path from 'path';
import type { IInitializeWorkspaceOptions, IWorkspaceViewService } from './interface';
import { registerMenu } from './registerMenu';
@injectable()
export class WorkspaceView implements IWorkspaceViewService {
@ -67,7 +64,7 @@ export class WorkspaceView implements IWorkspaceViewService {
private readonly nativeService!: INativeService;
constructor() {
void this.registerMenu();
void registerMenu();
}
public async initializeAllWorkspaceView(): Promise<void> {
@ -252,104 +249,6 @@ export class WorkspaceView implements IWorkspaceViewService {
}
}
private async registerMenu(): Promise<void> {
const hasActiveWorkspaces = async (): Promise<boolean> => (await this.workspaceService.getActiveWorkspace()) !== undefined;
await this.menuService.insertMenu('Workspaces', [
{
label: () => i18n.t('Menu.DeveloperToolsActiveWorkspace'),
accelerator: 'CmdOrCtrl+Option+I',
click: async () => (await this.viewService.getActiveBrowserView())?.webContents?.openDevTools?.({ mode: 'detach' }),
enabled: hasActiveWorkspaces,
},
]);
await this.menuService.insertMenu('Wiki', [
{
label: () => i18n.t('Menu.PrintPage'),
click: async () => {
try {
const browserView = await this.viewService.getActiveBrowserView();
const win = this.windowService.get(WindowNames.main);
logger.info(
`print page, browserView printToPDF method is ${browserView?.webContents?.printToPDF === undefined ? 'undefined' : 'define'}, win is ${
win === undefined ? 'undefined' : 'define'
}`,
);
if (browserView === undefined || win === undefined) {
return;
}
const pdfBuffer = await browserView?.webContents?.printToPDF({
generateTaggedPDF: true,
});
// turn buffer to data uri
const dataUri = `data:application/pdf;base64,${pdfBuffer?.toString('base64')}`;
await download(win, dataUri, { filename: 'wiki.pdf', overwrite: false });
logger.info(`print page done`);
} catch (error) {
if (error instanceof DownloadCancelError) {
logger.debug('item.cancel() was called');
} else {
logger.error(`print page error: ${(error as Error).message}`, error);
}
}
},
enabled: hasActiveWorkspaces,
},
// TODO: get active tiddler title
// {
// label: () => i18n.t('Menu.PrintActiveTiddler'),
// accelerator: 'CmdOrCtrl+Alt+Shift+P',
// click: async () => {
// await this.printTiddler(title);
// },
// enabled: hasActiveWorkspaces,
// },
{
label: () => i18n.t('Menu.ExportWholeWikiHTML'),
click: async () => {
const activeWorkspace = await this.workspaceService.getActiveWorkspace();
if (activeWorkspace === undefined) {
logger.error('Can not export whole wiki, activeWorkspace is undefined');
return;
}
const pathOfNewHTML = await this.nativeService.pickDirectory(DEFAULT_DOWNLOADS_PATH, {
allowOpenFile: true,
filters: [{ name: 'HTML', extensions: wikiHtmlExtensions }],
});
if (pathOfNewHTML.length > 0) {
const fileName = isHtmlWiki(pathOfNewHTML[0]) ? pathOfNewHTML[0] : path.join(pathOfNewHTML[0], `${activeWorkspace.name}.html`);
await this.wikiService.packetHTMLFromWikiFolder(activeWorkspace.wikiFolderLocation, fileName);
} else {
logger.error("Can not export whole wiki, pickDirectory's pathOfNewHTML is empty");
}
},
enabled: hasActiveWorkspaces,
},
{ type: 'separator' },
{
label: () => i18n.t('ContextMenu.CopyLink'),
accelerator: 'CmdOrCtrl+L',
click: async (_menuItem, browserWindow) => {
// if back is called in popup window
// copy the popup window URL instead
if (browserWindow !== undefined) {
const { isPopup } = await getFromRenderer<IBrowserViewMetaData>(MetaDataChannel.getViewMetaData, browserWindow);
if (isPopup === true) {
const url = browserWindow.webContents.getURL();
clipboard.writeText(url);
return;
}
}
const mainWindow = this.windowService.get(WindowNames.main);
const url = mainWindow?.getBrowserView()?.webContents?.getURL();
if (typeof url === 'string') {
clipboard.writeText(url);
}
},
},
]);
}
public async printTiddler(tiddlerName: string): Promise<void> {
const browserView = await this.viewService.getActiveBrowserView();
logger.info(`printTiddler() printing tiddler ${tiddlerName ?? 'undefined'}, browserView is ${browserView?.webContents === undefined ? 'undefined' : 'define'}`);

View file

@ -0,0 +1,123 @@
import { DEFAULT_DOWNLOADS_PATH } from '@/constants/appPaths';
import { MetaDataChannel } from '@/constants/channels';
import { isHtmlWiki, wikiHtmlExtensions } from '@/constants/fileNames';
import { container } from '@services/container';
import getFromRenderer from '@services/libs/getFromRenderer';
import { i18n } from '@services/libs/i18n';
import { logger } from '@services/libs/log';
import { IMenuService } from '@services/menu/interface';
import { INativeService } from '@services/native/interface';
import serviceIdentifier from '@services/serviceIdentifier';
import { IViewService } from '@services/view/interface';
import { IWikiService } from '@services/wiki/interface';
import { IWindowService } from '@services/windows/interface';
import { IBrowserViewMetaData, WindowNames } from '@services/windows/WindowProperties';
import { IWorkspaceService } from '@services/workspaces/interface';
import { clipboard } from 'electron';
import { CancelError as DownloadCancelError, download } from 'electron-dl';
import path from 'path';
export async function registerMenu(): Promise<void> {
const menuService = container.get<IMenuService>(serviceIdentifier.MenuService);
const windowService = container.get<IWindowService>(serviceIdentifier.Window);
const viewService = container.get<IViewService>(serviceIdentifier.View);
const workspaceService = container.get<IWorkspaceService>(serviceIdentifier.Workspace);
const nativeService = container.get<INativeService>(serviceIdentifier.NativeService);
const wikiService = container.get<IWikiService>(serviceIdentifier.Wiki);
const hasActiveWorkspaces = async (): Promise<boolean> => (await workspaceService.getActiveWorkspace()) !== undefined;
await menuService.insertMenu('Workspaces', [
{
label: () => i18n.t('Menu.DeveloperToolsActiveWorkspace'),
accelerator: 'CmdOrCtrl+Option+I',
click: async () => (await viewService.getActiveBrowserView())?.webContents?.openDevTools?.({ mode: 'detach' }),
enabled: hasActiveWorkspaces,
},
]);
await menuService.insertMenu('Wiki', [
{
label: () => i18n.t('Menu.PrintPage'),
click: async () => {
try {
const browserView = await viewService.getActiveBrowserView();
const win = windowService.get(WindowNames.main);
logger.info(
`print page, browserView printToPDF method is ${browserView?.webContents?.printToPDF === undefined ? 'undefined' : 'define'}, win is ${
win === undefined ? 'undefined' : 'define'
}`,
);
if (browserView === undefined || win === undefined) {
return;
}
const pdfBuffer = await browserView?.webContents?.printToPDF({
generateTaggedPDF: true,
});
// turn buffer to data uri
const dataUri = `data:application/pdf;base64,${pdfBuffer?.toString('base64')}`;
await download(win, dataUri, { filename: 'wiki.pdf', overwrite: false });
logger.info(`print page done`);
} catch (error) {
if (error instanceof DownloadCancelError) {
logger.debug('item.cancel() was called');
} else {
logger.error(`print page error: ${(error as Error).message}`, error);
}
}
},
enabled: hasActiveWorkspaces,
},
// TODO: get active tiddler title
// {
// label: () => i18n.t('Menu.PrintActiveTiddler'),
// accelerator: 'CmdOrCtrl+Alt+Shift+P',
// click: async () => {
// await printTiddler(title);
// },
// enabled: hasActiveWorkspaces,
// },
{
label: () => i18n.t('Menu.ExportWholeWikiHTML'),
click: async () => {
const activeWorkspace = await workspaceService.getActiveWorkspace();
if (activeWorkspace === undefined) {
logger.error('Can not export whole wiki, activeWorkspace is undefined');
return;
}
const pathOfNewHTML = await nativeService.pickDirectory(DEFAULT_DOWNLOADS_PATH, {
allowOpenFile: true,
filters: [{ name: 'HTML', extensions: wikiHtmlExtensions }],
});
if (pathOfNewHTML.length > 0) {
const fileName = isHtmlWiki(pathOfNewHTML[0]) ? pathOfNewHTML[0] : path.join(pathOfNewHTML[0], `${activeWorkspace.name}.html`);
await wikiService.packetHTMLFromWikiFolder(activeWorkspace.wikiFolderLocation, fileName);
} else {
logger.error("Can not export whole wiki, pickDirectory's pathOfNewHTML is empty");
}
},
enabled: hasActiveWorkspaces,
},
{ type: 'separator' },
{
label: () => i18n.t('ContextMenu.CopyLink'),
accelerator: 'CmdOrCtrl+L',
click: async (_menuItem, browserWindow) => {
// if back is called in popup window
// copy the popup window URL instead
if (browserWindow !== undefined) {
const { isPopup } = await getFromRenderer<IBrowserViewMetaData>(MetaDataChannel.getViewMetaData, browserWindow);
if (isPopup === true) {
const url = browserWindow.webContents.getURL();
clipboard.writeText(url);
return;
}
}
const mainWindow = windowService.get(WindowNames.main);
const url = mainWindow?.getBrowserView()?.webContents?.getURL();
if (typeof url === 'string') {
clipboard.writeText(url);
}
},
},
]);
}