refactor(electron): move electron-api to framework (#8601)

fix AF-1394
This commit is contained in:
pengx17
2024-10-30 09:16:20 +00:00
parent 50bae9c3e6
commit a791481ac8
81 changed files with 788 additions and 567 deletions

View File

@@ -0,0 +1,35 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { Entity } from '@toeverything/infra';
import type { DesktopApiProvider } from '../provider';
export class DesktopApi extends Entity {
constructor(public readonly provider: DesktopApiProvider) {
super();
if (!provider.handler || !provider.events || !provider.sharedStorage) {
throw new Error('DesktopApiProvider is not correctly initialized');
}
}
get handler() {
return this.provider.handler!;
}
get events() {
return this.provider.events!;
}
get sharedStorage() {
return this.provider.sharedStorage!;
}
get appInfo() {
return this.provider.appInfo;
}
}
export class DesktopAppInfo extends Entity {
constructor(public readonly provider: DesktopApiProvider) {
super();
}
}

View File

@@ -0,0 +1,19 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { apis, appInfo, events, sharedStorage } from '@affine/electron-api';
import { Service } from '@toeverything/infra';
import type { DesktopApiProvider } from '../provider';
export class ElectronApiImpl extends Service implements DesktopApiProvider {
constructor() {
super();
if (!apis || !events || !sharedStorage || !appInfo) {
throw new Error('Failed to initialize DesktopApiImpl');
}
}
handler = apis;
events = events;
sharedStorage = sharedStorage;
appInfo = appInfo!;
}

View File

@@ -0,0 +1,27 @@
import {
DocsService,
type Framework,
WorkspaceScope,
} from '@toeverything/infra';
import { WorkbenchService } from '../workbench';
import { DesktopApi } from './entities/electron-api';
import { ElectronApiImpl } from './impl';
import { DesktopApiProvider } from './provider';
import { DesktopApiService, WorkspaceDesktopApiService } from './service';
export function configureDesktopApiModule(framework: Framework) {
framework
.impl(DesktopApiProvider, ElectronApiImpl)
.entity(DesktopApi, [DesktopApiProvider])
.service(DesktopApiService, [DesktopApi])
.scope(WorkspaceScope)
.service(WorkspaceDesktopApiService, [
DesktopApiService,
DocsService,
WorkbenchService,
]);
}
export * from './service';
export type { ClientEvents, TabViewsMetaSchema } from '@affine/electron-api';

View File

@@ -0,0 +1,18 @@
import type { AppInfo } from '@affine/electron/preload/electron-api';
import type {
ClientEvents,
ClientHandler,
SharedStorage,
} from '@affine/electron-api';
import { createIdentifier } from '@toeverything/infra';
// for now desktop api's type are all inferred from electron-api
export interface DesktopApiProvider {
handler?: ClientHandler;
events?: ClientEvents;
sharedStorage?: SharedStorage;
appInfo: AppInfo;
}
export const DesktopApiProvider =
createIdentifier<DesktopApiProvider>('DesktopApiProvider');

View File

@@ -0,0 +1,167 @@
import { notify } from '@affine/component';
import { I18n } from '@affine/i18n';
import {
init,
reactRouterV6BrowserTracingIntegration,
setTags,
} from '@sentry/react';
import { ApplicationStarted, OnEvent, Service } from '@toeverything/infra';
import { debounce } from 'lodash-es';
import { useEffect } from 'react';
import {
createRoutesFromChildren,
matchRoutes,
useLocation,
useNavigationType,
} from 'react-router-dom';
import { AuthService } from '../../cloud';
import type { DesktopApi } from '../entities/electron-api';
@OnEvent(ApplicationStarted, e => e.setupStartListener)
export class DesktopApiService extends Service {
constructor(public readonly api: DesktopApi) {
super();
if (!api.handler || !api.events) {
throw new Error('DesktopApi is not initialized');
}
}
get appInfo() {
return this.api.appInfo;
}
get handler() {
return this.api.handler;
}
get events() {
return this.api.events;
}
get sharedStorage() {
return this.api.sharedStorage;
}
private setupStartListener() {
this.setupSentry();
this.setupCommonUIEvents();
this.setupAuthRequestEvent();
}
private setupSentry() {
if (
BUILD_CONFIG.debug ||
window.SENTRY_RELEASE ||
this.api.appInfo.windowName !== 'main'
) {
// https://docs.sentry.io/platforms/javascript/guides/electron/
init({
dsn: process.env.SENTRY_DSN,
environment: process.env.BUILD_TYPE ?? 'development',
integrations: [
reactRouterV6BrowserTracingIntegration({
useEffect,
useLocation,
useNavigationType,
createRoutesFromChildren,
matchRoutes,
}),
],
});
setTags({
appVersion: BUILD_CONFIG.appVersion,
editorVersion: BUILD_CONFIG.editorVersion,
});
this.api.handler.ui
.handleNetworkChange(navigator.onLine)
.catch(console.error);
window.addEventListener('offline', () => {
this.api.handler.ui.handleNetworkChange(false).catch(console.error);
});
window.addEventListener('online', () => {
this.api.handler.ui.handleNetworkChange(true).catch(console.error);
});
}
}
private setupCommonUIEvents() {
const handleMaximized = (maximized: boolean | undefined) => {
document.documentElement.dataset.maximized = String(maximized);
};
const handleFullscreen = (fullscreen: boolean | undefined) => {
document.documentElement.dataset.fullscreen = String(fullscreen);
};
this.api.handler.ui
.isMaximized()
.then(handleMaximized)
.catch(console.error);
this.api.handler.ui
.isFullScreen()
.then(handleFullscreen)
.catch(console.error);
this.api.events.ui.onMaximized(handleMaximized);
this.api.events.ui.onFullScreen(handleFullscreen);
const tabId = this.api.appInfo.viewId;
if (tabId && this.api.appInfo.windowName === 'main') {
let isActive = false;
const handleActiveTabChange = (active: boolean) => {
isActive = active;
document.documentElement.dataset.active = String(active);
};
this.api.handler.ui
.isActiveTab()
.then(active => {
handleActiveTabChange(active);
this.api.events.ui.onActiveTabChanged(id => {
handleActiveTabChange(id === tabId);
});
})
.catch(console.error);
const handleResize = debounce(() => {
if (isActive) {
this.api.handler.ui.handleWindowResize().catch(console.error);
}
}, 50);
window.addEventListener('resize', handleResize);
window.addEventListener('dragstart', () => {
document.documentElement.dataset.dragging = 'true';
});
window.addEventListener('dragend', () => {
document.documentElement.dataset.dragging = 'false';
});
}
}
private setupAuthRequestEvent() {
this.events.ui.onAuthenticationRequest(({ method, payload }) => {
(async () => {
const authService = this.framework.get(AuthService);
if (!(await this.api.handler.ui.isActiveTab())) {
return;
}
switch (method) {
case 'magic-link': {
const { email, token } = payload;
await authService.signInMagicLink(email, token);
break;
}
case 'oauth': {
const { code, state, provider } = payload;
await authService.signInOauth(code, state, provider);
break;
}
}
})().catch(e => {
notify.error({
title: I18n['com.affine.auth.toast.title.failed'](),
message: (e as any).message,
});
});
});
}
}

View File

@@ -0,0 +1,2 @@
export * from './desktop-api';
export * from './workspace-events';

View File

@@ -0,0 +1,41 @@
import type { DocsService } from '@toeverything/infra';
import { OnEvent, Service, WorkspaceInitialized } from '@toeverything/infra';
import { EditorSettingService } from '../../editor-setting';
import type { WorkbenchService } from '../../workbench';
import type { DesktopApiService } from './desktop-api';
// setup desktop events for workspace scope
@OnEvent(WorkspaceInitialized, e => e.setupApplicationMenuEvents)
export class WorkspaceDesktopApiService extends Service {
constructor(
private readonly desktopApi: DesktopApiService,
private readonly docsService: DocsService,
private readonly workbenchService: WorkbenchService
) {
super();
}
async setupApplicationMenuEvents() {
this.desktopApi.events.applicationMenu.onNewPageAction(() => {
const editorSetting =
this.framework.get(EditorSettingService).editorSetting;
const docProps = {
note: editorSetting.get('affine:note'),
};
this.desktopApi.handler.ui
.isActiveTab()
.then(isActive => {
if (!isActive) {
return;
}
const page = this.docsService.createDoc({ docProps });
this.workbenchService.workbench.openDoc(page.id);
})
.catch(err => {
console.error(err);
});
});
}
}