feat: add open app route (#3899)

This commit is contained in:
Peng Xiao
2023-08-30 06:40:25 +08:00
committed by GitHub
parent 71b195d9a9
commit 800f3c3cb6
29 changed files with 486 additions and 104 deletions

View File

@@ -1,8 +1,14 @@
import path from 'node:path';
import type { App } from 'electron';
import { buildType, isDev } from './config';
import { logger } from './logger';
import { handleOpenUrlInPopup } from './main-window';
import {
handleOpenUrlInHiddenWindow,
restoreOrCreateWindow,
} from './main-window';
import { uiSubjects } from './ui';
let protocol = buildType === 'stable' ? 'affine' : `affine-${buildType}`;
if (isDev) {
@@ -10,7 +16,16 @@ if (isDev) {
}
export function setupDeepLink(app: App) {
app.setAsDefaultProtocolClient(protocol);
if (process.defaultApp) {
if (process.argv.length >= 2) {
app.setAsDefaultProtocolClient(protocol, process.execPath, [
path.resolve(process.argv[1]),
]);
}
} else {
app.setAsDefaultProtocolClient(protocol);
}
app.on('open-url', (event, url) => {
if (url.startsWith(`${protocol}://`)) {
event.preventDefault();
@@ -19,17 +34,72 @@ export function setupDeepLink(app: App) {
});
}
});
// on windows & linux, we need to listen for the second-instance event
app.on('second-instance', (event, commandLine) => {
restoreOrCreateWindow()
.then(() => {
const url = commandLine.pop();
if (url?.startsWith(`${protocol}://`)) {
event.preventDefault();
handleAffineUrl(url).catch(e => {
logger.error('failed to handle affine url', e);
});
}
})
.catch(e => console.error('Failed to restore or create window:', e));
});
}
async function handleAffineUrl(url: string) {
logger.info('open affine url', url);
const urlObj = new URL(url);
if (urlObj.hostname === 'open-url') {
logger.info('handle affine schema action', urlObj.hostname);
// handle more actions here
// hostname is the action name
if (urlObj.hostname === 'sign-in') {
const urlToOpen = urlObj.search.slice(1);
if (urlToOpen) {
handleOpenUrlInPopup(urlToOpen).catch(e => {
logger.error('failed to open url in popup', e);
});
await handleSignIn(urlToOpen);
}
}
}
// todo: move to another place?
async function handleSignIn(url: string) {
if (url) {
try {
const mainWindow = await restoreOrCreateWindow();
mainWindow.show();
const urlObj = new URL(url);
const email = urlObj.searchParams.get('email');
if (!email) {
logger.error('no email in url', url);
return;
}
uiSubjects.onStartLogin.next({
email,
});
const window = await handleOpenUrlInHiddenWindow(url);
logger.info('opened url in hidden window', window.webContents.getURL());
// check path
// - if path === /auth/{signIn,signUp}, we know sign in succeeded
// - if path === expired, we know sign in failed
const finalUrl = new URL(window.webContents.getURL());
console.log('final url', finalUrl);
// hack: wait for the hidden window to send broadcast message to the main window
// that's how next-auth works for cross-tab communication
setTimeout(() => {
window.destroy();
}, 3000);
uiSubjects.onFinishLogin.next({
success: ['/auth/signIn', '/auth/signUp'].includes(finalUrl.pathname),
email,
});
} catch (e) {
logger.error('failed to open url in popup', e);
}
}
}

View File

@@ -33,12 +33,6 @@ if (!isSingleInstance) {
process.exit(0);
}
app.on('second-instance', () => {
restoreOrCreateWindow().catch(e =>
console.error('Failed to restore or create window:', e)
);
});
/**
* Shout down background process if all windows was closed
*/

View File

@@ -95,7 +95,13 @@ async function createWindow() {
browserWindow.on('close', e => {
e.preventDefault();
browserWindow.destroy();
// close and destroy all windows
BrowserWindow.getAllWindows().forEach(w => {
if (!w.isDestroyed()) {
w.close();
w.destroy();
}
});
helperConnectionUnsub?.();
// TODO: gracefully close the app, for example, ask user to save unsaved changes
});
@@ -123,44 +129,12 @@ async function createWindow() {
// singleton
let browserWindow: BrowserWindow | undefined;
let popup: BrowserWindow | undefined;
function createPopupWindow() {
if (!popup || popup?.isDestroyed()) {
const mainExposedMeta = getExposedMeta();
popup = new BrowserWindow({
width: 1200,
height: 600,
alwaysOnTop: true,
resizable: false,
webPreferences: {
preload: join(__dirname, './preload.js'),
additionalArguments: [
`--main-exposed-meta=` + JSON.stringify(mainExposedMeta),
// popup window does not need helper process, right?
],
},
});
popup.on('close', e => {
e.preventDefault();
popup?.destroy();
popup = undefined;
});
browserWindow?.webContents.once('did-finish-load', () => {
closePopup();
});
}
return popup;
}
/**
* Restore existing BrowserWindow or Create new BrowserWindow
*/
export async function restoreOrCreateWindow() {
browserWindow =
browserWindow || BrowserWindow.getAllWindows().find(w => !w.isDestroyed());
if (browserWindow === undefined) {
if (!browserWindow || browserWindow.isDestroyed()) {
browserWindow = await createWindow();
}
@@ -172,17 +146,29 @@ export async function restoreOrCreateWindow() {
return browserWindow;
}
export async function handleOpenUrlInPopup(url: string) {
const popup = createPopupWindow();
await popup.loadURL(url);
}
export function closePopup() {
if (!popup?.isDestroyed()) {
popup?.close();
popup?.destroy();
popup = undefined;
}
export async function handleOpenUrlInHiddenWindow(url: string) {
const mainExposedMeta = getExposedMeta();
const win = new BrowserWindow({
width: 1200,
height: 600,
webPreferences: {
preload: join(__dirname, './preload.js'),
additionalArguments: [
`--main-exposed-meta=` + JSON.stringify(mainExposedMeta),
// popup window does not need helper process, right?
],
},
show: false,
});
win.on('close', e => {
e.preventDefault();
if (!win.isDestroyed()) {
win.destroy();
}
});
logger.info('loading page at', url);
await win.loadURL(url);
return win;
}
export function reloadApp() {

View File

@@ -5,10 +5,18 @@ import { uiSubjects } from './subject';
* Events triggered by application menu
*/
export const uiEvents = {
onFinishLogin: (fn: () => void) => {
onFinishLogin: (
fn: (result: { success: boolean; email: string }) => void
) => {
const sub = uiSubjects.onFinishLogin.subscribe(fn);
return () => {
sub.unsubscribe();
};
},
onStartLogin: (fn: (opts: { email: string }) => void) => {
const sub = uiSubjects.onStartLogin.subscribe(fn);
return () => {
sub.unsubscribe();
};
},
} satisfies Record<string, MainEventRegister>;

View File

@@ -1,10 +1,8 @@
import { app, BrowserWindow, nativeTheme } from 'electron';
import { isMacOS } from '../../shared/utils';
import { closePopup } from '../main-window';
import type { NamespaceHandlers } from '../type';
import { getGoogleOauthCode } from './google-auth';
import { uiSubjects } from './subject';
export const uiHandlers = {
handleThemeChange: async (_, theme: (typeof nativeTheme)['themeSource']) => {
@@ -38,10 +36,6 @@ export const uiHandlers = {
handleCloseApp: async () => {
app.quit();
},
handleFinishLogin: async () => {
closePopup();
uiSubjects.onFinishLogin.next();
},
getGoogleOauthCode: async () => {
return getGoogleOauthCode();
},

View File

@@ -1,5 +1,6 @@
import { Subject } from 'rxjs';
export const uiSubjects = {
onFinishLogin: new Subject<void>(),
onStartLogin: new Subject<{ email: string }>(),
onFinishLogin: new Subject<{ success: boolean; email: string }>(),
};