mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-15 09:06:19 +08:00
@@ -1,9 +1,10 @@
|
||||
import { type Framework } from '@toeverything/infra';
|
||||
|
||||
import { DesktopApiService } from '../desktop-api';
|
||||
import { AppTabsHeaderService } from './services/app-tabs-header-service';
|
||||
|
||||
export { AppTabsHeader } from './views/app-tabs-header';
|
||||
|
||||
export function configureAppTabsHeaderModule(framework: Framework) {
|
||||
framework.service(AppTabsHeaderService);
|
||||
framework.service(AppTabsHeaderService, [DesktopApiService]);
|
||||
}
|
||||
|
||||
+15
-14
@@ -1,24 +1,25 @@
|
||||
import { apis, events } from '@affine/electron-api';
|
||||
import { LiveData, Service } from '@toeverything/infra';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import type { ClientEvents, DesktopApiService } from '../../desktop-api';
|
||||
|
||||
export type TabStatus = Parameters<
|
||||
Parameters<NonNullable<typeof events>['ui']['onTabsStatusChange']>[0]
|
||||
Parameters<NonNullable<ClientEvents>['ui']['onTabsStatusChange']>[0]
|
||||
>[0][number];
|
||||
|
||||
export class AppTabsHeaderService extends Service {
|
||||
constructor() {
|
||||
constructor(private readonly desktopApi: DesktopApiService) {
|
||||
super();
|
||||
}
|
||||
|
||||
tabsStatus$ = LiveData.from<TabStatus[]>(
|
||||
new Observable(subscriber => {
|
||||
let unsub: (() => void) | undefined;
|
||||
apis?.ui
|
||||
this.desktopApi.handler.ui
|
||||
.getTabsStatus()
|
||||
.then(tabs => {
|
||||
subscriber.next(tabs);
|
||||
unsub = events?.ui.onTabsStatusChange(tabs => {
|
||||
unsub = this.desktopApi.events.ui.onTabsStatusChange(tabs => {
|
||||
subscriber.next(tabs);
|
||||
});
|
||||
})
|
||||
@@ -31,20 +32,20 @@ export class AppTabsHeaderService extends Service {
|
||||
[]
|
||||
);
|
||||
|
||||
showContextMenu = apis?.ui.showTabContextMenu;
|
||||
showContextMenu = this.desktopApi.handler.ui.showTabContextMenu;
|
||||
|
||||
activateView = apis?.ui.activateView;
|
||||
activateView = this.desktopApi.handler.ui.activateView;
|
||||
|
||||
closeTab = apis?.ui.closeTab;
|
||||
closeTab = this.desktopApi.handler.ui.closeTab;
|
||||
|
||||
onAddTab = apis?.ui.addTab;
|
||||
onAddTab = this.desktopApi.handler.ui.addTab;
|
||||
|
||||
onAddDocTab = async (
|
||||
docId: string,
|
||||
targetTabId?: string,
|
||||
edge?: 'left' | 'right'
|
||||
) => {
|
||||
await apis?.ui.addTab({
|
||||
await this.desktopApi.handler.ui.addTab({
|
||||
view: {
|
||||
path: {
|
||||
pathname: '/' + docId,
|
||||
@@ -60,7 +61,7 @@ export class AppTabsHeaderService extends Service {
|
||||
targetTabId?: string,
|
||||
edge?: 'left' | 'right'
|
||||
) => {
|
||||
await apis?.ui.addTab({
|
||||
await this.desktopApi.handler.ui.addTab({
|
||||
view: {
|
||||
path: {
|
||||
pathname: '/tag/' + tagId,
|
||||
@@ -76,7 +77,7 @@ export class AppTabsHeaderService extends Service {
|
||||
targetTabId?: string,
|
||||
edge?: 'left' | 'right'
|
||||
) => {
|
||||
await apis?.ui.addTab({
|
||||
await this.desktopApi.handler.ui.addTab({
|
||||
view: {
|
||||
path: {
|
||||
pathname: '/collection/' + collectionId,
|
||||
@@ -87,7 +88,7 @@ export class AppTabsHeaderService extends Service {
|
||||
});
|
||||
};
|
||||
|
||||
onToggleRightSidebar = apis?.ui.toggleRightSidebar;
|
||||
onToggleRightSidebar = this.desktopApi.handler.ui.toggleRightSidebar;
|
||||
|
||||
moveTab = apis?.ui.moveTab;
|
||||
moveTab = this.desktopApi.handler.ui.moveTab;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { useCatchEventCallback } from '@affine/core/components/hooks/use-catch-event-hook';
|
||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||
import { apis, events } from '@affine/electron-api';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { CloseIcon, PlusIcon, RightSidebarIcon } from '@blocksuite/icons/rc';
|
||||
@@ -31,6 +30,7 @@ import {
|
||||
} from 'react';
|
||||
|
||||
import { AppSidebarService } from '../../app-sidebar';
|
||||
import { DesktopApiService } from '../../desktop-api';
|
||||
import { iconNameToIcon } from '../../workbench/constants';
|
||||
import { DesktopStateSynchronizer } from '../../workbench/services/desktop-state-synchronizer';
|
||||
import {
|
||||
@@ -274,17 +274,18 @@ const WorkbenchTab = ({
|
||||
};
|
||||
|
||||
const useIsFullScreen = () => {
|
||||
const desktopApi = useServiceOptional(DesktopApiService);
|
||||
const [fullScreen, setFullScreen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
apis?.ui
|
||||
desktopApi?.handler.ui
|
||||
.isFullScreen()
|
||||
.then(setFullScreen)
|
||||
.then(() => {
|
||||
events?.ui.onFullScreen(setFullScreen);
|
||||
desktopApi?.events.ui.onFullScreen(setFullScreen);
|
||||
})
|
||||
.catch(console.error);
|
||||
}, []);
|
||||
}, [desktopApi?.events.ui, desktopApi?.handler.ui]);
|
||||
return fullScreen;
|
||||
};
|
||||
|
||||
@@ -309,6 +310,8 @@ export const AppTabsHeader = ({
|
||||
const isWindowsDesktop = BUILD_CONFIG.isElectron && environment.isWindows;
|
||||
const fullScreen = useIsFullScreen();
|
||||
|
||||
const desktopApi = useService(DesktopApiService);
|
||||
|
||||
const tabsHeaderService = useService(AppTabsHeaderService);
|
||||
const tabs = useLiveData(tabsHeaderService.tabsStatus$);
|
||||
|
||||
@@ -328,9 +331,9 @@ export const AppTabsHeader = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (mode === 'app') {
|
||||
apis?.ui.pingAppLayoutReady().catch(console.error);
|
||||
desktopApi.handler.ui.pingAppLayoutReady().catch(console.error);
|
||||
}
|
||||
}, [mode]);
|
||||
}, [mode, desktopApi]);
|
||||
|
||||
const onDrop = useAsyncCallback(
|
||||
async (data: DropTargetDropEvent<AffineDNDData>, targetId?: string) => {
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { notify } from '@affine/component';
|
||||
import { AIProvider } from '@affine/core/blocksuite/presets/ai';
|
||||
import { apis, events } from '@affine/electron-api';
|
||||
import type { OAuthProviderType } from '@affine/graphql';
|
||||
import { I18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import {
|
||||
ApplicationFocused,
|
||||
@@ -78,33 +75,6 @@ export class AuthService extends Service {
|
||||
|
||||
private onApplicationStart() {
|
||||
this.session.revalidate();
|
||||
|
||||
if (BUILD_CONFIG.isElectron) {
|
||||
events?.ui.onAuthenticationRequest(({ method, payload }) => {
|
||||
(async () => {
|
||||
if (!(await apis?.ui.isActiveTab())) {
|
||||
return;
|
||||
}
|
||||
switch (method) {
|
||||
case 'magic-link': {
|
||||
const { email, token } = payload;
|
||||
await this.signInMagicLink(email, token);
|
||||
break;
|
||||
}
|
||||
case 'oauth': {
|
||||
const { code, state, provider } = payload;
|
||||
await this.signInOauth(code, state, provider);
|
||||
break;
|
||||
}
|
||||
}
|
||||
})().catch(e => {
|
||||
notify.error({
|
||||
title: I18n['com.affine.auth.toast.title.failed'](),
|
||||
message: (e as any).message,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private onApplicationFocused() {
|
||||
|
||||
@@ -4,7 +4,6 @@ import { CloudSvg } from '@affine/core/components/affine/share-page-modal/cloud-
|
||||
import { authAtom } from '@affine/core/components/atoms';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { apis } from '@affine/electron-api';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
@@ -12,6 +11,7 @@ import {
|
||||
FeatureFlagService,
|
||||
useLiveData,
|
||||
useService,
|
||||
useServiceOptional,
|
||||
WorkspacesService,
|
||||
} from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai';
|
||||
@@ -20,6 +20,7 @@ import { useCallback, useLayoutEffect, useState } from 'react';
|
||||
import { AuthService } from '../../../modules/cloud';
|
||||
import { _addLocalWorkspace } from '../../../modules/workspace-engine';
|
||||
import { buildShowcaseWorkspace } from '../../../utils/first-app-data';
|
||||
import { DesktopApiService } from '../../desktop-api';
|
||||
import { CreateWorkspaceDialogService } from '../services/dialog';
|
||||
import * as styles from './dialog.css';
|
||||
|
||||
@@ -162,6 +163,7 @@ const CreateWorkspaceDialog = () => {
|
||||
const t = useI18n();
|
||||
const workspacesService = useService(WorkspacesService);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const electronApi = useServiceOptional(DesktopApiService);
|
||||
|
||||
// TODO(@Peng): maybe refactor using xstate?
|
||||
useLayoutEffect(() => {
|
||||
@@ -173,11 +175,11 @@ const CreateWorkspaceDialog = () => {
|
||||
// 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 (!apis) {
|
||||
if (!electronApi) {
|
||||
return;
|
||||
}
|
||||
logger.info('load db file');
|
||||
const result = await apis.dialog.loadDBFile();
|
||||
const result = await electronApi.handler.dialog.loadDBFile();
|
||||
if (result.workspaceId && !canceled) {
|
||||
_addLocalWorkspace(result.workspaceId);
|
||||
workspacesService.list.revalidate();
|
||||
@@ -201,7 +203,7 @@ const CreateWorkspaceDialog = () => {
|
||||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
}, [createWorkspaceDialogService, mode, t, workspacesService]);
|
||||
}, [createWorkspaceDialogService, electronApi, mode, t, workspacesService]);
|
||||
|
||||
const onConfirmName = useAsyncCallback(
|
||||
async (name: string, workspaceFlavour: WorkspaceFlavour) => {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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');
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './desktop-api';
|
||||
export * from './workspace-events';
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { apis } from '@affine/electron-api';
|
||||
import { Entity, LiveData } from '@toeverything/infra';
|
||||
import {
|
||||
debounceTime,
|
||||
@@ -10,6 +9,8 @@ import {
|
||||
tap,
|
||||
} from 'rxjs';
|
||||
|
||||
import type { DesktopApiService } from '../../desktop-api';
|
||||
|
||||
const logger = new DebugLogger('affine:find-in-page');
|
||||
|
||||
export class FindInPage extends Entity {
|
||||
@@ -38,10 +39,10 @@ export class FindInPage extends Entity {
|
||||
let findNext = true;
|
||||
return this.direction$.pipe(
|
||||
switchMap(direction => {
|
||||
if (apis?.findInPage) {
|
||||
if (this.electronApi?.handler?.findInPage) {
|
||||
this.isSearching$.next(true);
|
||||
const currentId = ++searchId;
|
||||
return apis?.findInPage
|
||||
return this.electronApi.handler.findInPage
|
||||
.find(searchText, {
|
||||
forward: direction === 'forward',
|
||||
findNext,
|
||||
@@ -69,7 +70,7 @@ export class FindInPage extends Entity {
|
||||
null
|
||||
);
|
||||
|
||||
constructor() {
|
||||
constructor(private readonly electronApi: DesktopApiService) {
|
||||
super();
|
||||
// TODO(@Peng): hide on navigation
|
||||
}
|
||||
@@ -112,6 +113,6 @@ export class FindInPage extends Entity {
|
||||
|
||||
clear() {
|
||||
logger.debug('clear');
|
||||
apis?.findInPage.clear().catch(logger.error);
|
||||
this.electronApi.handler.findInPage.clear().catch(logger.error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { Framework } from '@toeverything/infra';
|
||||
|
||||
import { DesktopApiService } from '../desktop-api';
|
||||
import { FindInPage } from './entities/find-in-page';
|
||||
import { FindInPageService } from './services/find-in-page';
|
||||
|
||||
export function configureFindInPageModule(framework: Framework) {
|
||||
framework.service(FindInPageService).entity(FindInPage);
|
||||
framework.service(FindInPageService).entity(FindInPage, [DesktopApiService]);
|
||||
}
|
||||
|
||||
@@ -10,10 +10,9 @@ import { configureDocInfoModule } from './doc-info';
|
||||
import { configureDocLinksModule } from './doc-link';
|
||||
import { configureDocsSearchModule } from './docs-search';
|
||||
import { configureEditorModule } from './editor';
|
||||
import { configureEditorSettingModule } from './editor-settting';
|
||||
import { configureEditorSettingModule } from './editor-setting';
|
||||
import { configureExplorerModule } from './explorer';
|
||||
import { configureFavoriteModule } from './favorite';
|
||||
import { configureFindInPageModule } from './find-in-page';
|
||||
import { configureI18nModule } from './i18n';
|
||||
import { configureImportTemplateModule } from './import-template';
|
||||
import { configureJournalModule } from './journal';
|
||||
@@ -43,7 +42,6 @@ export function configureCommonModules(framework: Framework) {
|
||||
configureShareDocsModule(framework);
|
||||
configureShareSettingModule(framework);
|
||||
configureTelemetryModule(framework);
|
||||
configureFindInPageModule(framework);
|
||||
configurePeekViewModule(framework);
|
||||
configureDocDisplayMetaModule(framework);
|
||||
configureQuickSearchModule(framework);
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { EditorSettingService } from '../../editor-settting';
|
||||
import { EditorSettingService } from '../../editor-setting';
|
||||
import type { PeekViewAnimation, PeekViewMode } from '../entities/peek-view';
|
||||
import * as styles from './modal-container.css';
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Text } from '@blocksuite/affine/store';
|
||||
import type { DocProps, DocsService } from '@toeverything/infra';
|
||||
import { Service } from '@toeverything/infra';
|
||||
|
||||
import { EditorSettingService } from '../../editor-settting';
|
||||
import { EditorSettingService } from '../../editor-setting';
|
||||
import type { WorkbenchService } from '../../workbench';
|
||||
import { CollectionsQuickSearchSession } from '../impls/collections';
|
||||
import { CommandsQuickSearchSession } from '../impls/commands';
|
||||
|
||||
@@ -1,58 +1,66 @@
|
||||
import { sharedStorage } from '@affine/electron-api';
|
||||
import type { GlobalCache, GlobalState } from '@toeverything/infra';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const ensureSharedStorage = sharedStorage!;
|
||||
import type { DesktopApiService } from '../../desktop-api';
|
||||
|
||||
export class ElectronGlobalState implements GlobalState {
|
||||
constructor(private readonly electronApi: DesktopApiService) {}
|
||||
|
||||
keys(): string[] {
|
||||
return ensureSharedStorage.globalState.keys();
|
||||
return this.electronApi.sharedStorage.globalState.keys();
|
||||
}
|
||||
get<T>(key: string): T | undefined {
|
||||
return ensureSharedStorage.globalState.get(key);
|
||||
return this.electronApi.sharedStorage.globalState.get(key);
|
||||
}
|
||||
watch<T>(key: string) {
|
||||
return new Observable<T | undefined>(subscriber => {
|
||||
const unsubscribe = ensureSharedStorage.globalState.watch<T>(key, i => {
|
||||
subscriber.next(i);
|
||||
});
|
||||
const unsubscribe = this.electronApi.sharedStorage.globalState.watch<T>(
|
||||
key,
|
||||
i => {
|
||||
subscriber.next(i);
|
||||
}
|
||||
);
|
||||
return () => unsubscribe();
|
||||
});
|
||||
}
|
||||
set<T>(key: string, value: T): void {
|
||||
ensureSharedStorage.globalState.set(key, value);
|
||||
this.electronApi.sharedStorage.globalState.set(key, value);
|
||||
}
|
||||
del(key: string): void {
|
||||
ensureSharedStorage.globalState.del(key);
|
||||
this.electronApi.sharedStorage.globalState.del(key);
|
||||
}
|
||||
clear(): void {
|
||||
ensureSharedStorage.globalState.clear();
|
||||
this.electronApi.sharedStorage.globalState.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export class ElectronGlobalCache implements GlobalCache {
|
||||
constructor(private readonly electronApi: DesktopApiService) {}
|
||||
|
||||
keys(): string[] {
|
||||
return ensureSharedStorage.globalCache.keys();
|
||||
return this.electronApi.sharedStorage.globalCache.keys();
|
||||
}
|
||||
get<T>(key: string): T | undefined {
|
||||
return ensureSharedStorage.globalCache.get(key);
|
||||
return this.electronApi.sharedStorage.globalCache.get(key);
|
||||
}
|
||||
watch<T>(key: string) {
|
||||
return new Observable<T | undefined>(subscriber => {
|
||||
const unsubscribe = ensureSharedStorage.globalCache.watch<T>(key, i => {
|
||||
subscriber.next(i);
|
||||
});
|
||||
const unsubscribe = this.electronApi.sharedStorage.globalCache.watch<T>(
|
||||
key,
|
||||
i => {
|
||||
subscriber.next(i);
|
||||
}
|
||||
);
|
||||
return () => unsubscribe();
|
||||
});
|
||||
}
|
||||
set<T>(key: string, value: T): void {
|
||||
ensureSharedStorage.globalCache.set(key, value);
|
||||
this.electronApi.sharedStorage.globalCache.set(key, value);
|
||||
}
|
||||
del(key: string): void {
|
||||
ensureSharedStorage.globalCache.del(key);
|
||||
this.electronApi.sharedStorage.globalCache.del(key);
|
||||
}
|
||||
clear(): void {
|
||||
ensureSharedStorage.globalCache.clear();
|
||||
this.electronApi.sharedStorage.globalCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { type Framework, GlobalCache, GlobalState } from '@toeverything/infra';
|
||||
|
||||
import { DesktopApiService } from '../desktop-api';
|
||||
import { ElectronGlobalCache, ElectronGlobalState } from './impls/electron';
|
||||
import {
|
||||
LocalStorageGlobalCache,
|
||||
@@ -12,6 +13,6 @@ export function configureLocalStorageStateStorageImpls(framework: Framework) {
|
||||
}
|
||||
|
||||
export function configureElectronStateStorageImpls(framework: Framework) {
|
||||
framework.impl(GlobalCache, ElectronGlobalCache);
|
||||
framework.impl(GlobalState, ElectronGlobalState);
|
||||
framework.impl(GlobalCache, ElectronGlobalCache, [DesktopApiService]);
|
||||
framework.impl(GlobalState, ElectronGlobalState, [DesktopApiService]);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { apis } from '@affine/electron-api';
|
||||
import type {
|
||||
ByteKV,
|
||||
ByteKVBehavior,
|
||||
@@ -8,6 +7,8 @@ import type {
|
||||
} from '@toeverything/infra';
|
||||
import { AsyncLock } from '@toeverything/infra';
|
||||
|
||||
import type { DesktopApiService } from '../../desktop-api';
|
||||
|
||||
class BroadcastChannelDocEventBus implements DocEventBus {
|
||||
senderChannel = new BroadcastChannel('user-db:' + this.userId);
|
||||
constructor(private readonly userId: string) {}
|
||||
@@ -29,22 +30,26 @@ class BroadcastChannelDocEventBus implements DocEventBus {
|
||||
}
|
||||
|
||||
export class SqliteUserspaceDocStorage implements DocStorage {
|
||||
constructor(private readonly userId: string) {}
|
||||
constructor(
|
||||
private readonly userId: string,
|
||||
private readonly electronApi: DesktopApiService
|
||||
) {}
|
||||
eventBus = new BroadcastChannelDocEventBus(this.userId);
|
||||
readonly doc = new Doc(this.userId);
|
||||
readonly syncMetadata = new SyncMetadataKV(this.userId);
|
||||
readonly serverClock = new ServerClockKV(this.userId);
|
||||
readonly doc = new Doc(this.userId, this.electronApi);
|
||||
readonly syncMetadata = new SyncMetadataKV(this.userId, this.electronApi);
|
||||
readonly serverClock = new ServerClockKV(this.userId, this.electronApi);
|
||||
}
|
||||
|
||||
type DocType = DocStorage['doc'];
|
||||
|
||||
class Doc implements DocType {
|
||||
lock = new AsyncLock();
|
||||
constructor(private readonly userId: string) {
|
||||
if (!apis?.db) {
|
||||
throw new Error('sqlite datasource is not available');
|
||||
}
|
||||
}
|
||||
apis = this.electronApi.api.handler;
|
||||
|
||||
constructor(
|
||||
private readonly userId: string,
|
||||
private readonly electronApi: DesktopApiService
|
||||
) {}
|
||||
|
||||
async transaction<T>(
|
||||
cb: (transaction: ByteKVBehavior) => Promise<T>
|
||||
@@ -58,10 +63,7 @@ class Doc implements DocType {
|
||||
}
|
||||
|
||||
async get(docId: string) {
|
||||
if (!apis?.db) {
|
||||
throw new Error('sqlite datasource is not available');
|
||||
}
|
||||
const update = await apis.db.getDocAsUpdates(
|
||||
const update = await this.apis.db.getDocAsUpdates(
|
||||
'userspace',
|
||||
this.userId,
|
||||
docId
|
||||
@@ -82,10 +84,7 @@ class Doc implements DocType {
|
||||
}
|
||||
|
||||
async set(docId: string, data: Uint8Array) {
|
||||
if (!apis?.db) {
|
||||
throw new Error('sqlite datasource is not available');
|
||||
}
|
||||
await apis.db.applyDocUpdate('userspace', this.userId, data, docId);
|
||||
await this.apis.db.applyDocUpdate('userspace', this.userId, data, docId);
|
||||
}
|
||||
|
||||
clear(): void | Promise<void> {
|
||||
@@ -93,93 +92,68 @@ class Doc implements DocType {
|
||||
}
|
||||
|
||||
async del(docId: string) {
|
||||
if (!apis?.db) {
|
||||
throw new Error('sqlite datasource is not available');
|
||||
}
|
||||
await apis.db.deleteDoc('userspace', this.userId, docId);
|
||||
await this.apis.db.deleteDoc('userspace', this.userId, docId);
|
||||
}
|
||||
}
|
||||
|
||||
class SyncMetadataKV implements ByteKV {
|
||||
constructor(private readonly userId: string) {}
|
||||
apis = this.electronApi.api.handler;
|
||||
constructor(
|
||||
private readonly userId: string,
|
||||
private readonly electronApi: DesktopApiService
|
||||
) {}
|
||||
transaction<T>(cb: (behavior: ByteKVBehavior) => Promise<T>): Promise<T> {
|
||||
return cb(this);
|
||||
}
|
||||
|
||||
get(key: string): Uint8Array | null | Promise<Uint8Array | null> {
|
||||
if (!apis?.db) {
|
||||
throw new Error('sqlite datasource is not available');
|
||||
}
|
||||
return apis.db.getSyncMetadata('userspace', this.userId, key);
|
||||
return this.apis.db.getSyncMetadata('userspace', this.userId, key);
|
||||
}
|
||||
|
||||
set(key: string, data: Uint8Array): void | Promise<void> {
|
||||
if (!apis?.db) {
|
||||
throw new Error('sqlite datasource is not available');
|
||||
}
|
||||
return apis.db.setSyncMetadata('userspace', this.userId, key, data);
|
||||
return this.apis.db.setSyncMetadata('userspace', this.userId, key, data);
|
||||
}
|
||||
|
||||
keys(): string[] | Promise<string[]> {
|
||||
if (!apis?.db) {
|
||||
throw new Error('sqlite datasource is not available');
|
||||
}
|
||||
return apis.db.getSyncMetadataKeys('userspace', this.userId);
|
||||
return this.apis.db.getSyncMetadataKeys('userspace', this.userId);
|
||||
}
|
||||
|
||||
del(key: string): void | Promise<void> {
|
||||
if (!apis?.db) {
|
||||
throw new Error('sqlite datasource is not available');
|
||||
}
|
||||
return apis.db.delSyncMetadata('userspace', this.userId, key);
|
||||
return this.apis.db.delSyncMetadata('userspace', this.userId, key);
|
||||
}
|
||||
|
||||
clear(): void | Promise<void> {
|
||||
if (!apis?.db) {
|
||||
throw new Error('sqlite datasource is not available');
|
||||
}
|
||||
return apis.db.clearSyncMetadata('userspace', this.userId);
|
||||
return this.apis.db.clearSyncMetadata('userspace', this.userId);
|
||||
}
|
||||
}
|
||||
|
||||
class ServerClockKV implements ByteKV {
|
||||
constructor(private readonly userId: string) {}
|
||||
apis = this.electronApi.api.handler;
|
||||
constructor(
|
||||
private readonly userId: string,
|
||||
private readonly electronApi: DesktopApiService
|
||||
) {}
|
||||
transaction<T>(cb: (behavior: ByteKVBehavior) => Promise<T>): Promise<T> {
|
||||
return cb(this);
|
||||
}
|
||||
|
||||
get(key: string): Uint8Array | null | Promise<Uint8Array | null> {
|
||||
if (!apis?.db) {
|
||||
throw new Error('sqlite datasource is not available');
|
||||
}
|
||||
return apis.db.getServerClock('userspace', this.userId, key);
|
||||
return this.apis.db.getServerClock('userspace', this.userId, key);
|
||||
}
|
||||
|
||||
set(key: string, data: Uint8Array): void | Promise<void> {
|
||||
if (!apis?.db) {
|
||||
throw new Error('sqlite datasource is not available');
|
||||
}
|
||||
return apis.db.setServerClock('userspace', this.userId, key, data);
|
||||
return this.apis.db.setServerClock('userspace', this.userId, key, data);
|
||||
}
|
||||
|
||||
keys(): string[] | Promise<string[]> {
|
||||
if (!apis?.db) {
|
||||
throw new Error('sqlite datasource is not available');
|
||||
}
|
||||
return apis.db.getServerClockKeys('userspace', this.userId);
|
||||
return this.apis.db.getServerClockKeys('userspace', this.userId);
|
||||
}
|
||||
|
||||
del(key: string): void | Promise<void> {
|
||||
if (!apis?.db) {
|
||||
throw new Error('sqlite datasource is not available');
|
||||
}
|
||||
return apis.db.delServerClock('userspace', this.userId, key);
|
||||
return this.apis.db.delServerClock('userspace', this.userId, key);
|
||||
}
|
||||
|
||||
clear(): void | Promise<void> {
|
||||
if (!apis?.db) {
|
||||
throw new Error('sqlite datasource is not available');
|
||||
}
|
||||
return apis.db.clearServerClock('userspace', this.userId);
|
||||
return this.apis.db.clearServerClock('userspace', this.userId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ export { UserspaceService as UserDBService } from './services/userspace';
|
||||
import type { Framework } from '@toeverything/infra';
|
||||
|
||||
import { AuthService, WebSocketService } from '../cloud';
|
||||
import { DesktopApiService } from '../desktop-api';
|
||||
import { CurrentUserDB } from './entities/current-user-db';
|
||||
import { UserDB } from './entities/user-db';
|
||||
import { UserDBEngine } from './entities/user-db-engine';
|
||||
@@ -32,9 +33,9 @@ export function configureIndexedDBUserspaceStorageProvider(
|
||||
}
|
||||
|
||||
export function configureSqliteUserspaceStorageProvider(framework: Framework) {
|
||||
framework.impl(UserspaceStorageProvider, {
|
||||
framework.impl(UserspaceStorageProvider, p => ({
|
||||
getDocStorage(userId: string) {
|
||||
return new SqliteUserspaceDocStorage(userId);
|
||||
return new SqliteUserspaceDocStorage(userId, p.get(DesktopApiService));
|
||||
},
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ export class Workbench extends Entity {
|
||||
show?: boolean;
|
||||
} = {}
|
||||
) {
|
||||
this.newTabHandler({
|
||||
this.newTabHandler.handle({
|
||||
basename: this.basename$.value,
|
||||
to,
|
||||
show: show ?? true,
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
WorkspaceScope,
|
||||
} from '@toeverything/infra';
|
||||
|
||||
import { DesktopApiService } from '../desktop-api';
|
||||
import { SidebarTab } from './entities/sidebar-tab';
|
||||
import { View } from './entities/view';
|
||||
import { Workbench } from './entities/workbench';
|
||||
@@ -59,7 +60,10 @@ export function configureDesktopWorkbenchModule(services: Framework) {
|
||||
.scope(WorkspaceScope)
|
||||
.impl(WorkbenchDefaultState, DesktopWorkbenchDefaultState, [
|
||||
GlobalStateService,
|
||||
DesktopApiService,
|
||||
])
|
||||
.impl(WorkbenchNewTabHandler, () => DesktopWorkbenchNewTabHandler)
|
||||
.service(DesktopStateSynchronizer, [WorkbenchService]);
|
||||
.impl(WorkbenchNewTabHandler, DesktopWorkbenchNewTabHandler, [
|
||||
DesktopApiService,
|
||||
])
|
||||
.service(DesktopStateSynchronizer, [WorkbenchService, DesktopApiService]);
|
||||
}
|
||||
|
||||
+14
-10
@@ -1,13 +1,16 @@
|
||||
import { apis, appInfo, events } from '@affine/electron-api';
|
||||
import { LiveData, Service } from '@toeverything/infra';
|
||||
|
||||
import type { DesktopApiService } from '../../desktop-api';
|
||||
import type { WorkbenchService } from '../../workbench';
|
||||
|
||||
/**
|
||||
* Synchronize workbench state with state stored in main process
|
||||
*/
|
||||
export class DesktopStateSynchronizer extends Service {
|
||||
constructor(private readonly workbenchService: WorkbenchService) {
|
||||
constructor(
|
||||
private readonly workbenchService: WorkbenchService,
|
||||
private readonly electronApi: DesktopApiService
|
||||
) {
|
||||
super();
|
||||
this.startSync();
|
||||
}
|
||||
@@ -18,8 +21,9 @@ export class DesktopStateSynchronizer extends Service {
|
||||
}
|
||||
|
||||
const workbench = this.workbenchService.workbench;
|
||||
const appInfo = this.electronApi.appInfo;
|
||||
|
||||
events?.ui.onTabAction(event => {
|
||||
this.electronApi.events.ui.onTabAction(event => {
|
||||
if (
|
||||
event.type === 'open-in-split-view' &&
|
||||
event.payload.tabId === appInfo?.viewId
|
||||
@@ -51,7 +55,7 @@ export class DesktopStateSynchronizer extends Service {
|
||||
}
|
||||
});
|
||||
|
||||
events?.ui.onToggleRightSidebar(tabId => {
|
||||
this.electronApi.events.ui.onToggleRightSidebar(tabId => {
|
||||
if (tabId === appInfo?.viewId) {
|
||||
workbench.sidebarOpen$.next(!workbench.sidebarOpen$.value);
|
||||
}
|
||||
@@ -74,11 +78,11 @@ export class DesktopStateSynchronizer extends Service {
|
||||
};
|
||||
});
|
||||
}).subscribe(views => {
|
||||
if (!apis || !appInfo?.viewId) {
|
||||
if (!appInfo?.viewId) {
|
||||
return;
|
||||
}
|
||||
|
||||
apis.ui
|
||||
this.electronApi.handler.ui
|
||||
.updateWorkbenchMeta(appInfo.viewId, {
|
||||
views,
|
||||
})
|
||||
@@ -86,11 +90,11 @@ export class DesktopStateSynchronizer extends Service {
|
||||
});
|
||||
|
||||
workbench.activeViewIndex$.subscribe(activeViewIndex => {
|
||||
if (!apis || !appInfo?.viewId) {
|
||||
if (!appInfo?.viewId) {
|
||||
return;
|
||||
}
|
||||
|
||||
apis.ui
|
||||
this.electronApi.handler.ui
|
||||
.updateWorkbenchMeta(appInfo.viewId, {
|
||||
activeViewIndex: activeViewIndex,
|
||||
})
|
||||
@@ -98,11 +102,11 @@ export class DesktopStateSynchronizer extends Service {
|
||||
});
|
||||
|
||||
workbench.basename$.subscribe(basename => {
|
||||
if (!apis || !appInfo?.viewId) {
|
||||
if (!appInfo?.viewId) {
|
||||
return;
|
||||
}
|
||||
|
||||
apis.ui
|
||||
this.electronApi.handler.ui
|
||||
.updateWorkbenchMeta(appInfo.viewId, {
|
||||
basename: basename,
|
||||
})
|
||||
|
||||
+31
-28
@@ -1,37 +1,40 @@
|
||||
import { apis } from '@affine/electron-api';
|
||||
import { createIdentifier } from '@toeverything/infra';
|
||||
import { createIdentifier, Service } from '@toeverything/infra';
|
||||
import { parsePath, type To } from 'history';
|
||||
|
||||
export type WorkbenchNewTabHandler = (option: {
|
||||
basename: string;
|
||||
to: To;
|
||||
show: boolean;
|
||||
}) => void;
|
||||
import type { DesktopApiService } from '../../desktop-api';
|
||||
|
||||
export type WorkbenchNewTabHandler = {
|
||||
handle: (option: { basename: string; to: To; show: boolean }) => void;
|
||||
};
|
||||
|
||||
export const WorkbenchNewTabHandler = createIdentifier<WorkbenchNewTabHandler>(
|
||||
'WorkbenchNewTabHandler'
|
||||
);
|
||||
|
||||
export const BrowserWorkbenchNewTabHandler: WorkbenchNewTabHandler = ({
|
||||
basename,
|
||||
to,
|
||||
}) => {
|
||||
const link =
|
||||
basename +
|
||||
(typeof to === 'string' ? to : `${to.pathname}${to.search}${to.hash}`);
|
||||
window.open(link, '_blank');
|
||||
export const BrowserWorkbenchNewTabHandler: WorkbenchNewTabHandler = {
|
||||
handle: ({ basename, to }) => {
|
||||
const link =
|
||||
basename +
|
||||
(typeof to === 'string' ? to : `${to.pathname}${to.search}${to.hash}`);
|
||||
window.open(link, '_blank');
|
||||
},
|
||||
};
|
||||
|
||||
export const DesktopWorkbenchNewTabHandler: WorkbenchNewTabHandler = ({
|
||||
basename,
|
||||
to,
|
||||
}) => {
|
||||
const path = typeof to === 'string' ? parsePath(to) : to;
|
||||
apis?.ui
|
||||
.addTab({
|
||||
basename,
|
||||
view: { path },
|
||||
show: false,
|
||||
})
|
||||
.catch(console.error);
|
||||
};
|
||||
export class DesktopWorkbenchNewTabHandler
|
||||
extends Service
|
||||
implements WorkbenchNewTabHandler
|
||||
{
|
||||
constructor(private readonly electronApi: DesktopApiService) {
|
||||
super();
|
||||
}
|
||||
handle({ basename, to }: { basename: string; to: To }) {
|
||||
const path = typeof to === 'string' ? parsePath(to) : to;
|
||||
this.electronApi.api.handler.ui
|
||||
.addTab({
|
||||
basename,
|
||||
view: { path },
|
||||
show: false,
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { appInfo, type TabViewsMetaSchema } from '@affine/electron-api';
|
||||
import type { GlobalStateService } from '@toeverything/infra';
|
||||
import { createIdentifier, Service } from '@toeverything/infra';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import type { DesktopApiService, TabViewsMetaSchema } from '../../desktop-api';
|
||||
import type { ViewIconName } from '../constants';
|
||||
|
||||
export type WorkbenchDefaultState = {
|
||||
@@ -34,7 +34,10 @@ export class DesktopWorkbenchDefaultState
|
||||
extends Service
|
||||
implements WorkbenchDefaultState
|
||||
{
|
||||
constructor(private readonly globalStateService: GlobalStateService) {
|
||||
constructor(
|
||||
private readonly globalStateService: GlobalStateService,
|
||||
private readonly electronApi: DesktopApiService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@@ -45,8 +48,9 @@ export class DesktopWorkbenchDefaultState
|
||||
);
|
||||
|
||||
return (
|
||||
tabViewsMeta?.workbenches.find(w => w.id === appInfo?.viewId) ||
|
||||
InMemoryWorkbenchDefaultState
|
||||
tabViewsMeta?.workbenches.find(
|
||||
w => w.id === this.electronApi.appInfo.viewId
|
||||
) || InMemoryWorkbenchDefaultState
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
import { apis } from '@affine/electron-api';
|
||||
import { assertExists } from '@blocksuite/affine/global/utils';
|
||||
import type { DesktopApiService } from '@affine/core/modules/desktop-api';
|
||||
import type { BlobStorage } from '@toeverything/infra';
|
||||
|
||||
import { bufferToBlob } from '../../utils/buffer-to-blob';
|
||||
|
||||
export class SqliteBlobStorage implements BlobStorage {
|
||||
constructor(private readonly workspaceId: string) {}
|
||||
constructor(
|
||||
private readonly workspaceId: string,
|
||||
private readonly electronApi: DesktopApiService
|
||||
) {}
|
||||
name = 'sqlite';
|
||||
readonly = false;
|
||||
async get(key: string) {
|
||||
assertExists(apis);
|
||||
const buffer = await apis.db.getBlob('workspace', this.workspaceId, key);
|
||||
const buffer = await this.electronApi.handler.db.getBlob(
|
||||
'workspace',
|
||||
this.workspaceId,
|
||||
key
|
||||
);
|
||||
if (buffer) {
|
||||
return bufferToBlob(buffer);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
async set(key: string, value: Blob) {
|
||||
assertExists(apis);
|
||||
await apis.db.addBlob(
|
||||
await this.electronApi.handler.db.addBlob(
|
||||
'workspace',
|
||||
this.workspaceId,
|
||||
key,
|
||||
@@ -27,11 +31,16 @@ export class SqliteBlobStorage implements BlobStorage {
|
||||
return key;
|
||||
}
|
||||
delete(key: string) {
|
||||
assertExists(apis);
|
||||
return apis.db.deleteBlob('workspace', this.workspaceId, key);
|
||||
return this.electronApi.handler.db.deleteBlob(
|
||||
'workspace',
|
||||
this.workspaceId,
|
||||
key
|
||||
);
|
||||
}
|
||||
list() {
|
||||
assertExists(apis);
|
||||
return apis.db.getBlobKeys('workspace', this.workspaceId);
|
||||
return this.electronApi.handler.db.getBlobKeys(
|
||||
'workspace',
|
||||
this.workspaceId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,32 @@
|
||||
import { apis } from '@affine/electron-api';
|
||||
import type { DesktopApiService } from '@affine/core/modules/desktop-api';
|
||||
import type { ByteKV, ByteKVBehavior, DocStorage } from '@toeverything/infra';
|
||||
import { AsyncLock } from '@toeverything/infra';
|
||||
|
||||
import { BroadcastChannelDocEventBus } from './doc-broadcast-channel';
|
||||
|
||||
export class SqliteDocStorage implements DocStorage {
|
||||
constructor(private readonly workspaceId: string) {}
|
||||
constructor(
|
||||
private readonly workspaceId: string,
|
||||
private readonly electronApi: DesktopApiService
|
||||
) {}
|
||||
eventBus = new BroadcastChannelDocEventBus(this.workspaceId);
|
||||
readonly doc = new Doc(this.workspaceId);
|
||||
readonly syncMetadata = new SyncMetadataKV(this.workspaceId);
|
||||
readonly serverClock = new ServerClockKV(this.workspaceId);
|
||||
readonly doc = new Doc(this.workspaceId, this.electronApi);
|
||||
readonly syncMetadata = new SyncMetadataKV(
|
||||
this.workspaceId,
|
||||
this.electronApi
|
||||
);
|
||||
readonly serverClock = new ServerClockKV(this.workspaceId, this.electronApi);
|
||||
}
|
||||
|
||||
type DocType = DocStorage['doc'];
|
||||
|
||||
class Doc implements DocType {
|
||||
lock = new AsyncLock();
|
||||
constructor(private readonly workspaceId: string) {
|
||||
if (!apis?.db) {
|
||||
throw new Error('sqlite datasource is not available');
|
||||
}
|
||||
}
|
||||
apis = this.electronApi.handler;
|
||||
constructor(
|
||||
private readonly workspaceId: string,
|
||||
private readonly electronApi: DesktopApiService
|
||||
) {}
|
||||
|
||||
async transaction<T>(
|
||||
cb: (transaction: ByteKVBehavior) => Promise<T>
|
||||
@@ -34,10 +40,7 @@ class Doc implements DocType {
|
||||
}
|
||||
|
||||
async get(docId: string) {
|
||||
if (!apis?.db) {
|
||||
throw new Error('sqlite datasource is not available');
|
||||
}
|
||||
const update = await apis.db.getDocAsUpdates(
|
||||
const update = await this.apis.db.getDocAsUpdates(
|
||||
'workspace',
|
||||
this.workspaceId,
|
||||
docId
|
||||
@@ -58,10 +61,12 @@ class Doc implements DocType {
|
||||
}
|
||||
|
||||
async set(docId: string, data: Uint8Array) {
|
||||
if (!apis?.db) {
|
||||
throw new Error('sqlite datasource is not available');
|
||||
}
|
||||
await apis.db.applyDocUpdate('workspace', this.workspaceId, data, docId);
|
||||
await this.apis.db.applyDocUpdate(
|
||||
'workspace',
|
||||
this.workspaceId,
|
||||
data,
|
||||
docId
|
||||
);
|
||||
}
|
||||
|
||||
clear(): void | Promise<void> {
|
||||
@@ -69,93 +74,78 @@ class Doc implements DocType {
|
||||
}
|
||||
|
||||
async del(docId: string) {
|
||||
if (!apis?.db) {
|
||||
throw new Error('sqlite datasource is not available');
|
||||
}
|
||||
await apis.db.deleteDoc('workspace', this.workspaceId, docId);
|
||||
await this.apis.db.deleteDoc('workspace', this.workspaceId, docId);
|
||||
}
|
||||
}
|
||||
|
||||
class SyncMetadataKV implements ByteKV {
|
||||
constructor(private readonly workspaceId: string) {}
|
||||
apis = this.electronApi.handler;
|
||||
constructor(
|
||||
private readonly workspaceId: string,
|
||||
private readonly electronApi: DesktopApiService
|
||||
) {}
|
||||
transaction<T>(cb: (behavior: ByteKVBehavior) => Promise<T>): Promise<T> {
|
||||
return cb(this);
|
||||
}
|
||||
|
||||
get(key: string): Uint8Array | null | Promise<Uint8Array | null> {
|
||||
if (!apis?.db) {
|
||||
throw new Error('sqlite datasource is not available');
|
||||
}
|
||||
return apis.db.getSyncMetadata('workspace', this.workspaceId, key);
|
||||
return this.apis.db.getSyncMetadata('workspace', this.workspaceId, key);
|
||||
}
|
||||
|
||||
set(key: string, data: Uint8Array): void | Promise<void> {
|
||||
if (!apis?.db) {
|
||||
throw new Error('sqlite datasource is not available');
|
||||
}
|
||||
return apis.db.setSyncMetadata('workspace', this.workspaceId, key, data);
|
||||
return this.apis.db.setSyncMetadata(
|
||||
'workspace',
|
||||
this.workspaceId,
|
||||
key,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
keys(): string[] | Promise<string[]> {
|
||||
if (!apis?.db) {
|
||||
throw new Error('sqlite datasource is not available');
|
||||
}
|
||||
return apis.db.getSyncMetadataKeys('workspace', this.workspaceId);
|
||||
return this.apis.db.getSyncMetadataKeys('workspace', this.workspaceId);
|
||||
}
|
||||
|
||||
del(key: string): void | Promise<void> {
|
||||
if (!apis?.db) {
|
||||
throw new Error('sqlite datasource is not available');
|
||||
}
|
||||
return apis.db.delSyncMetadata('workspace', this.workspaceId, key);
|
||||
return this.apis.db.delSyncMetadata('workspace', this.workspaceId, key);
|
||||
}
|
||||
|
||||
clear(): void | Promise<void> {
|
||||
if (!apis?.db) {
|
||||
throw new Error('sqlite datasource is not available');
|
||||
}
|
||||
return apis.db.clearSyncMetadata('workspace', this.workspaceId);
|
||||
return this.apis.db.clearSyncMetadata('workspace', this.workspaceId);
|
||||
}
|
||||
}
|
||||
|
||||
class ServerClockKV implements ByteKV {
|
||||
constructor(private readonly workspaceId: string) {}
|
||||
apis = this.electronApi.handler;
|
||||
constructor(
|
||||
private readonly workspaceId: string,
|
||||
private readonly electronApi: DesktopApiService
|
||||
) {}
|
||||
transaction<T>(cb: (behavior: ByteKVBehavior) => Promise<T>): Promise<T> {
|
||||
return cb(this);
|
||||
}
|
||||
|
||||
get(key: string): Uint8Array | null | Promise<Uint8Array | null> {
|
||||
if (!apis?.db) {
|
||||
throw new Error('sqlite datasource is not available');
|
||||
}
|
||||
return apis.db.getServerClock('workspace', this.workspaceId, key);
|
||||
return this.apis.db.getServerClock('workspace', this.workspaceId, key);
|
||||
}
|
||||
|
||||
set(key: string, data: Uint8Array): void | Promise<void> {
|
||||
if (!apis?.db) {
|
||||
throw new Error('sqlite datasource is not available');
|
||||
}
|
||||
return apis.db.setServerClock('workspace', this.workspaceId, key, data);
|
||||
return this.apis.db.setServerClock(
|
||||
'workspace',
|
||||
this.workspaceId,
|
||||
key,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
keys(): string[] | Promise<string[]> {
|
||||
if (!apis?.db) {
|
||||
throw new Error('sqlite datasource is not available');
|
||||
}
|
||||
return apis.db.getServerClockKeys('workspace', this.workspaceId);
|
||||
return this.apis.db.getServerClockKeys('workspace', this.workspaceId);
|
||||
}
|
||||
|
||||
del(key: string): void | Promise<void> {
|
||||
if (!apis?.db) {
|
||||
throw new Error('sqlite datasource is not available');
|
||||
}
|
||||
return apis.db.delServerClock('workspace', this.workspaceId, key);
|
||||
return this.apis.db.delServerClock('workspace', this.workspaceId, key);
|
||||
}
|
||||
|
||||
clear(): void | Promise<void> {
|
||||
if (!apis?.db) {
|
||||
throw new Error('sqlite datasource is not available');
|
||||
}
|
||||
return apis.db.clearServerClock('workspace', this.workspaceId);
|
||||
return this.apis.db.clearServerClock('workspace', this.workspaceId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { apis } from '@affine/electron-api';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { DocCollection } from '@blocksuite/affine/store';
|
||||
import type {
|
||||
@@ -20,6 +19,7 @@ import { nanoid } from 'nanoid';
|
||||
import { Observable } from 'rxjs';
|
||||
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
|
||||
|
||||
import { DesktopApiService } from '../../desktop-api';
|
||||
import type { WorkspaceEngineStorageProvider } from '../providers/engine';
|
||||
import { BroadcastChannelAwarenessConnection } from './engine/awareness-broadcast-channel';
|
||||
import { StaticBlobStorage } from './engine/blob-static';
|
||||
@@ -72,8 +72,10 @@ export class LocalWorkspaceFlavourProvider
|
||||
async deleteWorkspace(id: string): Promise<void> {
|
||||
setLocalWorkspaceIds(ids => ids.filter(x => x !== id));
|
||||
|
||||
if (BUILD_CONFIG.isElectron && apis) {
|
||||
await apis.workspace.delete(id);
|
||||
const electronApi = this.framework.getOptional(DesktopApiService);
|
||||
|
||||
if (BUILD_CONFIG.isElectron && electronApi) {
|
||||
await electronApi.handler.workspace.delete(id);
|
||||
}
|
||||
|
||||
// notify all browser tabs, so they can update their workspace list
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
WorkspaceFlavourProvider,
|
||||
} from '@toeverything/infra';
|
||||
|
||||
import { DesktopApiService } from '../desktop-api';
|
||||
import { CloudWorkspaceFlavourProviderService } from './impls/cloud';
|
||||
import { IndexedDBBlobStorage } from './impls/engine/blob-indexeddb';
|
||||
import { SqliteBlobStorage } from './impls/engine/blob-sqlite';
|
||||
@@ -57,13 +58,16 @@ export function configureIndexedDBWorkspaceEngineStorageProvider(
|
||||
export function configureSqliteWorkspaceEngineStorageProvider(
|
||||
framework: Framework
|
||||
) {
|
||||
framework.impl(WorkspaceEngineStorageProvider, {
|
||||
getDocStorage(workspaceId: string) {
|
||||
return new SqliteDocStorage(workspaceId);
|
||||
},
|
||||
getBlobStorage(workspaceId: string) {
|
||||
return new SqliteBlobStorage(workspaceId);
|
||||
},
|
||||
framework.impl(WorkspaceEngineStorageProvider, p => {
|
||||
const electronApi = p.get(DesktopApiService);
|
||||
return {
|
||||
getDocStorage(workspaceId: string) {
|
||||
return new SqliteDocStorage(workspaceId, electronApi);
|
||||
},
|
||||
getBlobStorage(workspaceId: string) {
|
||||
return new SqliteBlobStorage(workspaceId, electronApi);
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user