mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-09-11 19:47:35 +03:00
refactor(electron): create electron api package (#5334)
This commit is contained in:
parent
ce17daba42
commit
4e861d8118
@ -1,15 +1,7 @@
|
||||
{
|
||||
"name": "@toeverything/infra",
|
||||
"type": "module",
|
||||
"module": "./dist/index.js",
|
||||
"main": "./dist/index.cjs",
|
||||
"types": "./dist/src/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/src/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs"
|
||||
},
|
||||
"./blocksuite": {
|
||||
"types": "./dist/src/blocksuite/index.d.ts",
|
||||
"import": "./dist/blocksuite.js",
|
||||
@ -20,26 +12,11 @@
|
||||
"import": "./dist/command.js",
|
||||
"require": "./dist/command.cjs"
|
||||
},
|
||||
"./core/*": {
|
||||
"types": "./dist/src/core/*.d.ts",
|
||||
"import": "./dist/core/*.js",
|
||||
"require": "./dist/core/*.cjs"
|
||||
},
|
||||
"./preload/*": {
|
||||
"types": "./dist/src/preload/*.d.ts",
|
||||
"import": "./dist/preload/*.js",
|
||||
"require": "./dist/preload/*.cjs"
|
||||
},
|
||||
"./atom": {
|
||||
"type": "./dist/src/atom.d.ts",
|
||||
"import": "./dist/atom.js",
|
||||
"require": "./dist/atom.cjs"
|
||||
},
|
||||
"./type": {
|
||||
"type": "./dist/src/type.d.ts",
|
||||
"import": "./dist/type.js",
|
||||
"require": "./dist/type.cjs"
|
||||
},
|
||||
"./app-config-storage": {
|
||||
"type": "./dist/src/app-config-storage.d.ts",
|
||||
"import": "./dist/app-config-storage.js",
|
||||
|
3
packages/common/infra/preload/electron.d.ts
vendored
3
packages/common/infra/preload/electron.d.ts
vendored
@ -1,3 +0,0 @@
|
||||
/* eslint-disable */
|
||||
// @ts-ignore
|
||||
export * from '../dist/src/preload/electron';
|
@ -1,3 +0,0 @@
|
||||
/* eslint-disable */
|
||||
/// <reference types="../dist/preload/electron.d.ts" />
|
||||
export * from '../dist/preload/electron.js';
|
@ -72,12 +72,13 @@ const appSettingEffect = atomEffect(get => {
|
||||
// some values in settings should be synced into electron side
|
||||
if (environment.isDesktop) {
|
||||
console.log('set config', settings);
|
||||
window.apis?.updater
|
||||
// this api type in @affine/electron-api, but it is circular dependency this package, use any here
|
||||
(window as any).apis?.updater
|
||||
.setConfig({
|
||||
autoCheckUpdate: settings.autoCheckUpdate,
|
||||
autoDownloadUpdate: settings.autoDownloadUpdate,
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err: any) => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
@ -1,74 +0,0 @@
|
||||
/**
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2018 Andy Wermke
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
export type EventMap = {
|
||||
[key: string]: (...args: any[]) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Type-safe event emitter.
|
||||
*
|
||||
* Use it like this:
|
||||
*
|
||||
* ```typescript
|
||||
* type MyEvents = {
|
||||
* error: (error: Error) => void;
|
||||
* message: (from: string, content: string) => void;
|
||||
* }
|
||||
*
|
||||
* const myEmitter = new EventEmitter() as TypedEmitter<MyEvents>;
|
||||
*
|
||||
* myEmitter.emit("error", "x") // <- Will catch this type error;
|
||||
* ```
|
||||
*
|
||||
* Lifecycle:
|
||||
* invoke -> handle -> emit -> on/once
|
||||
*/
|
||||
export interface TypedEventEmitter<Events extends EventMap> {
|
||||
addListener<E extends keyof Events>(event: E, listener: Events[E]): this;
|
||||
on<E extends keyof Events>(event: E, listener: Events[E]): this;
|
||||
once<E extends keyof Events>(event: E, listener: Events[E]): this;
|
||||
|
||||
off<E extends keyof Events>(event: E, listener: Events[E]): this;
|
||||
removeAllListeners<E extends keyof Events>(event?: E): this;
|
||||
removeListener<E extends keyof Events>(event: E, listener: Events[E]): this;
|
||||
|
||||
emit<E extends keyof Events>(
|
||||
event: E,
|
||||
...args: Parameters<Events[E]>
|
||||
): boolean;
|
||||
// The sloppy `eventNames()` return type is to mitigate type incompatibilities - see #5
|
||||
eventNames(): (keyof Events | string | symbol)[];
|
||||
rawListeners<E extends keyof Events>(event: E): Events[E][];
|
||||
listeners<E extends keyof Events>(event: E): Events[E][];
|
||||
listenerCount<E extends keyof Events>(event: E): number;
|
||||
|
||||
handle<E extends keyof Events>(event: E, handler: Events[E]): this;
|
||||
invoke<E extends keyof Events>(
|
||||
event: E,
|
||||
...args: Parameters<Events[E]>
|
||||
): Promise<ReturnType<Events[E]>>;
|
||||
|
||||
getMaxListeners(): number;
|
||||
setMaxListeners(maxListeners: number): this;
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
import type {
|
||||
ClipboardHandlers,
|
||||
ConfigStorageHandlers,
|
||||
DBHandlers,
|
||||
DebugHandlers,
|
||||
DialogHandlers,
|
||||
ExportHandlers,
|
||||
UIHandlers,
|
||||
UpdaterHandlers,
|
||||
WorkspaceHandlers,
|
||||
} from './type.js';
|
||||
import { HandlerManager } from './type.js';
|
||||
|
||||
export abstract class DBHandlerManager extends HandlerManager<
|
||||
'db',
|
||||
DBHandlers
|
||||
> {}
|
||||
|
||||
export abstract class DebugHandlerManager extends HandlerManager<
|
||||
'debug',
|
||||
DebugHandlers
|
||||
> {}
|
||||
|
||||
export abstract class DialogHandlerManager extends HandlerManager<
|
||||
'dialog',
|
||||
DialogHandlers
|
||||
> {}
|
||||
|
||||
export abstract class UIHandlerManager extends HandlerManager<
|
||||
'ui',
|
||||
UIHandlers
|
||||
> {}
|
||||
|
||||
export abstract class ClipboardHandlerManager extends HandlerManager<
|
||||
'clipboard',
|
||||
ClipboardHandlers
|
||||
> {}
|
||||
|
||||
export abstract class ExportHandlerManager extends HandlerManager<
|
||||
'export',
|
||||
ExportHandlers
|
||||
> {}
|
||||
|
||||
export abstract class UpdaterHandlerManager extends HandlerManager<
|
||||
'updater',
|
||||
UpdaterHandlers
|
||||
> {}
|
||||
|
||||
export abstract class WorkspaceHandlerManager extends HandlerManager<
|
||||
'workspace',
|
||||
WorkspaceHandlers
|
||||
> {}
|
||||
|
||||
export abstract class ConfigStorageHandlerManager extends HandlerManager<
|
||||
'configStorage',
|
||||
ConfigStorageHandlers
|
||||
> {}
|
@ -1,2 +0,0 @@
|
||||
export * from './handler.js';
|
||||
export * from './type.js';
|
@ -1,285 +0,0 @@
|
||||
import type Buffer from 'buffer';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { AppConfigSchema } from './app-config-storage.js';
|
||||
import type { TypedEventEmitter } from './core/event-emitter.js';
|
||||
|
||||
type Buffer = Buffer.Buffer;
|
||||
|
||||
export const packageJsonInputSchema = z.object({
|
||||
name: z.string(),
|
||||
version: z.string(),
|
||||
description: z.string(),
|
||||
affinePlugin: z.object({
|
||||
release: z.union([z.boolean(), z.enum(['development'])]),
|
||||
entry: z.object({
|
||||
core: z.string(),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const packageJsonOutputSchema = z.object({
|
||||
name: z.string(),
|
||||
version: z.string(),
|
||||
description: z.string(),
|
||||
affinePlugin: z.object({
|
||||
release: z.union([z.boolean(), z.enum(['development'])]),
|
||||
entry: z.object({
|
||||
core: z.string(),
|
||||
}),
|
||||
assets: z.array(z.string()),
|
||||
}),
|
||||
});
|
||||
|
||||
export abstract class HandlerManager<
|
||||
Namespace extends string,
|
||||
Handlers extends Record<string, PrimitiveHandlers>,
|
||||
> {
|
||||
static instance: HandlerManager<string, Record<string, PrimitiveHandlers>>;
|
||||
private readonly _app: App<Namespace, Handlers>;
|
||||
private readonly _namespace: Namespace;
|
||||
private _handlers: Handlers;
|
||||
|
||||
constructor() {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
private _initialized = false;
|
||||
|
||||
registerHandlers(handlers: Handlers) {
|
||||
if (this._initialized) {
|
||||
throw new Error('Already initialized');
|
||||
}
|
||||
this._handlers = handlers;
|
||||
for (const [name, handler] of Object.entries(this._handlers)) {
|
||||
this._app.handle(`${this._namespace}:${name}`, (async (...args: any[]) =>
|
||||
handler(...args)) as any);
|
||||
}
|
||||
this._initialized = true;
|
||||
}
|
||||
|
||||
invokeHandler<K extends keyof Handlers>(
|
||||
name: K,
|
||||
...args: Parameters<Handlers[K]>
|
||||
): Promise<ReturnType<Handlers[K]>> {
|
||||
return this._handlers[name](...args);
|
||||
}
|
||||
|
||||
static getInstance(): HandlerManager<
|
||||
string,
|
||||
Record<string, PrimitiveHandlers>
|
||||
> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
||||
export interface WorkspaceMeta {
|
||||
id: string;
|
||||
mainDBPath: string;
|
||||
secondaryDBPath?: string; // assume there will be only one
|
||||
}
|
||||
|
||||
export type PrimitiveHandlers = (...args: any[]) => Promise<any>;
|
||||
|
||||
export type DBHandlers = {
|
||||
getDocAsUpdates: (
|
||||
workspaceId: string,
|
||||
subdocId?: string
|
||||
) => Promise<Uint8Array | false>;
|
||||
applyDocUpdate: (
|
||||
id: string,
|
||||
update: Uint8Array,
|
||||
subdocId?: string
|
||||
) => Promise<void>;
|
||||
addBlob: (
|
||||
workspaceId: string,
|
||||
key: string,
|
||||
data: Uint8Array
|
||||
) => Promise<void>;
|
||||
getBlob: (workspaceId: string, key: string) => Promise<Buffer | null>;
|
||||
deleteBlob: (workspaceId: string, key: string) => Promise<void>;
|
||||
getBlobKeys: (workspaceId: string) => Promise<string[]>;
|
||||
getDefaultStorageLocation: () => Promise<string>;
|
||||
};
|
||||
|
||||
export type DebugHandlers = {
|
||||
revealLogFile: () => Promise<string>;
|
||||
logFilePath: () => Promise<string>;
|
||||
};
|
||||
|
||||
export type ErrorMessage =
|
||||
| 'DB_FILE_ALREADY_LOADED'
|
||||
| 'DB_FILE_PATH_INVALID'
|
||||
| 'DB_FILE_INVALID'
|
||||
| 'DB_FILE_MIGRATION_FAILED'
|
||||
| 'FILE_ALREADY_EXISTS'
|
||||
| 'UNKNOWN_ERROR';
|
||||
|
||||
export interface LoadDBFileResult {
|
||||
workspaceId?: string;
|
||||
error?: ErrorMessage;
|
||||
canceled?: boolean;
|
||||
}
|
||||
|
||||
export interface SaveDBFileResult {
|
||||
filePath?: string;
|
||||
canceled?: boolean;
|
||||
error?: ErrorMessage;
|
||||
}
|
||||
|
||||
export interface SelectDBFileLocationResult {
|
||||
filePath?: string;
|
||||
error?: ErrorMessage;
|
||||
canceled?: boolean;
|
||||
}
|
||||
|
||||
export interface MoveDBFileResult {
|
||||
filePath?: string;
|
||||
error?: ErrorMessage;
|
||||
canceled?: boolean;
|
||||
}
|
||||
|
||||
// provide a backdoor to set dialog path for testing in playwright
|
||||
export interface FakeDialogResult {
|
||||
canceled?: boolean;
|
||||
filePath?: string;
|
||||
filePaths?: string[];
|
||||
}
|
||||
|
||||
export type DialogHandlers = {
|
||||
revealDBFile: (workspaceId: string) => Promise<void>;
|
||||
loadDBFile: () => Promise<LoadDBFileResult>;
|
||||
saveDBFileAs: (workspaceId: string) => Promise<SaveDBFileResult>;
|
||||
moveDBFile: (
|
||||
workspaceId: string,
|
||||
dbFileLocation?: string
|
||||
) => Promise<MoveDBFileResult>;
|
||||
selectDBFileLocation: () => Promise<SelectDBFileLocationResult>;
|
||||
setFakeDialogResult: (result: any) => Promise<void>;
|
||||
};
|
||||
|
||||
export type UIHandlers = {
|
||||
handleThemeChange: (theme: 'system' | 'light' | 'dark') => Promise<any>;
|
||||
handleSidebarVisibilityChange: (visible: boolean) => Promise<any>;
|
||||
handleMinimizeApp: () => Promise<any>;
|
||||
handleMaximizeApp: () => Promise<any>;
|
||||
handleCloseApp: () => Promise<any>;
|
||||
getGoogleOauthCode: () => Promise<any>;
|
||||
getChallengeResponse: (resource: string) => Promise<string>;
|
||||
handleOpenMainApp: () => Promise<any>;
|
||||
};
|
||||
|
||||
export type ClipboardHandlers = {
|
||||
copyAsImageFromString: (dataURL: string) => Promise<void>;
|
||||
};
|
||||
|
||||
export type ExportHandlers = {
|
||||
savePDFFileAs: (title: string) => Promise<any>;
|
||||
};
|
||||
|
||||
export interface UpdateMeta {
|
||||
version: string;
|
||||
allowAutoUpdate: boolean;
|
||||
}
|
||||
|
||||
export type UpdaterConfig = {
|
||||
autoCheckUpdate: boolean;
|
||||
autoDownloadUpdate: boolean;
|
||||
};
|
||||
|
||||
export type UpdaterHandlers = {
|
||||
currentVersion: () => Promise<string>;
|
||||
quitAndInstall: () => Promise<void>;
|
||||
downloadUpdate: () => Promise<void>;
|
||||
getConfig: () => Promise<UpdaterConfig>;
|
||||
setConfig: (newConfig: Partial<UpdaterConfig>) => Promise<void>;
|
||||
checkForUpdates: () => Promise<{ version: string } | null>;
|
||||
};
|
||||
|
||||
export type WorkspaceHandlers = {
|
||||
list: () => Promise<[workspaceId: string, meta: WorkspaceMeta][]>;
|
||||
delete: (id: string) => Promise<void>;
|
||||
getMeta: (id: string) => Promise<WorkspaceMeta>;
|
||||
clone: (id: string, newId: string) => Promise<void>;
|
||||
};
|
||||
|
||||
export type ConfigStorageHandlers = {
|
||||
set: (config: AppConfigSchema | Partial<AppConfigSchema>) => Promise<void>;
|
||||
get: () => Promise<AppConfigSchema>;
|
||||
};
|
||||
|
||||
export type UnwrapManagerHandlerToServerSide<
|
||||
ElectronEvent extends {
|
||||
frameId: number;
|
||||
processId: number;
|
||||
},
|
||||
Manager extends HandlerManager<string, Record<string, PrimitiveHandlers>>,
|
||||
> = Manager extends HandlerManager<infer _, infer Handlers>
|
||||
? {
|
||||
[K in keyof Handlers]: Handlers[K] extends (
|
||||
...args: infer Args
|
||||
) => Promise<infer R>
|
||||
? (event: ElectronEvent, ...args: Args) => Promise<R>
|
||||
: never;
|
||||
}
|
||||
: never;
|
||||
|
||||
export type UnwrapManagerHandlerToClientSide<
|
||||
Manager extends HandlerManager<string, Record<string, PrimitiveHandlers>>,
|
||||
> = Manager extends HandlerManager<infer _, infer Handlers>
|
||||
? {
|
||||
[K in keyof Handlers]: Handlers[K] extends (
|
||||
...args: infer Args
|
||||
) => Promise<infer R>
|
||||
? (...args: Args) => Promise<R>
|
||||
: never;
|
||||
}
|
||||
: never;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export type App<
|
||||
Namespace extends string,
|
||||
Handlers extends Record<string, PrimitiveHandlers>,
|
||||
> = TypedEventEmitter<{
|
||||
[K in keyof Handlers as `${Namespace}:${K & string}`]: Handlers[K];
|
||||
}>;
|
||||
|
||||
export interface UpdaterEvents {
|
||||
onUpdateAvailable: (fn: (versionMeta: UpdateMeta) => void) => () => void;
|
||||
onUpdateReady: (fn: (versionMeta: UpdateMeta) => void) => () => void;
|
||||
onDownloadProgress: (fn: (progress: number) => void) => () => void;
|
||||
}
|
||||
|
||||
export interface ApplicationMenuEvents {
|
||||
onNewPageAction: (fn: () => void) => () => void;
|
||||
}
|
||||
|
||||
export interface DBEvents {
|
||||
onExternalUpdate: (
|
||||
fn: (update: {
|
||||
workspaceId: string;
|
||||
update: Uint8Array;
|
||||
docId?: string;
|
||||
}) => void
|
||||
) => () => void;
|
||||
}
|
||||
|
||||
export interface WorkspaceEvents {
|
||||
onMetaChange: (
|
||||
fn: (workspaceId: string, meta: WorkspaceMeta) => void
|
||||
) => () => void;
|
||||
}
|
||||
|
||||
export interface UIEvents {
|
||||
onMaximized: (fn: (maximized: boolean) => void) => () => void;
|
||||
}
|
||||
|
||||
export interface EventMap {
|
||||
updater: UpdaterEvents;
|
||||
applicationMenu: ApplicationMenuEvents;
|
||||
db: DBEvents;
|
||||
ui: UIEvents;
|
||||
workspace: WorkspaceEvents;
|
||||
}
|
@ -12,12 +12,8 @@ export default defineConfig({
|
||||
lib: {
|
||||
entry: {
|
||||
blocksuite: resolve(root, 'src/blocksuite/index.ts'),
|
||||
index: resolve(root, 'src/index.ts'),
|
||||
atom: resolve(root, 'src/atom/index.ts'),
|
||||
command: resolve(root, 'src/command/index.ts'),
|
||||
type: resolve(root, 'src/type.ts'),
|
||||
'core/event-emitter': resolve(root, 'src/core/event-emitter.ts'),
|
||||
'preload/electron': resolve(root, 'src/preload/electron.ts'),
|
||||
'app-config-storage': resolve(root, 'src/app-config-storage.ts'),
|
||||
},
|
||||
formats: ['es', 'cjs'],
|
||||
|
@ -20,6 +20,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@affine/debug": "workspace:*",
|
||||
"@affine/electron-api": "workspace:*",
|
||||
"@affine/graphql": "workspace:*",
|
||||
"@affine/i18n": "workspace:*",
|
||||
"@affine/workspace": "workspace:*",
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { apis } from '@affine/electron-api';
|
||||
import { ThemeProvider as NextThemeProvider, useTheme } from 'next-themes';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { memo, useRef } from 'react';
|
||||
@ -10,7 +11,7 @@ const DesktopThemeSync = memo(function DesktopThemeSync() {
|
||||
const onceRef = useRef(false);
|
||||
if (lastThemeRef.current !== theme || !onceRef.current) {
|
||||
if (environment.isDesktop && theme) {
|
||||
window.apis?.ui
|
||||
apis?.ui
|
||||
.handleThemeChange(theme as 'dark' | 'light' | 'system')
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
|
@ -14,6 +14,9 @@
|
||||
{
|
||||
"path": "../../frontend/hooks"
|
||||
},
|
||||
{
|
||||
"path": "../../frontend/electron-api"
|
||||
},
|
||||
{ "path": "../../frontend/workspace" },
|
||||
{
|
||||
"path": "../../common/debug"
|
||||
|
@ -20,6 +20,7 @@
|
||||
"@affine/cmdk": "workspace:*",
|
||||
"@affine/component": "workspace:*",
|
||||
"@affine/debug": "workspace:*",
|
||||
"@affine/electron-api": "workspace:*",
|
||||
"@affine/env": "workspace:*",
|
||||
"@affine/graphql": "workspace:*",
|
||||
"@affine/i18n": "workspace:*",
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { apis } from '@affine/electron-api';
|
||||
import type { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { ResetIcon } from '@blocksuite/icons';
|
||||
import { updateReadyAtom } from '@toeverything/hooks/use-app-updater';
|
||||
@ -21,7 +22,7 @@ export function registerAffineUpdatesCommands({
|
||||
label: t['com.affine.cmdk.affine.restart-to-upgrade'](),
|
||||
preconditionStrategy: () => !!store.get(updateReadyAtom),
|
||||
run() {
|
||||
window.apis?.updater.quitAndInstall().catch(err => {
|
||||
apis?.updater.quitAndInstall().catch(err => {
|
||||
// TODO: add error toast here
|
||||
console.error(err);
|
||||
});
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { apis } from '@affine/electron-api';
|
||||
import { fetchWithTraceReport } from '@affine/graphql';
|
||||
import { Turnstile } from '@marsidev/react-turnstile';
|
||||
import { atom, useAtom, useSetAtom } from 'jotai';
|
||||
@ -32,7 +33,7 @@ const generateChallengeResponse = async (challenge: string) => {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return await window.apis?.ui?.getChallengeResponse(challenge);
|
||||
return await apis?.ui?.getChallengeResponse(challenge);
|
||||
};
|
||||
|
||||
const captchaAtom = atom<string | undefined>(undefined);
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
Modal,
|
||||
} from '@affine/component/ui/modal';
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { apis } from '@affine/electron-api';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { workspaceManagerAtom } from '@affine/workspace/atom';
|
||||
@ -14,7 +15,6 @@ import {
|
||||
buildShowcaseWorkspace,
|
||||
initEmptyPage,
|
||||
} from '@toeverything/infra/blocksuite';
|
||||
import type { LoadDBFileResult } from '@toeverything/infra/type';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import type { KeyboardEvent } from 'react';
|
||||
import { useLayoutEffect } from 'react';
|
||||
@ -112,12 +112,12 @@ export const CreateWorkspaceModal = ({
|
||||
// after it is done, it will effectively add a new workspace to app-data folder
|
||||
// so after that, we will be able to load it via importLocalWorkspace
|
||||
(async () => {
|
||||
if (!window.apis) {
|
||||
if (!apis) {
|
||||
return;
|
||||
}
|
||||
logger.info('load db file');
|
||||
setStep(undefined);
|
||||
const result: LoadDBFileResult = await window.apis.dialog.loadDBFile();
|
||||
const result = await apis.dialog.loadDBFile();
|
||||
if (result.workspaceId && !canceled) {
|
||||
workspaceManager._addLocalWorkspace(result.workspaceId);
|
||||
onCreate(result.workspaceId);
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { apis } from '@affine/electron-api';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { Workspace, WorkspaceMetadata } from '@affine/workspace';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import type { SaveDBFileResult } from '@toeverything/infra/type';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useState } from 'react';
|
||||
|
||||
@ -30,8 +30,7 @@ export const ExportPanel = ({
|
||||
try {
|
||||
await workspace.engine.sync.waitForSynced();
|
||||
await workspace.engine.blob.sync();
|
||||
const result: SaveDBFileResult =
|
||||
await window.apis?.dialog.saveDBFileAs(workspaceId);
|
||||
const result = await apis?.dialog.saveDBFileAs(workspaceId);
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
} else if (!result?.canceled) {
|
||||
|
@ -2,17 +2,17 @@ import { FlexWrapper, toast } from '@affine/component';
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||
import { apis, events } from '@affine/electron-api';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { WorkspaceMetadata } from '@affine/workspace/metadata';
|
||||
import type { MoveDBFileResult } from '@toeverything/infra/type';
|
||||
import { useMemo } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
const useDBFileSecondaryPath = (workspaceId: string) => {
|
||||
const [path, setPath] = useState<string | undefined>(undefined);
|
||||
useEffect(() => {
|
||||
if (window.apis && window.events && environment.isDesktop) {
|
||||
window.apis?.workspace
|
||||
if (apis && events && environment.isDesktop) {
|
||||
apis?.workspace
|
||||
.getMeta(workspaceId)
|
||||
.then(meta => {
|
||||
setPath(meta.secondaryDBPath);
|
||||
@ -20,7 +20,7 @@ const useDBFileSecondaryPath = (workspaceId: string) => {
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
return window.events.workspace.onMetaChange((newMeta: any) => {
|
||||
return events.workspace.onMetaChange((newMeta: any) => {
|
||||
if (newMeta.workspaceId === workspaceId) {
|
||||
const meta = newMeta.meta;
|
||||
setPath(meta.secondaryDBPath);
|
||||
@ -43,7 +43,7 @@ export const StoragePanel = ({ workspaceMetadata }: StoragePanelProps) => {
|
||||
|
||||
const [moveToInProgress, setMoveToInProgress] = useState<boolean>(false);
|
||||
const onRevealDBFile = useCallback(() => {
|
||||
window.apis?.dialog.revealDBFile(workspaceId).catch(err => {
|
||||
apis?.dialog.revealDBFile(workspaceId).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}, [workspaceId]);
|
||||
@ -53,9 +53,9 @@ export const StoragePanel = ({ workspaceMetadata }: StoragePanelProps) => {
|
||||
return;
|
||||
}
|
||||
setMoveToInProgress(true);
|
||||
window.apis?.dialog
|
||||
apis?.dialog
|
||||
.moveDBFile(workspaceId)
|
||||
.then((result: MoveDBFileResult) => {
|
||||
.then(result => {
|
||||
if (!result?.error && !result?.canceled) {
|
||||
toast(t['Move folder success']());
|
||||
} else if (result?.error) {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { apis, events } from '@affine/electron-api';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { atomWithObservable } from 'jotai/utils';
|
||||
import { useCallback } from 'react';
|
||||
@ -8,7 +9,7 @@ import * as style from './style.css';
|
||||
const maximizedAtom = atomWithObservable(() => {
|
||||
return new Observable<boolean>(subscriber => {
|
||||
subscriber.next(false);
|
||||
return window.events?.ui.onMaximized(maximized => {
|
||||
return events?.ui.onMaximized(maximized => {
|
||||
return subscriber.next(maximized);
|
||||
});
|
||||
});
|
||||
@ -76,17 +77,17 @@ const unmaximizedSVG = (
|
||||
|
||||
export const WindowsAppControls = () => {
|
||||
const handleMinimizeApp = useCallback(() => {
|
||||
window.apis?.ui.handleMinimizeApp().catch(err => {
|
||||
apis?.ui.handleMinimizeApp().catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}, []);
|
||||
const handleMaximizeApp = useCallback(() => {
|
||||
window.apis?.ui.handleMaximizeApp().catch(err => {
|
||||
apis?.ui.handleMaximizeApp().catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}, []);
|
||||
const handleCloseApp = useCallback(() => {
|
||||
window.apis?.ui.handleCloseApp().catch(err => {
|
||||
apis?.ui.handleCloseApp().catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}, []);
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
} from '@affine/component/page-list';
|
||||
import { Menu } from '@affine/component/ui/menu';
|
||||
import { collectionsCRUDAtom } from '@affine/core/atoms/collections';
|
||||
import { apis, events } from '@affine/electron-api';
|
||||
import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { Workspace } from '@affine/workspace';
|
||||
@ -141,7 +142,7 @@ export const RootAppSidebar = ({
|
||||
// Listen to the "New Page" action from the menu
|
||||
useEffect(() => {
|
||||
if (environment.isDesktop) {
|
||||
return window.events?.applicationMenu.onNewPageAction(onClickNewPage);
|
||||
return events?.applicationMenu.onNewPageAction(onClickNewPage);
|
||||
}
|
||||
return;
|
||||
}, [onClickNewPage]);
|
||||
@ -149,7 +150,7 @@ export const RootAppSidebar = ({
|
||||
const sidebarOpen = useAtomValue(appSidebarOpenAtom);
|
||||
useEffect(() => {
|
||||
if (environment.isDesktop) {
|
||||
window.apis?.ui.handleSidebarVisibilityChange(sidebarOpen).catch(err => {
|
||||
apis?.ui.handleSidebarVisibilityChange(sidebarOpen).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import {
|
||||
resolveGlobalLoadingEventAtom,
|
||||
} from '@affine/component/global-loading';
|
||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||
import { apis } from '@affine/electron-api';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import {
|
||||
HtmlTransformer,
|
||||
@ -49,7 +50,7 @@ async function exportHandler({ page, type }: ExportHandlerOptions) {
|
||||
break;
|
||||
case 'pdf':
|
||||
if (environment.isDesktop && page.meta.mode === 'page') {
|
||||
await window.apis?.export.savePDFFileAs(
|
||||
await apis?.export.savePDFFileAs(
|
||||
(page.root as PageBlockModel).title.toString()
|
||||
);
|
||||
} else {
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { apis } from '@affine/electron-api';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import {
|
||||
type AppConfigSchema,
|
||||
AppConfigStorage,
|
||||
@ -13,11 +15,13 @@ class AppConfigProxy {
|
||||
value: AppConfigSchema = defaultAppConfig;
|
||||
|
||||
async getSync(): Promise<AppConfigSchema> {
|
||||
return (this.value = await window.apis.configStorage.get());
|
||||
assertExists(apis);
|
||||
return (this.value = await apis.configStorage.get());
|
||||
}
|
||||
|
||||
async setSync(): Promise<void> {
|
||||
await window.apis.configStorage.set(this.value);
|
||||
assertExists(apis);
|
||||
await apis.configStorage.set(this.value);
|
||||
}
|
||||
|
||||
get(): AppConfigSchema {
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { apis } from '@affine/electron-api';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { useCallback } from 'react';
|
||||
import { redirect } from 'react-router-dom';
|
||||
|
||||
@ -23,7 +25,8 @@ export const Component = () => {
|
||||
|
||||
const openApp = useCallback(() => {
|
||||
if (environment.isDesktop) {
|
||||
window.apis.ui.handleOpenMainApp().catch(err => {
|
||||
assertExists(apis);
|
||||
apis.ui.handleOpenMainApp().catch(err => {
|
||||
console.log('failed to open main app', err);
|
||||
});
|
||||
} else {
|
||||
|
@ -23,6 +23,9 @@
|
||||
{
|
||||
"path": "../../frontend/workspace"
|
||||
},
|
||||
{
|
||||
"path": "../../frontend/electron-api"
|
||||
},
|
||||
{
|
||||
"path": "../../common/debug"
|
||||
},
|
||||
|
14
packages/frontend/electron-api/package.json
Normal file
14
packages/frontend/electron-api/package.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "@affine/electron-api",
|
||||
"version": "0.10.3-canary.2",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@toeverything/infra": "workspace:*",
|
||||
"electron": "^27.1.0"
|
||||
}
|
||||
}
|
37
packages/frontend/electron-api/src/index.ts
Normal file
37
packages/frontend/electron-api/src/index.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import type {
|
||||
events as helperEvents,
|
||||
handlers as helperHandlers,
|
||||
} from '@affine/electron/helper/exposed';
|
||||
import type {
|
||||
events as mainEvents,
|
||||
handlers as mainHandlers,
|
||||
} from '@affine/electron/main/exposed';
|
||||
import type {
|
||||
affine as exposedAffineGlobal,
|
||||
appInfo as exposedAppInfo,
|
||||
} from '@affine/electron/preload/electron-api';
|
||||
|
||||
type MainHandlers = typeof mainHandlers;
|
||||
type HelperHandlers = typeof helperHandlers;
|
||||
type HelperEvents = typeof helperEvents;
|
||||
type MainEvents = typeof mainEvents;
|
||||
type ClientHandler = {
|
||||
[namespace in keyof MainHandlers]: {
|
||||
[method in keyof MainHandlers[namespace]]: MainHandlers[namespace][method] extends (
|
||||
arg0: any,
|
||||
...rest: infer A
|
||||
) => any
|
||||
? (...args: A) => Promise<ReturnType<MainHandlers[namespace][method]>>
|
||||
: never;
|
||||
};
|
||||
} & HelperHandlers;
|
||||
type ClientEvents = MainEvents & HelperEvents;
|
||||
|
||||
export const appInfo = (window as any).appInfo as typeof exposedAppInfo | null;
|
||||
export const apis = (window as any).apis as ClientHandler | null;
|
||||
export const events = (window as any).events as ClientEvents | null;
|
||||
export const affine = (window as any).affine as
|
||||
| typeof exposedAffineGlobal
|
||||
| null;
|
||||
|
||||
export type { UpdateMeta } from '@affine/electron/main/updater/event';
|
17
packages/frontend/electron-api/tsconfig.json
Normal file
17
packages/frontend/electron-api/tsconfig.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"include": ["./src"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"noEmit": false,
|
||||
"outDir": "lib"
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "../../common/infra"
|
||||
},
|
||||
{
|
||||
"path": "../../frontend/electron"
|
||||
}
|
||||
]
|
||||
}
|
@ -2,13 +2,6 @@ import path from 'node:path';
|
||||
|
||||
import { ValidationResult } from '@affine/native';
|
||||
import { WorkspaceVersion } from '@toeverything/infra/blocksuite';
|
||||
import type {
|
||||
FakeDialogResult,
|
||||
LoadDBFileResult,
|
||||
MoveDBFileResult,
|
||||
SaveDBFileResult,
|
||||
SelectDBFileLocationResult,
|
||||
} from '@toeverything/infra/type';
|
||||
import fs from 'fs-extra';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
@ -28,6 +21,45 @@ import {
|
||||
getWorkspacesBasePath,
|
||||
} from '../workspace/meta';
|
||||
|
||||
export type ErrorMessage =
|
||||
| 'DB_FILE_ALREADY_LOADED'
|
||||
| 'DB_FILE_PATH_INVALID'
|
||||
| 'DB_FILE_INVALID'
|
||||
| 'DB_FILE_MIGRATION_FAILED'
|
||||
| 'FILE_ALREADY_EXISTS'
|
||||
| 'UNKNOWN_ERROR';
|
||||
|
||||
export interface LoadDBFileResult {
|
||||
workspaceId?: string;
|
||||
error?: ErrorMessage;
|
||||
canceled?: boolean;
|
||||
}
|
||||
|
||||
export interface SaveDBFileResult {
|
||||
filePath?: string;
|
||||
canceled?: boolean;
|
||||
error?: ErrorMessage;
|
||||
}
|
||||
|
||||
export interface SelectDBFileLocationResult {
|
||||
filePath?: string;
|
||||
error?: ErrorMessage;
|
||||
canceled?: boolean;
|
||||
}
|
||||
|
||||
export interface MoveDBFileResult {
|
||||
filePath?: string;
|
||||
error?: ErrorMessage;
|
||||
canceled?: boolean;
|
||||
}
|
||||
|
||||
// provide a backdoor to set dialog path for testing in playwright
|
||||
export interface FakeDialogResult {
|
||||
canceled?: boolean;
|
||||
filePath?: string;
|
||||
filePaths?: string[];
|
||||
}
|
||||
|
||||
// NOTE:
|
||||
// we are using native dialogs because HTML dialogs do not give full file paths
|
||||
|
||||
|
@ -1,25 +1,13 @@
|
||||
import type {
|
||||
DBHandlers,
|
||||
DialogHandlers,
|
||||
WorkspaceHandlers,
|
||||
} from '@toeverything/infra/type';
|
||||
|
||||
import { dbEvents, dbHandlers } from './db';
|
||||
import { dialogHandlers } from './dialog';
|
||||
import { provideExposed } from './provide';
|
||||
import { workspaceEvents, workspaceHandlers } from './workspace';
|
||||
|
||||
type AllHandlers = {
|
||||
db: DBHandlers;
|
||||
workspace: WorkspaceHandlers;
|
||||
dialog: DialogHandlers;
|
||||
};
|
||||
|
||||
export const handlers = {
|
||||
db: dbHandlers,
|
||||
workspace: workspaceHandlers,
|
||||
dialog: dialogHandlers,
|
||||
} satisfies AllHandlers;
|
||||
};
|
||||
|
||||
export const events = {
|
||||
db: dbEvents,
|
||||
|
@ -1,6 +1,6 @@
|
||||
import type { RendererToHelper } from '@toeverything/infra/preload/electron';
|
||||
import { AsyncCall } from 'async-call-rpc';
|
||||
|
||||
import type { RendererToHelper } from '../shared/type';
|
||||
import { events, handlers } from './exposed';
|
||||
import { logger } from './logger';
|
||||
|
||||
|
@ -1,10 +1,7 @@
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type {
|
||||
HelperToMain,
|
||||
MainToHelper,
|
||||
} from '@toeverything/infra/preload/electron';
|
||||
import { AsyncCall } from 'async-call-rpc';
|
||||
|
||||
import type { HelperToMain, MainToHelper } from '../shared/type';
|
||||
import { exposed } from './provide';
|
||||
|
||||
const helperToMainServer: HelperToMain = {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import type { ExposedMeta } from '@toeverything/infra/preload/electron';
|
||||
import type { ExposedMeta } from '../shared/type';
|
||||
|
||||
/**
|
||||
* A naive DI implementation to get rid of circular dependency.
|
||||
|
@ -1,12 +1,3 @@
|
||||
import type {
|
||||
ClipboardHandlerManager,
|
||||
ConfigStorageHandlerManager,
|
||||
DebugHandlerManager,
|
||||
ExportHandlerManager,
|
||||
UIHandlerManager,
|
||||
UnwrapManagerHandlerToServerSide,
|
||||
UpdaterHandlerManager,
|
||||
} from '@toeverything/infra/index';
|
||||
import { ipcMain } from 'electron';
|
||||
|
||||
import { clipboardHandlers } from './clipboard';
|
||||
@ -25,33 +16,6 @@ export const debugHandlers = {
|
||||
},
|
||||
};
|
||||
|
||||
type AllHandlers = {
|
||||
debug: UnwrapManagerHandlerToServerSide<
|
||||
Electron.IpcMainInvokeEvent,
|
||||
DebugHandlerManager
|
||||
>;
|
||||
clipboard: UnwrapManagerHandlerToServerSide<
|
||||
Electron.IpcMainInvokeEvent,
|
||||
ClipboardHandlerManager
|
||||
>;
|
||||
export: UnwrapManagerHandlerToServerSide<
|
||||
Electron.IpcMainInvokeEvent,
|
||||
ExportHandlerManager
|
||||
>;
|
||||
ui: UnwrapManagerHandlerToServerSide<
|
||||
Electron.IpcMainInvokeEvent,
|
||||
UIHandlerManager
|
||||
>;
|
||||
updater: UnwrapManagerHandlerToServerSide<
|
||||
Electron.IpcMainInvokeEvent,
|
||||
UpdaterHandlerManager
|
||||
>;
|
||||
configStorage: UnwrapManagerHandlerToServerSide<
|
||||
Electron.IpcMainInvokeEvent,
|
||||
ConfigStorageHandlerManager
|
||||
>;
|
||||
};
|
||||
|
||||
// Note: all of these handlers will be the single-source-of-truth for the apis exposed to the renderer process
|
||||
export const allHandlers = {
|
||||
debug: debugHandlers,
|
||||
@ -60,7 +24,7 @@ export const allHandlers = {
|
||||
export: exportHandlers,
|
||||
updater: updaterHandlers,
|
||||
configStorage: configStorageHandlers,
|
||||
} satisfies AllHandlers;
|
||||
};
|
||||
|
||||
export const registerHandlers = () => {
|
||||
// TODO: listen to namespace instead of individual event types
|
||||
|
@ -1,9 +1,5 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import type {
|
||||
HelperToMain,
|
||||
MainToHelper,
|
||||
} from '@toeverything/infra/preload/electron';
|
||||
import { type _AsyncVersionOf, AsyncCall } from 'async-call-rpc';
|
||||
import {
|
||||
app,
|
||||
@ -15,6 +11,7 @@ import {
|
||||
type WebContents,
|
||||
} from 'electron';
|
||||
|
||||
import type { HelperToMain, MainToHelper } from '../shared/type';
|
||||
import { MessageEventChannel } from '../shared/utils';
|
||||
import { logger } from './logger';
|
||||
|
||||
|
@ -1,57 +1,15 @@
|
||||
import { contextBridge, ipcRenderer } from 'electron';
|
||||
import { contextBridge } from 'electron';
|
||||
|
||||
(async () => {
|
||||
const { appInfo, getElectronAPIs } = await import(
|
||||
'@toeverything/infra/preload/electron'
|
||||
);
|
||||
const { apis, events } = getElectronAPIs();
|
||||
import { affine, appInfo, getElectronAPIs } from './electron-api';
|
||||
|
||||
contextBridge.exposeInMainWorld('appInfo', appInfo);
|
||||
contextBridge.exposeInMainWorld('apis', apis);
|
||||
contextBridge.exposeInMainWorld('events', events);
|
||||
const { apis, events } = getElectronAPIs();
|
||||
|
||||
// Credit to microsoft/vscode
|
||||
const globals = {
|
||||
ipcRenderer: {
|
||||
send(channel: string, ...args: any[]) {
|
||||
ipcRenderer.send(channel, ...args);
|
||||
},
|
||||
contextBridge.exposeInMainWorld('appInfo', appInfo);
|
||||
contextBridge.exposeInMainWorld('apis', apis);
|
||||
contextBridge.exposeInMainWorld('events', events);
|
||||
|
||||
invoke(channel: string, ...args: any[]) {
|
||||
return ipcRenderer.invoke(channel, ...args);
|
||||
},
|
||||
|
||||
on(
|
||||
channel: string,
|
||||
listener: (event: Electron.IpcRendererEvent, ...args: any[]) => void
|
||||
) {
|
||||
ipcRenderer.on(channel, listener);
|
||||
return this;
|
||||
},
|
||||
|
||||
once(
|
||||
channel: string,
|
||||
listener: (event: Electron.IpcRendererEvent, ...args: any[]) => void
|
||||
) {
|
||||
ipcRenderer.once(channel, listener);
|
||||
return this;
|
||||
},
|
||||
|
||||
removeListener(
|
||||
channel: string,
|
||||
listener: (event: Electron.IpcRendererEvent, ...args: any[]) => void
|
||||
) {
|
||||
ipcRenderer.removeListener(channel, listener);
|
||||
return this;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
contextBridge.exposeInMainWorld('affine', globals);
|
||||
} catch (error) {
|
||||
console.error('Failed to expose affine APIs to window object!', error);
|
||||
}
|
||||
})().catch(err => {
|
||||
console.error('Failed to bootstrap preload script!', err);
|
||||
});
|
||||
try {
|
||||
contextBridge.exposeInMainWorld('affine', affine);
|
||||
} catch (error) {
|
||||
console.error('Failed to expose affine APIs to window object!', error);
|
||||
}
|
||||
|
@ -1,37 +1,50 @@
|
||||
// Please add modules to `external` in `rollupOptions` to avoid wrong bundling.
|
||||
import { AsyncCall, type EventBasedChannel } from 'async-call-rpc';
|
||||
import type { app, dialog, shell } from 'electron';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { Subject } from 'rxjs';
|
||||
import { z } from 'zod';
|
||||
|
||||
export interface ExposedMeta {
|
||||
handlers: [string, string[]][];
|
||||
events: [string, string[]][];
|
||||
}
|
||||
import type {
|
||||
ExposedMeta,
|
||||
HelperToRenderer,
|
||||
RendererToHelper,
|
||||
} from '../shared/type';
|
||||
|
||||
// render <-> helper
|
||||
export interface RendererToHelper {
|
||||
postEvent: (channel: string, ...args: any[]) => void;
|
||||
}
|
||||
export const affine = {
|
||||
ipcRenderer: {
|
||||
send(channel: string, ...args: any[]) {
|
||||
ipcRenderer.send(channel, ...args);
|
||||
},
|
||||
|
||||
export interface HelperToRenderer {
|
||||
[key: string]: (...args: any[]) => Promise<any>;
|
||||
}
|
||||
invoke(channel: string, ...args: any[]) {
|
||||
return ipcRenderer.invoke(channel, ...args);
|
||||
},
|
||||
|
||||
// helper <-> main
|
||||
export interface HelperToMain {
|
||||
getMeta: () => ExposedMeta;
|
||||
}
|
||||
on(
|
||||
channel: string,
|
||||
listener: (event: Electron.IpcRendererEvent, ...args: any[]) => void
|
||||
) {
|
||||
ipcRenderer.on(channel, listener);
|
||||
return this;
|
||||
},
|
||||
|
||||
export type MainToHelper = Pick<
|
||||
typeof dialog & typeof shell & typeof app,
|
||||
| 'showOpenDialog'
|
||||
| 'showSaveDialog'
|
||||
| 'openExternal'
|
||||
| 'showItemInFolder'
|
||||
| 'getPath'
|
||||
>;
|
||||
once(
|
||||
channel: string,
|
||||
listener: (event: Electron.IpcRendererEvent, ...args: any[]) => void
|
||||
) {
|
||||
ipcRenderer.once(channel, listener);
|
||||
return this;
|
||||
},
|
||||
|
||||
removeListener(
|
||||
channel: string,
|
||||
listener: (event: Electron.IpcRendererEvent, ...args: any[]) => void
|
||||
) {
|
||||
ipcRenderer.removeListener(channel, listener);
|
||||
return this;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function getElectronAPIs() {
|
||||
const mainAPIs = getMainAPIs();
|
29
packages/frontend/electron/src/shared/type.ts
Normal file
29
packages/frontend/electron/src/shared/type.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import type { app, dialog, shell } from 'electron';
|
||||
|
||||
export interface ExposedMeta {
|
||||
handlers: [string, string[]][];
|
||||
events: [string, string[]][];
|
||||
}
|
||||
|
||||
// render <-> helper
|
||||
export interface RendererToHelper {
|
||||
postEvent: (channel: string, ...args: any[]) => void;
|
||||
}
|
||||
|
||||
export interface HelperToRenderer {
|
||||
[key: string]: (...args: any[]) => Promise<any>;
|
||||
}
|
||||
|
||||
// helper <-> main
|
||||
export interface HelperToMain {
|
||||
getMeta: () => ExposedMeta;
|
||||
}
|
||||
|
||||
export type MainToHelper = Pick<
|
||||
typeof dialog & typeof shell & typeof app,
|
||||
| 'showOpenDialog'
|
||||
| 'showSaveDialog'
|
||||
| 'openExternal'
|
||||
| 'showItemInFolder'
|
||||
| 'getPath'
|
||||
>;
|
@ -18,6 +18,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@affine/debug": "workspace:*",
|
||||
"@affine/electron-api": "workspace:*",
|
||||
"@affine/env": "workspace:*",
|
||||
"@affine/workspace": "workspace:*",
|
||||
"@blocksuite/block-std": "0.11.0-nightly-202312220916-e3abcbb",
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { apis, events, type UpdateMeta } from '@affine/electron-api';
|
||||
import { isBrowser } from '@affine/env/constant';
|
||||
import { appSettingAtom } from '@toeverything/infra/atom';
|
||||
import type { UpdateMeta } from '@toeverything/infra/type';
|
||||
import { atom, useAtom, useAtomValue } from 'jotai';
|
||||
import { atomWithObservable, atomWithStorage } from 'jotai/utils';
|
||||
import { useCallback, useState } from 'react';
|
||||
@ -47,21 +47,21 @@ function rpcToObservable<
|
||||
// download complete, ready to install
|
||||
export const updateReadyAtom = atomWithObservable(() => {
|
||||
return rpcToObservable(null as UpdateMeta | null, {
|
||||
event: window.events?.updater.onUpdateReady,
|
||||
event: events?.updater.onUpdateReady,
|
||||
});
|
||||
});
|
||||
|
||||
// update available, but not downloaded yet
|
||||
export const updateAvailableAtom = atomWithObservable(() => {
|
||||
return rpcToObservable(null as UpdateMeta | null, {
|
||||
event: window.events?.updater.onUpdateAvailable,
|
||||
event: events?.updater.onUpdateAvailable,
|
||||
});
|
||||
});
|
||||
|
||||
// downloading new update
|
||||
export const downloadProgressAtom = atomWithObservable(() => {
|
||||
return rpcToObservable(null as number | null, {
|
||||
event: window.events?.updater.onDownloadProgress,
|
||||
event: events?.updater.onDownloadProgress,
|
||||
});
|
||||
});
|
||||
|
||||
@ -76,7 +76,7 @@ export const currentVersionAtom = atom(async () => {
|
||||
if (!isBrowser) {
|
||||
return null;
|
||||
}
|
||||
const currentVersion = await window.apis?.updater.currentVersion();
|
||||
const currentVersion = await apis?.updater.currentVersion();
|
||||
return currentVersion;
|
||||
});
|
||||
|
||||
@ -121,7 +121,7 @@ export const useAppUpdater = () => {
|
||||
const quitAndInstall = useCallback(() => {
|
||||
if (updateReady) {
|
||||
setAppQuitting(true);
|
||||
window.apis?.updater.quitAndInstall().catch(err => {
|
||||
apis?.updater.quitAndInstall().catch(err => {
|
||||
// TODO: add error toast here
|
||||
console.error(err);
|
||||
});
|
||||
@ -134,7 +134,7 @@ export const useAppUpdater = () => {
|
||||
}
|
||||
setCheckingForUpdates(true);
|
||||
try {
|
||||
const updateInfo = await window.apis?.updater.checkForUpdates();
|
||||
const updateInfo = await apis?.updater.checkForUpdates();
|
||||
return updateInfo?.version ?? false;
|
||||
} catch (err) {
|
||||
console.error('Error checking for updates:', err);
|
||||
@ -145,7 +145,7 @@ export const useAppUpdater = () => {
|
||||
}, [checkingForUpdates, setCheckingForUpdates]);
|
||||
|
||||
const downloadUpdate = useCallback(() => {
|
||||
window.apis?.updater.downloadUpdate().catch(err => {
|
||||
apis?.updater.downloadUpdate().catch(err => {
|
||||
console.error('Error downloading update:', err);
|
||||
});
|
||||
}, []);
|
||||
|
@ -11,6 +11,7 @@
|
||||
{ "path": "../../common/y-indexeddb" },
|
||||
{ "path": "../../common/debug" },
|
||||
{ "path": "../../common/infra" },
|
||||
{ "path": "../electron-api" },
|
||||
{ "path": "../workspace" }
|
||||
]
|
||||
}
|
||||
|
@ -14,6 +14,7 @@
|
||||
"dependencies": {
|
||||
"@affine-test/fixtures": "workspace:*",
|
||||
"@affine/debug": "workspace:*",
|
||||
"@affine/electron-api": "workspace:*",
|
||||
"@affine/env": "workspace:*",
|
||||
"@affine/graphql": "workspace:*",
|
||||
"@toeverything/infra": "workspace:*",
|
||||
|
@ -1,15 +1,16 @@
|
||||
import { apis } from '@affine/electron-api';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
|
||||
import type { BlobStorage } from '../../engine/blob';
|
||||
import { bufferToBlob } from '../../utils/buffer-to-blob';
|
||||
|
||||
export const createSQLiteBlobStorage = (workspaceId: string): BlobStorage => {
|
||||
const apis = window.apis;
|
||||
assertExists(apis);
|
||||
return {
|
||||
name: 'sqlite',
|
||||
readonly: false,
|
||||
get: async (key: string) => {
|
||||
assertExists(apis);
|
||||
const buffer = await apis.db.getBlob(workspaceId, key);
|
||||
if (buffer) {
|
||||
return bufferToBlob(buffer);
|
||||
@ -17,6 +18,7 @@ export const createSQLiteBlobStorage = (workspaceId: string): BlobStorage => {
|
||||
return null;
|
||||
},
|
||||
set: async (key: string, value: Blob) => {
|
||||
assertExists(apis);
|
||||
await apis.db.addBlob(
|
||||
workspaceId,
|
||||
key,
|
||||
@ -25,9 +27,11 @@ export const createSQLiteBlobStorage = (workspaceId: string): BlobStorage => {
|
||||
return key;
|
||||
},
|
||||
delete: async (key: string) => {
|
||||
assertExists(apis);
|
||||
return apis.db.deleteBlob(workspaceId, key);
|
||||
},
|
||||
list: async () => {
|
||||
assertExists(apis);
|
||||
return apis.db.getBlobKeys(workspaceId);
|
||||
},
|
||||
};
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { apis } from '@affine/electron-api';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
||||
import { difference } from 'lodash-es';
|
||||
@ -96,8 +97,8 @@ export function createLocalWorkspaceListProvider(): WorkspaceListProvider {
|
||||
JSON.stringify(allWorkspaceIDs.filter(x => x !== workspaceId))
|
||||
);
|
||||
|
||||
if (window.apis && environment.isDesktop) {
|
||||
await window.apis.workspace.delete(workspaceId);
|
||||
if (apis && environment.isDesktop) {
|
||||
await apis.workspace.delete(workspaceId);
|
||||
}
|
||||
|
||||
// notify all browser tabs, so they can update their workspace list
|
||||
|
@ -1,16 +1,20 @@
|
||||
import { apis } from '@affine/electron-api';
|
||||
import { encodeStateVectorFromUpdate } from 'yjs';
|
||||
|
||||
import type { SyncStorage } from '../../engine/sync';
|
||||
|
||||
export function createSQLiteStorage(workspaceId: string): SyncStorage {
|
||||
if (!window.apis?.db) {
|
||||
if (!apis?.db) {
|
||||
throw new Error('sqlite datasource is not available');
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'sqlite',
|
||||
async pull(docId, _state) {
|
||||
const update = await window.apis.db.getDocAsUpdates(
|
||||
if (!apis?.db) {
|
||||
throw new Error('sqlite datasource is not available');
|
||||
}
|
||||
const update = await apis.db.getDocAsUpdates(
|
||||
workspaceId,
|
||||
workspaceId === docId ? undefined : docId
|
||||
);
|
||||
@ -25,7 +29,10 @@ export function createSQLiteStorage(workspaceId: string): SyncStorage {
|
||||
return null;
|
||||
},
|
||||
async push(docId, data) {
|
||||
return window.apis.db.applyDocUpdate(
|
||||
if (!apis?.db) {
|
||||
throw new Error('sqlite datasource is not available');
|
||||
}
|
||||
return apis.db.applyDocUpdate(
|
||||
workspaceId,
|
||||
data,
|
||||
workspaceId === docId ? undefined : docId
|
||||
|
@ -10,6 +10,7 @@
|
||||
{ "path": "../../common/env" },
|
||||
{ "path": "../../common/debug" },
|
||||
{ "path": "../../common/infra" },
|
||||
{ "path": "../../frontend/graphql" }
|
||||
{ "path": "../../frontend/graphql" },
|
||||
{ "path": "../../frontend/electron-api" }
|
||||
]
|
||||
}
|
||||
|
@ -1,10 +1,17 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import type { apis } from '@affine/electron-api';
|
||||
import { test } from '@affine-test/kit/electron';
|
||||
import { clickSideBarCurrentWorkspaceBanner } from '@affine-test/kit/utils/sidebar';
|
||||
import { expect } from '@playwright/test';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
apis: typeof apis;
|
||||
}
|
||||
}
|
||||
|
||||
test('check workspace has a DB file', async ({ appInfo, workspace }) => {
|
||||
const w = await workspace.current();
|
||||
const dbPath = path.join(
|
||||
|
@ -7,6 +7,7 @@
|
||||
"devDependencies": {
|
||||
"@affine-test/fixtures": "workspace:*",
|
||||
"@affine-test/kit": "workspace:*",
|
||||
"@affine/electron-api": "workspace:*",
|
||||
"@playwright/test": "^1.39.0",
|
||||
"@types/fs-extra": "^11.0.2",
|
||||
"fs-extra": "^11.1.1",
|
||||
|
@ -11,6 +11,9 @@
|
||||
},
|
||||
{
|
||||
"path": "../../tests/fixtures"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/frontend/electron-api"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -10,6 +10,7 @@
|
||||
"./e2e-enhance/*": "./e2e-enhance/*.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@affine/electron-api": "workspace:*",
|
||||
"@node-rs/argon2": "^1.5.2",
|
||||
"@playwright/test": "^1.39.0",
|
||||
"express": "^4.18.2",
|
||||
|
@ -1,11 +1,18 @@
|
||||
import type { affine } from '@affine/electron-api';
|
||||
// Credit: https://github.com/spaceagetv/electron-playwright-helpers/blob/main/src/ipc_helpers.ts
|
||||
import type { Page } from '@playwright/test';
|
||||
import type { ElectronApplication } from 'playwright';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
affine: typeof affine;
|
||||
}
|
||||
}
|
||||
|
||||
export function ipcRendererInvoke(page: Page, channel: string, ...args: any[]) {
|
||||
return page.evaluate(
|
||||
({ channel, args }) => {
|
||||
return window.affine.ipcRenderer.invoke(channel, ...args);
|
||||
return window.affine?.ipcRenderer.invoke(channel, ...args);
|
||||
},
|
||||
{ channel, args }
|
||||
);
|
||||
@ -14,7 +21,7 @@ export function ipcRendererInvoke(page: Page, channel: string, ...args: any[]) {
|
||||
export function ipcRendererSend(page: Page, channel: string, ...args: any[]) {
|
||||
return page.evaluate(
|
||||
({ channel, args }) => {
|
||||
window.affine.ipcRenderer.send(channel, ...args);
|
||||
window.affine?.ipcRenderer.send(channel, ...args);
|
||||
},
|
||||
{ channel, args }
|
||||
);
|
||||
|
47
tools/@types/env/__all.d.ts
vendored
47
tools/@types/env/__all.d.ts
vendored
@ -1,53 +1,6 @@
|
||||
import type { Environment, RuntimeConfig } from '@affine/env/global';
|
||||
import type {
|
||||
ConfigStorageHandlerManager,
|
||||
DBHandlerManager,
|
||||
DebugHandlerManager,
|
||||
DialogHandlerManager,
|
||||
EventMap,
|
||||
ExportHandlerManager,
|
||||
UIHandlerManager,
|
||||
UnwrapManagerHandlerToClientSide,
|
||||
UpdaterHandlerManager,
|
||||
WorkspaceHandlerManager,
|
||||
} from '@toeverything/infra/index';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
appInfo: {
|
||||
electron: boolean;
|
||||
};
|
||||
apis: {
|
||||
db: UnwrapManagerHandlerToClientSide<DBHandlerManager>;
|
||||
debug: UnwrapManagerHandlerToClientSide<DebugHandlerManager>;
|
||||
dialog: UnwrapManagerHandlerToClientSide<DialogHandlerManager>;
|
||||
export: UnwrapManagerHandlerToClientSide<ExportHandlerManager>;
|
||||
ui: UnwrapManagerHandlerToClientSide<UIHandlerManager>;
|
||||
updater: UnwrapManagerHandlerToClientSide<UpdaterHandlerManager>;
|
||||
workspace: UnwrapManagerHandlerToClientSide<WorkspaceHandlerManager>;
|
||||
configStorage: UnwrapManagerHandlerToClientSide<ConfigStorageHandlerManager>;
|
||||
};
|
||||
events: EventMap;
|
||||
affine: {
|
||||
ipcRenderer: {
|
||||
send(channel: string, ...args: any[]): void;
|
||||
invoke(channel: string, ...args: any[]): Promise<any>;
|
||||
on(
|
||||
channel: string,
|
||||
listener: (event: Electron.IpcRendererEvent, ...args: any[]) => void
|
||||
): this;
|
||||
once(
|
||||
channel: string,
|
||||
listener: (event: Electron.IpcRendererEvent, ...args: any[]) => void
|
||||
): this;
|
||||
removeListener(
|
||||
channel: string,
|
||||
listener: (event: Electron.IpcRendererEvent, ...args: any[]) => void
|
||||
): this;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
var process: {
|
||||
env: Record<string, string>;
|
||||
|
3
tools/@types/env/package.json
vendored
3
tools/@types/env/package.json
vendored
@ -4,8 +4,7 @@
|
||||
"types": "./__all.d.ts",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@affine/env": "workspace:*",
|
||||
"@toeverything/infra": "workspace:*"
|
||||
"@affine/env": "workspace:*"
|
||||
},
|
||||
"version": "0.11.0"
|
||||
}
|
||||
|
16
yarn.lock
16
yarn.lock
@ -115,6 +115,7 @@ __metadata:
|
||||
dependencies:
|
||||
"@affine-test/fixtures": "workspace:*"
|
||||
"@affine-test/kit": "workspace:*"
|
||||
"@affine/electron-api": "workspace:*"
|
||||
"@playwright/test": "npm:^1.39.0"
|
||||
"@types/fs-extra": "npm:^11.0.2"
|
||||
fs-extra: "npm:^11.1.1"
|
||||
@ -156,6 +157,7 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@affine-test/kit@workspace:tests/kit"
|
||||
dependencies:
|
||||
"@affine/electron-api": "workspace:*"
|
||||
"@node-rs/argon2": "npm:^1.5.2"
|
||||
"@playwright/test": "npm:^1.39.0"
|
||||
express: "npm:^4.18.2"
|
||||
@ -209,6 +211,7 @@ __metadata:
|
||||
resolution: "@affine/component@workspace:packages/frontend/component"
|
||||
dependencies:
|
||||
"@affine/debug": "workspace:*"
|
||||
"@affine/electron-api": "workspace:*"
|
||||
"@affine/graphql": "workspace:*"
|
||||
"@affine/i18n": "workspace:*"
|
||||
"@affine/workspace": "workspace:*"
|
||||
@ -309,6 +312,7 @@ __metadata:
|
||||
"@affine/cmdk": "workspace:*"
|
||||
"@affine/component": "workspace:*"
|
||||
"@affine/debug": "workspace:*"
|
||||
"@affine/electron-api": "workspace:*"
|
||||
"@affine/env": "workspace:*"
|
||||
"@affine/graphql": "workspace:*"
|
||||
"@affine/i18n": "workspace:*"
|
||||
@ -418,6 +422,15 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@affine/electron-api@workspace:*, @affine/electron-api@workspace:packages/frontend/electron-api":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@affine/electron-api@workspace:packages/frontend/electron-api"
|
||||
dependencies:
|
||||
"@toeverything/infra": "workspace:*"
|
||||
electron: "npm:^27.1.0"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@affine/electron@workspace:packages/frontend/electron":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@affine/electron@workspace:packages/frontend/electron"
|
||||
@ -788,6 +801,7 @@ __metadata:
|
||||
dependencies:
|
||||
"@affine-test/fixtures": "workspace:*"
|
||||
"@affine/debug": "workspace:*"
|
||||
"@affine/electron-api": "workspace:*"
|
||||
"@affine/env": "workspace:*"
|
||||
"@affine/graphql": "workspace:*"
|
||||
"@testing-library/react": "npm:^14.0.0"
|
||||
@ -13438,6 +13452,7 @@ __metadata:
|
||||
resolution: "@toeverything/hooks@workspace:packages/frontend/hooks"
|
||||
dependencies:
|
||||
"@affine/debug": "workspace:*"
|
||||
"@affine/electron-api": "workspace:*"
|
||||
"@affine/env": "workspace:*"
|
||||
"@affine/workspace": "workspace:*"
|
||||
"@blocksuite/block-std": "npm:0.11.0-nightly-202312220916-e3abcbb"
|
||||
@ -13646,7 +13661,6 @@ __metadata:
|
||||
resolution: "@types/affine__env@workspace:tools/@types/env"
|
||||
dependencies:
|
||||
"@affine/env": "workspace:*"
|
||||
"@toeverything/infra": "workspace:*"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user