mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
feat: add open app route (#3899)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
|
||||
@@ -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 }>(),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user