mirror of
https://github.com/tiddly-gittly/TidGi-Desktop.git
synced 2026-01-24 05:21:02 -08:00
439 lines
18 KiB
TypeScript
439 lines
18 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-misused-promises */
|
|
/* eslint-disable unicorn/consistent-destructuring */
|
|
import { app, BrowserView, shell, nativeImage, BrowserWindowConstructorOptions, BrowserWindow } from 'electron';
|
|
import path from 'path';
|
|
import fsExtra from 'fs-extra';
|
|
|
|
import { IWorkspace } from '@services/workspaces/interface';
|
|
import getViewBounds from '@services/libs/getViewBounds';
|
|
import { extractDomain, isInternalUrl } from '@/helpers/url';
|
|
import { buildResourcePath } from '@/constants/paths';
|
|
|
|
import serviceIdentifier from '@services/serviceIdentifier';
|
|
import type { IPreferenceService } from '@services/preferences/interface';
|
|
import type { IWorkspaceService } from '@services/workspaces/interface';
|
|
import type { IWorkspaceViewService } from '@services/workspacesView/interface';
|
|
import type { IWindowService } from '@services/windows/interface';
|
|
import { WindowNames, IBrowserViewMetaData } from '@services/windows/WindowProperties';
|
|
import { container } from '@services/container';
|
|
import { MetaDataChannel, ViewChannel, WindowChannel } from '@/constants/channels';
|
|
import { logger } from '@services/libs/log';
|
|
import { getLocalHostUrlWithActualIP } from '@services/libs/url';
|
|
import { LOAD_VIEW_MAX_RETRIES } from '@/constants/parameters';
|
|
|
|
export interface IViewContext {
|
|
loadInitialUrlWithCatch: () => Promise<void>;
|
|
sharedWebPreferences: BrowserWindowConstructorOptions['webPreferences'];
|
|
shouldPauseNotifications: boolean;
|
|
workspace: IWorkspace;
|
|
}
|
|
|
|
export interface IViewMeta {
|
|
forceNewWindow: boolean;
|
|
}
|
|
|
|
/**
|
|
* Bind workspace related event handler to view.webContent
|
|
*/
|
|
export default function setupViewEventHandlers(
|
|
view: BrowserView,
|
|
browserWindow: BrowserWindow,
|
|
{ workspace, sharedWebPreferences, loadInitialUrlWithCatch }: IViewContext,
|
|
): void {
|
|
// metadata and state about current BrowserView
|
|
const viewMeta: IViewMeta = {
|
|
forceNewWindow: false,
|
|
};
|
|
|
|
const workspaceService = container.get<IWorkspaceService>(serviceIdentifier.Workspace);
|
|
const workspaceViewService = container.get<IWorkspaceViewService>(serviceIdentifier.WorkspaceView);
|
|
const windowService = container.get<IWindowService>(serviceIdentifier.Window);
|
|
const preferenceService = container.get<IPreferenceService>(serviceIdentifier.Preference);
|
|
|
|
view.webContents.once('did-stop-loading', () => {});
|
|
view.webContents.on('did-start-loading', async () => {
|
|
const workspaceObject = await workspaceService.get(workspace.id);
|
|
// this event might be triggered
|
|
// even after the workspace obj and BrowserView
|
|
// are destroyed. See https://github.com/atomery/webcatalog/issues/836
|
|
if (workspaceObject === undefined) {
|
|
return;
|
|
}
|
|
if (
|
|
workspaceObject.active &&
|
|
typeof (await workspaceService.getMetaData(workspace.id)).didFailLoadErrorMessage === 'string' &&
|
|
browserWindow !== undefined &&
|
|
!browserWindow.isDestroyed()
|
|
) {
|
|
// fix https://github.com/atomery/singlebox/issues/228
|
|
const contentSize = browserWindow.getContentSize();
|
|
view.setBounds(await getViewBounds(contentSize as [number, number]));
|
|
}
|
|
await workspaceService.updateMetaData(workspace.id, {
|
|
// eslint-disable-next-line unicorn/no-null
|
|
didFailLoadErrorMessage: null,
|
|
isLoading: true,
|
|
});
|
|
});
|
|
view.webContents.on('did-stop-loading', async () => {
|
|
const workspaceObject = await workspaceService.get(workspace.id);
|
|
// this event might be triggered
|
|
// even after the workspace obj and BrowserView
|
|
// are destroyed. See https://github.com/atomery/webcatalog/issues/836
|
|
if (workspaceObject === undefined) {
|
|
return;
|
|
}
|
|
const currentUrl = view.webContents.getURL();
|
|
await workspaceService.update(workspace.id, {
|
|
lastUrl: currentUrl,
|
|
});
|
|
});
|
|
view.webContents.on('did-finish-load', async () => {
|
|
// fix https://github.com/atomery/webcatalog/issues/870
|
|
await workspaceViewService.realignActiveWorkspace();
|
|
// update isLoading to false when load succeed
|
|
await workspaceService.updateMetaData(workspace.id, {
|
|
isLoading: false,
|
|
didFailLoadTimes: 0,
|
|
});
|
|
});
|
|
// focus on initial load
|
|
// https://github.com/atomery/webcatalog/issues/398
|
|
if (workspace.active) {
|
|
view.webContents.once('did-stop-loading', () => {
|
|
if (browserWindow.isFocused() && !view.webContents.isFocused()) {
|
|
view.webContents.focus();
|
|
// try to fix sometimes new size is not correct https://github.com/tiddly-gittly/TiddlyGit-Desktop/issues/97
|
|
void workspaceViewService.realignActiveWorkspace();
|
|
}
|
|
});
|
|
}
|
|
// https://electronjs.org/docs/api/web-contents#event-did-fail-load
|
|
// https://github.com/webcatalog/neutron/blob/3d9e65c255792672c8bc6da025513a5404d98730/main-src/libs/views.js#L397
|
|
view.webContents.on('did-fail-load', async (_event, errorCode, errorDesc, _validateUrl, isMainFrame) => {
|
|
const [workspaceObject, workspaceMetaData] = await Promise.all([workspaceService.get(workspace.id), workspaceService.getMetaData(workspace.id)]);
|
|
const didFailLoadTimes = workspaceMetaData.didFailLoadTimes ?? 0;
|
|
// this event might be triggered
|
|
// even after the workspace obj and BrowserView
|
|
// are destroyed. See https://github.com/atomery/webcatalog/issues/836
|
|
if (workspaceObject === undefined) {
|
|
return;
|
|
}
|
|
if (isMainFrame && errorCode < 0 && errorCode !== -3) {
|
|
// Fix nodejs wiki start slow on system startup, which cause `-102 ERR_CONNECTION_REFUSED` even if wiki said it is booted, we have to retry several times
|
|
if (
|
|
errorCode === -102 &&
|
|
view.webContents.getURL().length > 0 &&
|
|
workspaceObject.homeUrl.startsWith('http') &&
|
|
didFailLoadTimes < LOAD_VIEW_MAX_RETRIES
|
|
) {
|
|
setTimeout(async () => {
|
|
await workspaceService.updateMetaData(workspace.id, {
|
|
didFailLoadTimes: didFailLoadTimes + 1,
|
|
});
|
|
await loadInitialUrlWithCatch();
|
|
}, 1000);
|
|
return;
|
|
}
|
|
await workspaceService.updateMetaData(workspace.id, {
|
|
didFailLoadErrorMessage: `${errorCode} ${errorDesc} , retryTimes: ${didFailLoadTimes}`,
|
|
});
|
|
if (workspaceObject.active && browserWindow !== undefined && !browserWindow.isDestroyed()) {
|
|
// fix https://github.com/atomery/singlebox/issues/228
|
|
const contentSize = browserWindow.getContentSize();
|
|
view.setBounds(await getViewBounds(contentSize as [number, number], false, 0, 0)); // hide browserView to show error message
|
|
}
|
|
}
|
|
// edge case to handle failed auth, use setTimeout to prevent infinite loop
|
|
if (errorCode === -300 && view.webContents.getURL().length === 0 && workspaceObject.homeUrl.startsWith('http')) {
|
|
setTimeout(async () => {
|
|
await loadInitialUrlWithCatch();
|
|
}, 1000);
|
|
}
|
|
});
|
|
view.webContents.on('did-navigate', async (_event, url) => {
|
|
const workspaceObject = await workspaceService.get(workspace.id);
|
|
// this event might be triggered
|
|
// even after the workspace obj and BrowserView
|
|
// are destroyed. See https://github.com/atomery/webcatalog/issues/836
|
|
if (workspaceObject === undefined) {
|
|
return;
|
|
}
|
|
if (workspaceObject.active) {
|
|
await windowService.sendToAllWindows(WindowChannel.updateCanGoBack, view.webContents.canGoBack());
|
|
await windowService.sendToAllWindows(WindowChannel.updateCanGoForward, view.webContents.canGoForward());
|
|
}
|
|
});
|
|
view.webContents.on('did-navigate-in-page', async (_event, url) => {
|
|
const workspaceObject = await workspaceService.get(workspace.id);
|
|
// this event might be triggered
|
|
// even after the workspace obj and BrowserView
|
|
// are destroyed. See https://github.com/atomery/webcatalog/issues/836
|
|
if (workspaceObject === undefined) {
|
|
return;
|
|
}
|
|
if (workspaceObject.active) {
|
|
await windowService.sendToAllWindows(WindowChannel.updateCanGoBack, view.webContents.canGoBack());
|
|
await windowService.sendToAllWindows(WindowChannel.updateCanGoForward, view.webContents.canGoForward());
|
|
}
|
|
});
|
|
view.webContents.on('page-title-updated', async (_event, title) => {
|
|
const workspaceObject = await workspaceService.get(workspace.id);
|
|
// this event might be triggered
|
|
// even after the workspace obj and BrowserView
|
|
// are destroyed. See https://github.com/atomery/webcatalog/issues/836
|
|
if (workspaceObject === undefined) {
|
|
return;
|
|
}
|
|
if (workspaceObject.active) {
|
|
browserWindow.setTitle(title);
|
|
}
|
|
});
|
|
|
|
view.webContents.on(
|
|
'new-window',
|
|
async (
|
|
_event: Electron.NewWindowWebContentsEvent,
|
|
nextUrl: string,
|
|
_frameName: string,
|
|
disposition: 'default' | 'new-window' | 'foreground-tab' | 'background-tab' | 'save-to-disk' | 'other',
|
|
options: BrowserWindowConstructorOptions,
|
|
_additionalFeatures: string[],
|
|
_referrer: Electron.Referrer,
|
|
_postBody: Electron.PostBody,
|
|
) =>
|
|
await handleNewWindow(_event, nextUrl, _frameName, disposition, options, _additionalFeatures, _referrer, _postBody, {
|
|
workspace,
|
|
sharedWebPreferences,
|
|
view,
|
|
meta: viewMeta,
|
|
}),
|
|
);
|
|
// Handle downloads
|
|
// https://electronjs.org/docs/api/download-item
|
|
view.webContents.session.on('will-download', async (_event, item) => {
|
|
const { askForDownloadPath, downloadPath } = await preferenceService.getPreferences();
|
|
// Set the save path, making Electron not to prompt a save dialog.
|
|
if (!askForDownloadPath) {
|
|
const finalFilePath = path.join(downloadPath, item.getFilename());
|
|
if (!fsExtra.existsSync(finalFilePath)) {
|
|
// eslint-disable-next-line no-param-reassign
|
|
item.savePath = finalFilePath;
|
|
}
|
|
} else {
|
|
// set preferred path for save dialog
|
|
const options = {
|
|
...item.getSaveDialogOptions(),
|
|
defaultPath: path.join(downloadPath, item.getFilename()),
|
|
};
|
|
item.setSaveDialogOptions(options);
|
|
}
|
|
});
|
|
// Unread count badge
|
|
void preferenceService.get('unreadCountBadge').then((unreadCountBadge) => {
|
|
if (unreadCountBadge) {
|
|
view.webContents.on('page-title-updated', async (_event, title) => {
|
|
const itemCountRegex = /[([{](\d*?)[)\]}]/;
|
|
const match = itemCountRegex.exec(title);
|
|
const incString = match !== null ? match[1] : '';
|
|
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
const inc = Number.parseInt(incString, 10) || 0;
|
|
await workspaceService.updateMetaData(workspace.id, {
|
|
badgeCount: inc,
|
|
});
|
|
let count = 0;
|
|
const workspaceMetaData = await workspaceService.getAllMetaData();
|
|
Object.values(workspaceMetaData).forEach((metaData) => {
|
|
if (typeof metaData?.badgeCount === 'number') {
|
|
count += metaData.badgeCount;
|
|
}
|
|
});
|
|
app.badgeCount = count;
|
|
if (process.platform === 'win32') {
|
|
if (count > 0) {
|
|
const icon = nativeImage.createFromPath(path.resolve(buildResourcePath, 'overlay-icon.png'));
|
|
browserWindow.setOverlayIcon(icon, `You have ${count} new messages.`);
|
|
} else {
|
|
// eslint-disable-next-line unicorn/no-null
|
|
browserWindow.setOverlayIcon(null, '');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
// Find In Page
|
|
view.webContents.on('found-in-page', async (_event, result) => {
|
|
await windowService.sendToAllWindows(ViewChannel.updateFindInPageMatches, result.activeMatchOrdinal, result.matches);
|
|
});
|
|
// Link preview
|
|
view.webContents.on('update-target-url', (_event, url) => {
|
|
try {
|
|
view.webContents.send('update-target-url', url);
|
|
} catch (error) {
|
|
logger.warn(error); // eslint-disable-line no-console
|
|
}
|
|
});
|
|
}
|
|
|
|
export interface INewWindowContext {
|
|
meta: IViewMeta;
|
|
sharedWebPreferences: BrowserWindowConstructorOptions['webPreferences'];
|
|
view: BrowserView;
|
|
workspace: IWorkspace;
|
|
}
|
|
|
|
async function handleNewWindow(
|
|
event: Electron.NewWindowWebContentsEvent,
|
|
nextUrl: string,
|
|
_frameName: string,
|
|
disposition: 'default' | 'new-window' | 'foreground-tab' | 'background-tab' | 'save-to-disk' | 'other',
|
|
options: BrowserWindowConstructorOptions,
|
|
_additionalFeatures: string[],
|
|
_referrer: Electron.Referrer,
|
|
_postBody: Electron.PostBody,
|
|
newWindowContext: INewWindowContext,
|
|
): Promise<void> {
|
|
const nextDomain = extractDomain(nextUrl);
|
|
// open external url in browser
|
|
if (nextDomain !== undefined && (disposition === 'foreground-tab' || disposition === 'background-tab')) {
|
|
event.preventDefault();
|
|
void shell.openExternal(nextUrl);
|
|
return;
|
|
}
|
|
const workspaceService = container.get<IWorkspaceService>(serviceIdentifier.Workspace);
|
|
const { view, workspace, sharedWebPreferences } = newWindowContext;
|
|
let appUrl = (await workspaceService.get(workspace.id))?.homeUrl;
|
|
if (appUrl === undefined) {
|
|
throw new Error(`Workspace ${workspace.id} not existed, or don't have homeUrl setting`);
|
|
}
|
|
appUrl = await getLocalHostUrlWithActualIP(appUrl);
|
|
const appDomain = extractDomain(appUrl);
|
|
const currentUrl = view.webContents.getURL();
|
|
const openInNewWindow = (): void => {
|
|
// https://gist.github.com/Gvozd/2cec0c8c510a707854e439fb15c561b0
|
|
event.preventDefault();
|
|
// if 'new-window' is triggered with Cmd+Click
|
|
// options is undefined
|
|
// https://github.com/atomery/webcatalog/issues/842
|
|
const cmdClick = options === undefined;
|
|
const browserViewMetaData: IBrowserViewMetaData = {
|
|
isPopup: true,
|
|
...(JSON.parse(
|
|
decodeURIComponent(sharedWebPreferences?.additionalArguments?.[1]?.replace(MetaDataChannel.browserViewMetaData, '') ?? '{}'),
|
|
) as IBrowserViewMetaData),
|
|
};
|
|
const metadataConfig = {
|
|
additionalArguments: [
|
|
`${MetaDataChannel.browserViewMetaData}${WindowNames.newWindow}`,
|
|
`${MetaDataChannel.browserViewMetaData}${encodeURIComponent(JSON.stringify(browserViewMetaData))}`,
|
|
],
|
|
preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY,
|
|
};
|
|
const newOptions: BrowserWindowConstructorOptions = cmdClick
|
|
? {
|
|
show: true,
|
|
width: 1200,
|
|
height: 800,
|
|
webPreferences: { ...sharedWebPreferences, ...metadataConfig },
|
|
}
|
|
: { ...options, width: 1200, height: 800, webPreferences: metadataConfig };
|
|
const popupWin = new BrowserWindow(newOptions);
|
|
popupWin.setMenuBarVisibility(false);
|
|
popupWin.webContents.on(
|
|
'new-window',
|
|
async (
|
|
_event: Electron.NewWindowWebContentsEvent,
|
|
nextUrl: string,
|
|
_frameName: string,
|
|
disposition: 'default' | 'new-window' | 'foreground-tab' | 'background-tab' | 'save-to-disk' | 'other',
|
|
options: BrowserWindowConstructorOptions,
|
|
_additionalFeatures: string[],
|
|
_referrer: Electron.Referrer,
|
|
_postBody: Electron.PostBody,
|
|
) => await handleNewWindow(_event, nextUrl, _frameName, disposition, options, _additionalFeatures, _referrer, _postBody, newWindowContext),
|
|
);
|
|
// if 'new-window' is triggered with Cmd+Click
|
|
// url is not loaded automatically
|
|
// https://github.com/atomery/webcatalog/issues/842
|
|
if (cmdClick) {
|
|
void popupWin.loadURL(nextUrl);
|
|
}
|
|
event.newGuest = popupWin;
|
|
};
|
|
// 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
|
|
if (
|
|
newWindowContext.meta.forceNewWindow ||
|
|
disposition === 'new-window' ||
|
|
disposition === 'default' ||
|
|
(appDomain === 'drive.google.com' && nextDomain === 'docs.google.com')
|
|
) {
|
|
newWindowContext.meta.forceNewWindow = false;
|
|
openInNewWindow();
|
|
return;
|
|
}
|
|
// load in same window
|
|
// if (
|
|
// // Google: Add account
|
|
// nextDomain === 'accounts.google.com' ||
|
|
// // Google: Switch account
|
|
// (typeof nextDomain === 'string' &&
|
|
// nextDomain.indexOf('google.com') > 0 &&
|
|
// isInternalUrl(nextUrl, [appUrl, currentUrl]) &&
|
|
// (nextUrl.includes('authuser=') || // https://drive.google.com/drive/u/1/priority?authuser=2 (has authuser query)
|
|
// /\/u\/\d+\/{0,1}$/.test(nextUrl))) || // https://mail.google.com/mail/u/1/ (ends with /u/1/)
|
|
// // https://github.com/atomery/webcatalog/issues/315
|
|
// // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions, @typescript-eslint/prefer-nullish-coalescing
|
|
// ((appDomain?.includes('asana.com') || currentDomain?.includes('asana.com')) && nextDomain?.includes('asana.com'))
|
|
// ) {
|
|
// event.preventDefault();
|
|
// void view.webContents.loadURL(nextUrl);
|
|
// return;
|
|
// }
|
|
// // open new window
|
|
// if (isInternalUrl(nextUrl, [appUrl, currentUrl])) {
|
|
// openInNewWindow();
|
|
// return;
|
|
// }
|
|
|
|
// 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
|
|
if (nextDomain === null && (disposition === 'foreground-tab' || disposition === 'background-tab')) {
|
|
event.preventDefault();
|
|
const newOptions = {
|
|
...options,
|
|
show: false,
|
|
};
|
|
const popupWin = new BrowserWindow(newOptions);
|
|
popupWin.setMenuBarVisibility(false);
|
|
popupWin.webContents.on(
|
|
'new-window',
|
|
async (
|
|
_event: Electron.NewWindowWebContentsEvent,
|
|
nextUrl: string,
|
|
_frameName: string,
|
|
disposition: 'default' | 'new-window' | 'foreground-tab' | 'background-tab' | 'save-to-disk' | 'other',
|
|
options: BrowserWindowConstructorOptions,
|
|
_additionalFeatures: string[],
|
|
_referrer: Electron.Referrer,
|
|
_postBody: Electron.PostBody,
|
|
) => await handleNewWindow(_event, nextUrl, _frameName, disposition, options, _additionalFeatures, _referrer, _postBody, newWindowContext),
|
|
);
|
|
popupWin.webContents.once('will-navigate', (_event, url) => {
|
|
// if the window is used for the current app, then use default behavior
|
|
if (isInternalUrl(url, [appUrl, currentUrl])) {
|
|
popupWin.show();
|
|
} else {
|
|
// if not, open in browser
|
|
event.preventDefault();
|
|
void shell.openExternal(url);
|
|
popupWin.close();
|
|
}
|
|
});
|
|
event.newGuest = popupWin;
|
|
}
|
|
}
|