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

fix AF-1394
This commit is contained in:
pengx17 2024-10-30 09:16:20 +00:00
parent 50bae9c3e6
commit a791481ac8
No known key found for this signature in database
GPG Key ID: 23F23D9E8B3971ED
81 changed files with 788 additions and 567 deletions

View File

@ -73,6 +73,7 @@
"async-call-rpc": "^6.4.2",
"electron-updater": "^6.2.1",
"link-preview-js": "^3.0.5",
"next-themes": "^0.3.0",
"yjs": "patch:yjs@npm%3A13.6.18#~/.yarn/patches/yjs-npm-13.6.18-ad0d5f7c43.patch"
},
"build": {

View File

@ -7,6 +7,11 @@ import { router } from '@affine/core/desktop/router';
import { configureCommonModules } from '@affine/core/modules';
import { configureAppTabsHeaderModule } from '@affine/core/modules/app-tabs-header';
import { ValidatorProvider } from '@affine/core/modules/cloud';
import {
configureDesktopApiModule,
DesktopApiService,
} from '@affine/core/modules/desktop-api';
import { configureFindInPageModule } from '@affine/core/modules/find-in-page';
import { I18nProvider } from '@affine/core/modules/i18n';
import { configureElectronStateStorageImpls } from '@affine/core/modules/storage';
import { CustomThemeModifier } from '@affine/core/modules/theme-editor';
@ -21,7 +26,6 @@ import {
configureSqliteWorkspaceEngineStorageProvider,
} from '@affine/core/modules/workspace-engine';
import createEmotionCache from '@affine/core/utils/create-emotion-cache';
import { apis, appInfo } from '@affine/electron-api';
import { CacheProvider } from '@emotion/react';
import {
Framework,
@ -32,6 +36,8 @@ import {
import { Suspense } from 'react';
import { RouterProvider } from 'react-router-dom';
import { DesktopThemeSync } from './theme-sync';
const desktopWhiteList = [
'/open-app/signin-redirect',
'/open-app/url',
@ -64,26 +70,38 @@ configureSqliteWorkspaceEngineStorageProvider(framework);
configureSqliteUserspaceStorageProvider(framework);
configureDesktopWorkbenchModule(framework);
configureAppTabsHeaderModule(framework);
framework.impl(PopupWindowProvider, {
open: (url: string) => {
apis?.ui.openExternal(url).catch(e => {
console.error('Failed to open external URL', e);
});
},
configureFindInPageModule(framework);
configureDesktopApiModule(framework);
framework.impl(PopupWindowProvider, p => {
const apis = p.get(DesktopApiService).api;
return {
open: (url: string) => {
apis.handler.ui.openExternal(url).catch(e => {
console.error('Failed to open external URL', e);
});
},
};
});
framework.impl(ClientSchemeProvider, {
getClientScheme() {
return appInfo?.scheme;
},
framework.impl(ClientSchemeProvider, p => {
const appInfo = p.get(DesktopApiService).appInfo;
return {
getClientScheme() {
return appInfo?.scheme;
},
};
});
framework.impl(ValidatorProvider, {
async validate(_challenge, resource) {
const token = await apis?.ui?.getChallengeResponse(resource);
if (!token) {
throw new Error('Challenge failed');
}
return token;
},
framework.impl(ValidatorProvider, p => {
const apis = p.get(DesktopApiService).api;
return {
async validate(_challenge, resource) {
const token = await apis.handler.ui.getChallengeResponse(resource);
if (!token) {
throw new Error('Challenge failed');
}
return token;
},
};
});
const frameworkProvider = framework.provider();
@ -100,6 +118,7 @@ export function App() {
<CacheProvider value={cache}>
<I18nProvider>
<AffineContext store={getCurrentStore()}>
<DesktopThemeSync />
<Telemetry />
<CustomThemeModifier />
<GlobalLoading />

View File

@ -1,21 +1,8 @@
import './setup';
import { appConfigProxy } from '@affine/core/components/hooks/use-app-config-storage';
import { apis, appInfo, events } from '@affine/electron-api';
import {
init,
reactRouterV6BrowserTracingIntegration,
setTags,
} from '@sentry/react';
import { debounce } from 'lodash-es';
import { StrictMode, useEffect } from 'react';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import {
createRoutesFromChildren,
matchRoutes,
useLocation,
useNavigationType,
} from 'react-router-dom';
import { App } from './app';
@ -26,82 +13,6 @@ function main() {
.getSync()
.catch(() => console.error('failed to load app config'));
// skip bootstrap setup for desktop onboarding
if (
!(
appInfo?.windowName === 'onboarding' ||
appInfo?.windowName === 'theme-editor'
)
) {
if (BUILD_CONFIG.debug || window.SENTRY_RELEASE) {
// 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,
});
apis?.ui.handleNetworkChange(navigator.onLine);
window.addEventListener('offline', () => {
apis?.ui.handleNetworkChange(false);
});
window.addEventListener('online', () => {
apis?.ui.handleNetworkChange(true);
});
}
const handleMaximized = (maximized: boolean | undefined) => {
document.documentElement.dataset.maximized = String(maximized);
};
const handleFullscreen = (fullscreen: boolean | undefined) => {
document.documentElement.dataset.fullscreen = String(fullscreen);
};
apis?.ui.isMaximized().then(handleMaximized).catch(console.error);
apis?.ui.isFullScreen().then(handleFullscreen).catch(console.error);
events?.ui.onMaximized(handleMaximized);
events?.ui.onFullScreen(handleFullscreen);
const tabId = appInfo?.viewId;
const handleActiveTabChange = (active: boolean) => {
document.documentElement.dataset.active = String(active);
};
if (tabId) {
apis?.ui
.isActiveTab()
.then(active => {
handleActiveTabChange(active);
events?.ui.onActiveTabChanged(id => {
handleActiveTabChange(id === tabId);
});
})
.catch(console.error);
}
const handleResize = debounce(() => {
apis?.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';
});
}
mountApp();
}

View File

@ -6,6 +6,7 @@ import {
AppTabsHeader,
configureAppTabsHeaderModule,
} from '@affine/core/modules/app-tabs-header';
import { configureDesktopApiModule } from '@affine/core/modules/desktop-api';
import { configureI18nModule, I18nProvider } from '@affine/core/modules/i18n';
import { configureElectronStateStorageImpls } from '@affine/core/modules/storage';
import { SplitViewFallback } from '@affine/core/modules/workbench/view/split-view/split-view';
@ -23,6 +24,7 @@ configureElectronStateStorageImpls(framework);
configureAppTabsHeaderModule(framework);
configureAppSidebarModule(framework);
configureI18nModule(framework);
configureDesktopApiModule(framework);
const frameworkProvider = framework.provider();
export function App() {

View File

@ -1,28 +1,16 @@
import './setup';
import { apis, events } from '@affine/electron-api';
import { events } from '@affine/electron-api';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './app';
async function main() {
const handleMaximized = (maximized: boolean | undefined) => {
document.documentElement.dataset.maximized = String(maximized);
};
const handleFullscreen = (fullscreen: boolean | undefined) => {
document.documentElement.dataset.fullscreen = String(fullscreen);
};
const handleActive = (active: boolean | undefined) => {
document.documentElement.dataset.active = String(active);
};
apis?.ui.isMaximized().then(handleMaximized).catch(console.error);
apis?.ui.isFullScreen().then(handleFullscreen).catch(console.error);
events?.ui.onMaximized(handleMaximized);
events?.ui.onFullScreen(handleFullscreen);
events?.ui.onTabShellViewActiveChange(handleActive);
mountApp();
}

View File

@ -0,0 +1,25 @@
import { DesktopApiService } from '@affine/core/modules/desktop-api/service';
import { useService } from '@toeverything/infra';
import { useTheme } from 'next-themes';
import { useRef } from 'react';
export const DesktopThemeSync = () => {
const { theme } = useTheme();
const lastThemeRef = useRef(theme);
const onceRef = useRef(false);
const handler = useService(DesktopApiService).api.handler;
if (lastThemeRef.current !== theme || !onceRef.current) {
if (BUILD_CONFIG.isElectron && theme) {
handler.ui
.handleThemeChange(theme as 'dark' | 'light' | 'system')
.catch(err => {
console.error(err);
});
}
lastThemeRef.current = theme;
onceRef.current = true;
}
return null;
};

View File

@ -56,6 +56,8 @@ export const appInfo = {
scheme,
};
export type AppInfo = typeof appInfo;
function getMainAPIs() {
const meta: ExposedMeta = (() => {
const val = process.argv

View File

@ -94,3 +94,5 @@ export const sharedStorage = {
globalState,
globalCache,
};
export type SharedStorage = typeof sharedStorage;

View File

@ -1,33 +1,12 @@
import { apis } from '@affine/electron-api';
import { ThemeProvider as NextThemeProvider, useTheme } from 'next-themes';
import { ThemeProvider as NextThemeProvider } from 'next-themes';
import type { PropsWithChildren } from 'react';
import { memo, useRef } from 'react';
const themes = ['dark', 'light'];
const DesktopThemeSync = memo(function DesktopThemeSync() {
const { theme } = useTheme();
const lastThemeRef = useRef(theme);
const onceRef = useRef(false);
if (lastThemeRef.current !== theme || !onceRef.current) {
if (BUILD_CONFIG.isElectron && theme) {
apis?.ui
.handleThemeChange(theme as 'dark' | 'light' | 'system')
.catch(err => {
console.error(err);
});
}
lastThemeRef.current = theme;
onceRef.current = true;
}
return null;
});
export const ThemeProvider = ({ children }: PropsWithChildren) => {
return (
<NextThemeProvider themes={themes} enableSystem={true}>
{children}
<DesktopThemeSync />
</NextThemeProvider>
);
};

View File

@ -5,7 +5,7 @@ import { appSettingAtom } from '@toeverything/infra';
import type { createStore } from 'jotai';
import type { useTheme } from 'next-themes';
import type { EditorSettingService } from '../modules/editor-settting';
import type { EditorSettingService } from '../modules/editor-setting';
import { registerAffineCommand } from './registry';
export function registerAffineSettingsCommands({

View File

@ -1,6 +1,5 @@
import { notify } from '@affine/component';
import { updateReadyAtom } from '@affine/core/components/hooks/use-app-updater';
import { apis } from '@affine/electron-api';
import type { useI18n } from '@affine/i18n';
import { track } from '@affine/track';
import { ResetIcon } from '@blocksuite/icons/rc';
@ -11,9 +10,11 @@ import { registerAffineCommand } from './registry';
export function registerAffineUpdatesCommands({
t,
store,
quitAndInstall,
}: {
t: ReturnType<typeof useI18n>;
store: ReturnType<typeof createStore>;
quitAndInstall: () => Promise<void>;
}) {
const unsubs: Array<() => void> = [];
@ -27,7 +28,7 @@ export function registerAffineUpdatesCommands({
run() {
track.$.cmdk.updates.quitAndInstall();
apis?.updater.quitAndInstall().catch(err => {
quitAndInstall().catch(err => {
notify.error({
title: 'Failed to restart to upgrade',
message: 'Please restart the app manually to upgrade.',

View File

@ -1,10 +1,14 @@
import { Button } from '@affine/component';
import { SettingRow } from '@affine/component/setting-components';
import { DesktopApiService } from '@affine/core/modules/desktop-api/service';
import { ThemeEditorService } from '@affine/core/modules/theme-editor';
import { UrlService } from '@affine/core/modules/url';
import { apis } from '@affine/electron-api';
import { DeleteIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra';
import {
useLiveData,
useService,
useServiceOptional,
} from '@toeverything/infra';
import { cssVar } from '@toeverything/theme';
import { useCallback } from 'react';
@ -12,14 +16,15 @@ export const ThemeEditorSetting = () => {
const themeEditor = useService(ThemeEditorService);
const modified = useLiveData(themeEditor.modified$);
const urlService = useService(UrlService);
const desktopApi = useServiceOptional(DesktopApiService);
const open = useCallback(() => {
if (BUILD_CONFIG.isElectron) {
apis?.ui.openThemeEditor().catch(console.error);
if (desktopApi) {
desktopApi.handler.ui.openThemeEditor().catch(console.error);
} else if (BUILD_CONFIG.isMobileWeb || BUILD_CONFIG.isWeb) {
urlService.openPopupWindow(location.origin + '/theme-editor');
}
}, [urlService]);
}, [desktopApi, urlService]);
return (
<SettingRow

View File

@ -6,7 +6,7 @@ import {
Slider,
} from '@affine/component';
import { SettingRow } from '@affine/component/setting-components';
import { EditorSettingService } from '@affine/core/modules/editor-settting';
import { EditorSettingService } from '@affine/core/modules/editor-setting';
import { useI18n } from '@affine/i18n';
import {
ConnectorMode,

View File

@ -5,7 +5,7 @@ import {
type RadioItem,
} from '@affine/component';
import { SettingRow } from '@affine/component/setting-components';
import { EditorSettingService } from '@affine/core/modules/editor-settting';
import { EditorSettingService } from '@affine/core/modules/editor-setting';
import { useI18n } from '@affine/i18n';
import { LayoutType, MindmapStyle } from '@blocksuite/affine/blocks';
import type { Doc } from '@blocksuite/affine/store';

View File

@ -6,7 +6,7 @@ import {
Slider,
} from '@affine/component';
import { SettingRow } from '@affine/component/setting-components';
import { EditorSettingService } from '@affine/core/modules/editor-settting';
import { EditorSettingService } from '@affine/core/modules/editor-setting';
import { useI18n } from '@affine/i18n';
import {
createEnumMap,

View File

@ -1,6 +1,6 @@
import { MenuItem, MenuTrigger, Slider } from '@affine/component';
import { SettingRow } from '@affine/component/setting-components';
import { EditorSettingService } from '@affine/core/modules/editor-settting';
import { EditorSettingService } from '@affine/core/modules/editor-setting';
import { useI18n } from '@affine/i18n';
import { LineColor, LineColorMap } from '@blocksuite/affine/blocks';
import type { Doc } from '@blocksuite/affine/store';

View File

@ -6,7 +6,7 @@ import {
Slider,
} from '@affine/component';
import { SettingRow } from '@affine/component/setting-components';
import { EditorSettingService } from '@affine/core/modules/editor-settting';
import { EditorSettingService } from '@affine/core/modules/editor-setting';
import { useI18n } from '@affine/i18n';
import type { EditorHost } from '@blocksuite/affine/block-std';
import type {

View File

@ -1,6 +1,6 @@
import { Skeleton } from '@affine/component';
import type { EditorSettingSchema } from '@affine/core/modules/editor-settting';
import { EditorSettingService } from '@affine/core/modules/editor-settting';
import type { EditorSettingSchema } from '@affine/core/modules/editor-setting';
import { EditorSettingService } from '@affine/core/modules/editor-setting';
import type { EditorHost } from '@blocksuite/affine/block-std';
import { BlockStdScope } from '@blocksuite/affine/block-std';
import type { GfxPrimitiveElementModel } from '@blocksuite/affine/block-std/gfx';

View File

@ -5,7 +5,7 @@ import {
type RadioItem,
} from '@affine/component';
import { SettingRow } from '@affine/component/setting-components';
import { EditorSettingService } from '@affine/core/modules/editor-settting';
import { EditorSettingService } from '@affine/core/modules/editor-setting';
import { useI18n } from '@affine/i18n';
import {
FontFamily,

View File

@ -20,7 +20,7 @@ import {
EditorSettingService,
type FontFamily,
fontStyleOptions,
} from '@affine/core/modules/editor-settting';
} from '@affine/core/modules/editor-setting';
import {
type FontData,
SystemFontFamilyService,

View File

@ -3,7 +3,7 @@ import {
SettingRow,
SettingWrapper,
} from '@affine/component/setting-components';
import { EditorSettingService } from '@affine/core/modules/editor-settting';
import { EditorSettingService } from '@affine/core/modules/editor-setting';
import { useI18n } from '@affine/i18n';
import { useLiveData, useService } from '@toeverything/infra';
import { useCallback } from 'react';

View File

@ -3,9 +3,13 @@ import { SettingRow } from '@affine/component/setting-components';
import { Button } from '@affine/component/ui/button';
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import { useSystemOnline } from '@affine/core/components/hooks/use-system-online';
import { apis } from '@affine/electron-api';
import { DesktopApiService } from '@affine/core/modules/desktop-api/service';
import { useI18n } from '@affine/i18n';
import type { Workspace, WorkspaceMetadata } from '@toeverything/infra';
import {
useService,
type Workspace,
type WorkspaceMetadata,
} from '@toeverything/infra';
import { useState } from 'react';
interface ExportPanelProps {
@ -13,7 +17,7 @@ interface ExportPanelProps {
workspace: Workspace | null;
}
export const ExportPanel = ({
export const DesktopExportPanel = ({
workspaceMetadata,
workspace,
}: ExportPanelProps) => {
@ -21,6 +25,7 @@ export const ExportPanel = ({
const t = useI18n();
const [saving, setSaving] = useState(false);
const isOnline = useSystemOnline();
const desktopApi = useService(DesktopApiService);
const onExport = useAsyncCallback(async () => {
if (saving || !workspace) {
@ -33,7 +38,7 @@ export const ExportPanel = ({
await workspace.engine.blob.sync();
}
const result = await apis?.dialog.saveDBFileAs(workspaceId);
const result = await desktopApi.handler.dialog.saveDBFileAs(workspaceId);
if (result?.error) {
throw new Error(result.error);
} else if (!result?.canceled) {
@ -44,7 +49,7 @@ export const ExportPanel = ({
} finally {
setSaving(false);
}
}, [isOnline, saving, t, workspace, workspaceId]);
}, [isOnline, saving, t, workspace, workspaceId, desktopApi]);
return (
<SettingRow name={t['Export']()} desc={t['Export Description']()}>

View File

@ -13,7 +13,7 @@ import { useCallback } from 'react';
import { DeleteLeaveWorkspace } from './delete-leave-workspace';
import { EnableCloudPanel } from './enable-cloud';
import { ExportPanel } from './export';
import { DesktopExportPanel } from './export';
import { LabelsPanel } from './labels';
import { MembersPanel } from './members';
import { ProfilePanel } from './profile';
@ -71,7 +71,7 @@ export const WorkspaceSettingDetail = ({
<SharingPanel />
{BUILD_CONFIG.isElectron && (
<SettingWrapper title={t['Storage and Export']()}>
<ExportPanel
<DesktopExportPanel
workspace={workspace}
workspaceMetadata={workspaceMetadata}
/>

View File

@ -5,7 +5,7 @@ import {
} from '@affine/component';
import { ServerConfigService } from '@affine/core/modules/cloud';
import { EditorService } from '@affine/core/modules/editor';
import { EditorSettingService } from '@affine/core/modules/editor-settting';
import { EditorSettingService } from '@affine/core/modules/editor-setting';
import { JournalService } from '@affine/core/modules/journal';
import { toURLSearchParams } from '@affine/core/modules/navigation';
import { PeekViewService } from '@affine/core/modules/peek-view/services/peek-view';

View File

@ -2,7 +2,7 @@ import {
AIEdgelessRootBlockSpec,
AIPageRootBlockSpec,
} from '@affine/core/blocksuite/presets/ai';
import { EditorSettingService } from '@affine/core/modules/editor-settting';
import { EditorSettingService } from '@affine/core/modules/editor-setting';
import { mixpanel } from '@affine/track';
import {
ConfigExtension,

View File

@ -8,7 +8,7 @@ import {
type useConfirmModal,
} from '@affine/component';
import type { EditorService } from '@affine/core/modules/editor';
import { EditorSettingService } from '@affine/core/modules/editor-settting';
import { EditorSettingService } from '@affine/core/modules/editor-setting';
import { resolveLinkToDoc } from '@affine/core/modules/navigation';
import type { PeekViewService } from '@affine/core/modules/peek-view';
import {

View File

@ -1,6 +1,6 @@
import { toast } from '@affine/component';
import { AppSidebarService } from '@affine/core/modules/app-sidebar';
import { EditorSettingService } from '@affine/core/modules/editor-settting';
import { EditorSettingService } from '@affine/core/modules/editor-setting';
import { WorkbenchService } from '@affine/core/modules/workbench';
import type { DocMode } from '@blocksuite/affine/blocks';
import type { DocCollection } from '@blocksuite/affine/store';

View File

@ -4,17 +4,17 @@ import {
} from '@affine/core/commands';
import { FindInPageService } from '@affine/core/modules/find-in-page/services/find-in-page';
import { track } from '@affine/track';
import { useService } from '@toeverything/infra';
import { useServiceOptional } from '@toeverything/infra';
import { useCallback, useEffect } from 'react';
export function useRegisterFindInPageCommands() {
const findInPage = useService(FindInPageService).findInPage;
const findInPage = useServiceOptional(FindInPageService)?.findInPage;
const toggleVisible = useCallback(() => {
// get the selected text in page
const selection = window.getSelection();
const selectedText = selection?.toString();
findInPage.toggleVisible(selectedText);
findInPage?.toggleVisible(selectedText);
}, [findInPage]);
useEffect(() => {

View File

@ -1,3 +1,4 @@
// todo(@pengx17): remove jotai
import { UrlService } from '@affine/core/modules/url';
import type { UpdateMeta } from '@affine/electron-api';
import { apis, events } from '@affine/electron-api';

View File

@ -1,4 +1,4 @@
import { EditorSettingService } from '@affine/core/modules/editor-settting';
import { EditorSettingService } from '@affine/core/modules/editor-setting';
import { JournalService } from '@affine/core/modules/journal';
import { i18nTime } from '@affine/i18n';
import { track } from '@affine/track';

View File

@ -1,9 +1,14 @@
import { AppSidebarService } from '@affine/core/modules/app-sidebar';
import { DesktopApiService } from '@affine/core/modules/desktop-api/service';
import { I18nService } from '@affine/core/modules/i18n';
import { UrlService } from '@affine/core/modules/url';
import { useI18n } from '@affine/i18n';
import type { AffineEditorContainer } from '@blocksuite/affine/presets';
import { useService, WorkspaceService } from '@toeverything/infra';
import {
useService,
useServiceOptional,
WorkspaceService,
} from '@toeverything/infra';
import { useStore } from 'jotai';
import { useTheme } from 'next-themes';
import { useEffect } from 'react';
@ -21,7 +26,7 @@ import {
} from '../../commands';
import { usePageHelper } from '../../components/blocksuite/block-suite-page-list/utils';
import { CreateWorkspaceDialogService } from '../../modules/create-workspace';
import { EditorSettingService } from '../../modules/editor-settting';
import { EditorSettingService } from '../../modules/editor-setting';
import { CMDKQuickSearchService } from '../../modules/quicksearch/services/cmdk';
import { useActiveBlocksuiteEditor } from './use-block-suite-editor';
import { useNavigateHelper } from './use-navigate-helper';
@ -77,6 +82,9 @@ export function useRegisterWorkspaceCommands() {
const appSidebarService = useService(AppSidebarService);
const i18n = useService(I18nService).i18n;
const quitAndInstall =
useServiceOptional(DesktopApiService)?.handler.updater.quitAndInstall;
useEffect(() => {
const unsub = registerCMDKCommand(cmdkQuickSearchService, editor);
@ -87,15 +95,20 @@ export function useRegisterWorkspaceCommands() {
// register AffineUpdatesCommands
useEffect(() => {
if (!quitAndInstall) {
return;
}
const unsub = registerAffineUpdatesCommands({
store,
t,
quitAndInstall,
});
return () => {
unsub();
};
}, [store, t]);
}, [quitAndInstall, store, t]);
// register AffineNavigationCommands
useEffect(() => {

View File

@ -4,6 +4,7 @@ import {
resolveGlobalLoadingEventAtom,
} from '@affine/component/global-loading';
import { SidebarSwitch } from '@affine/core/modules/app-sidebar/views';
import { WorkspaceDesktopApiService } from '@affine/core/modules/desktop-api/service';
import { useI18n } from '@affine/i18n';
import { type DocMode, ZipTransformer } from '@blocksuite/affine/blocks';
import {
@ -33,7 +34,7 @@ import { Map as YMap } from 'yjs';
import { AIProvider } from '../../blocksuite/presets/ai';
import { AppTabsHeader } from '../../modules/app-tabs-header';
import { EditorSettingService } from '../../modules/editor-settting';
import { EditorSettingService } from '../../modules/editor-setting';
import { NavigationButtons } from '../../modules/navigation';
import { useRegisterNavigationCommands } from '../../modules/navigation/view/use-register-navigation-commands';
import { QuickSearchContainer } from '../../modules/quicksearch';
@ -179,6 +180,8 @@ export const WorkspaceLayoutProviders = ({ children }: PropsWithChildren) => {
};
const DesktopLayout = ({ children }: PropsWithChildren) => {
// is there a better way to make sure service is always available even if it's not explicitly used?
useService(WorkspaceDesktopApiService);
return (
<div className={styles.desktopAppViewContainer}>
<div className={styles.desktopTabsHeader}>

View File

@ -11,7 +11,7 @@ import { EditorService } from '../modules/editor';
import {
EditorSettingService,
fontStyleOptions,
} from '../modules/editor-settting';
} from '../modules/editor-setting';
import { BlockSuiteEditor as Editor } from './blocksuite/block-suite-editor';
import * as styles from './page-detail-editor.css';

View File

@ -1,10 +1,11 @@
import { NotificationCenter, notify } from '@affine/component';
import { events } from '@affine/electron-api';
import { DesktopApiService } from '@affine/core/modules/desktop-api/service';
import { WorkspaceFlavour } from '@affine/env/workspace';
import {
GlobalContextService,
useLiveData,
useService,
useServiceOptional,
WorkspaceService,
WorkspacesService,
} from '@toeverything/infra';
@ -61,17 +62,16 @@ export const Setting = () => {
[setOpenSettingModalAtom]
);
const desktopApi = useServiceOptional(DesktopApiService);
useEffect(() => {
if (BUILD_CONFIG.isElectron) {
return events?.applicationMenu.openAboutPageInSettingModal(() =>
setOpenSettingModalAtom({
activeTab: 'about',
open: true,
})
);
}
return;
}, [setOpenSettingModalAtom]);
return desktopApi?.events?.applicationMenu.openAboutPageInSettingModal(() =>
setOpenSettingModalAtom({
activeTab: 'about',
open: true,
})
);
}, [desktopApi?.events?.applicationMenu, setOpenSettingModalAtom]);
if (!open) {
return null;

View File

@ -1,37 +1,9 @@
import { apis, events } from '@affine/electron-api';
import { useAtomValue } from 'jotai';
import { atomWithObservable } from 'jotai/utils';
import { useCallback } from 'react';
import { combineLatest, map, Observable } from 'rxjs';
import { DesktopApiService } from '@affine/core/modules/desktop-api/service';
import { useService } from '@toeverything/infra';
import { useCallback, useEffect, useState } from 'react';
import * as style from './style.css';
const maximized$ = new Observable<boolean>(subscriber => {
subscriber.next(false);
if (events) {
return events.ui.onMaximized(res => {
subscriber.next(res);
});
}
return () => {};
});
const fullscreen$ = new Observable<boolean>(subscriber => {
subscriber.next(false);
if (events) {
return events.ui.onFullScreen(res => {
subscriber.next(res);
});
}
return () => {};
});
const maximizedAtom = atomWithObservable(() => {
return combineLatest([maximized$, fullscreen$]).pipe(
map(([maximized, fullscreen]) => maximized || fullscreen)
);
});
const minimizeSVG = (
<svg
width="10"
@ -93,23 +65,34 @@ const unmaximizedSVG = (
);
export const WindowsAppControls = () => {
const handleMinimizeApp = useCallback(() => {
apis?.ui.handleMinimizeApp().catch(err => {
console.error(err);
});
}, []);
const handleMaximizeApp = useCallback(() => {
apis?.ui.handleMaximizeApp().catch(err => {
console.error(err);
});
}, []);
const handleCloseApp = useCallback(() => {
apis?.ui.handleCloseApp().catch(err => {
console.error(err);
});
}, []);
const desktopApi = useService(DesktopApiService);
const maximized = useAtomValue(maximizedAtom);
const handleMinimizeApp = useCallback(() => {
desktopApi.handler.ui.handleMinimizeApp().catch(err => {
console.error(err);
});
}, [desktopApi.handler.ui]);
const handleMaximizeApp = useCallback(() => {
desktopApi.handler.ui.handleMaximizeApp().catch(err => {
console.error(err);
});
}, [desktopApi.handler.ui]);
const handleCloseApp = useCallback(() => {
desktopApi.handler.ui.handleCloseApp().catch(err => {
console.error(err);
});
}, [desktopApi.handler.ui]);
const [maximized, setMaximized] = useState(false);
const [fullscreen, setFullscreen] = useState(false);
useEffect(() => {
return desktopApi.events.ui.onMaximized(setMaximized);
}, [desktopApi.events.ui]);
useEffect(() => {
return desktopApi.events.ui.onFullScreen(setFullscreen);
}, [desktopApi.events.ui]);
return (
<div
@ -128,7 +111,7 @@ export const WindowsAppControls = () => {
className={style.windowAppControl}
onClick={handleMaximizeApp}
>
{maximized ? unmaximizedSVG : maximizeSVG}
{maximized || fullscreen ? unmaximizedSVG : maximizeSVG}
</button>
<button
data-type="close"

View File

@ -21,7 +21,6 @@ import {
import { ExplorerTags } from '@affine/core/modules/explorer/views/sections/tags';
import { CMDKQuickSearchService } from '@affine/core/modules/quicksearch/services/cmdk';
import { isNewTabTrigger } from '@affine/core/utils';
import { apis, events } from '@affine/electron-api';
import { useI18n } from '@affine/i18n';
import { track } from '@affine/track';
import type { Doc } from '@blocksuite/affine/store';
@ -39,7 +38,7 @@ import {
} from '@toeverything/infra';
import { useSetAtom } from 'jotai';
import type { MouseEvent, ReactElement } from 'react';
import { useCallback, useEffect } from 'react';
import { useCallback } from 'react';
import { WorkbenchService } from '../../modules/workbench';
import { usePageHelper } from '../blocksuite/block-suite-page-list/utils';
@ -105,25 +104,6 @@ export const RootAppSidebar = (): ReactElement => {
[pageHelper]
);
useEffect(() => {
if (BUILD_CONFIG.isElectron) {
return events?.applicationMenu.onNewPageAction(() => {
apis?.ui
.isActiveTab()
.then(isActive => {
if (!isActive) {
return;
}
onClickNewPage();
})
.catch(err => {
console.error(err);
});
});
}
return;
}, [onClickNewPage]);
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
const onOpenSettingModal = useCallback(() => {

View File

@ -3,8 +3,12 @@ import {
NotFoundPage,
} from '@affine/component/not-found-page';
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import { apis } from '@affine/electron-api';
import { useLiveData, useService } from '@toeverything/infra';
import { DesktopApiService } from '@affine/core/modules/desktop-api/service';
import {
useLiveData,
useService,
useServiceOptional,
} from '@toeverything/infra';
import type { ReactElement } from 'react';
import { useCallback, useEffect, useState } from 'react';
@ -22,6 +26,7 @@ export const PageNotFound = ({
noPermission?: boolean;
}): ReactElement => {
const authService = useService(AuthService);
const desktopApi = useServiceOptional(DesktopApiService);
const account = useLiveData(authService.session.account$);
const { jumpToIndex } = useNavigateHelper();
const [open, setOpen] = useState(false);
@ -41,8 +46,8 @@ export const PageNotFound = ({
}, [authService]);
useEffect(() => {
apis?.ui.pingAppLayoutReady().catch(console.error);
}, []);
desktopApi?.handler.ui.pingAppLayoutReady().catch(console.error);
}, [desktopApi]);
// not using workbench location or router location deliberately
// strip the origin

View File

@ -1,8 +1,9 @@
import { apis } from '@affine/electron-api';
import { DesktopApiService } from '@affine/core/modules/desktop-api/service';
import { WorkspaceFlavour } from '@affine/env/workspace';
import {
useLiveData,
useService,
useServiceOptional,
WorkspacesService,
} from '@toeverything/infra';
import {
@ -118,9 +119,11 @@ export const Component = ({
defaultIndexRoute,
]);
const desktopApi = useServiceOptional(DesktopApiService);
useEffect(() => {
apis?.ui.pingAppLayoutReady().catch(console.error);
}, []);
desktopApi?.handler.ui.pingAppLayoutReady().catch(console.error);
}, [desktopApi]);
useEffect(() => {
setCreating(true);

View File

@ -1,5 +1,5 @@
import { apis } from '@affine/electron-api';
import { assertExists } from '@blocksuite/affine/global/utils';
import { DesktopApiService } from '@affine/core/modules/desktop-api/service';
import { useServiceOptional } from '@toeverything/infra';
import { useCallback } from 'react';
import { redirect } from 'react-router-dom';
@ -25,18 +25,18 @@ export const loader = () => {
export const Component = () => {
const { jumpToIndex } = useNavigateHelper();
const [, setOnboarding] = useAppConfigStorage('onBoarding');
const desktopApi = useServiceOptional(DesktopApiService);
const openApp = useCallback(() => {
if (BUILD_CONFIG.isElectron) {
assertExists(apis);
apis.ui.handleOpenMainApp().catch(err => {
desktopApi?.handler.ui.handleOpenMainApp().catch(err => {
console.log('failed to open main app', err);
});
} else {
jumpToIndex(RouteLogic.REPLACE);
setOnboarding(false);
}
}, [jumpToIndex, setOnboarding]);
}, [jumpToIndex, setOnboarding, desktopApi]);
return <Onboarding onOpenApp={openApp} />;
};

View File

@ -53,7 +53,7 @@ export const Component = (): ReactElement => {
match &&
match.params.docId &&
match.params.workspaceId &&
// // TODO(eyhn): need a better way to check if it's a docId
// TODO(eyhn): need a better way to check if it's a docId
workbenchRoutes.find(route =>
matchPath(route.path, '/' + match.params.docId)
)?.path === '/:pageId'

View File

@ -2,7 +2,7 @@ import { getBaseFontStyleOptions } from '@affine/core/components/affine/setting-
import {
EditorSettingService,
type FontFamily,
} from '@affine/core/modules/editor-settting';
} from '@affine/core/modules/editor-setting';
import { useI18n } from '@affine/i18n';
import { useLiveData, useService } from '@toeverything/infra';
import { useCallback, useMemo } from 'react';

View File

@ -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]);
}

View File

@ -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;
}

View File

@ -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) => {

View File

@ -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() {

View File

@ -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) => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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);
}
}

View File

@ -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]);
}

View File

@ -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);

View File

@ -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';

View File

@ -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';

View File

@ -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();
}
}

View File

@ -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]);
}

View File

@ -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);
}
}

View File

@ -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));
},
});
}));
}

View File

@ -116,7 +116,7 @@ export class Workbench extends Entity {
show?: boolean;
} = {}
) {
this.newTabHandler({
this.newTabHandler.handle({
basename: this.basename$.value,
to,
show: show ?? true,

View File

@ -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]);
}

View File

@ -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,
})

View File

@ -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);
}
}

View File

@ -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
);
}

View File

@ -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
);
}
}

View File

@ -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);
}
}

View File

@ -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

View File

@ -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);
},
};
});
}

View File

@ -7,13 +7,13 @@ import type {
handlers as mainHandlers,
} from '@affine/electron/main/exposed';
import type { appInfo as exposedAppInfo } from '@affine/electron/preload/electron-api';
import type { sharedStorage as exposedSharedStorage } from '@affine/electron/preload/shared-storage';
import type { SharedStorage } from '@affine/electron/preload/shared-storage';
type MainHandlers = typeof mainHandlers;
type HelperHandlers = typeof helperHandlers;
type HelperEvents = typeof helperEvents;
type MainEvents = typeof mainEvents;
type ClientHandler = {
export type ClientHandler = {
[namespace in keyof MainHandlers]: {
[method in keyof MainHandlers[namespace]]: MainHandlers[namespace][method] extends (
arg0: any,
@ -27,17 +27,19 @@ type ClientHandler = {
: never;
};
} & HelperHandlers;
type ClientEvents = MainEvents & HelperEvents;
export type ClientEvents = MainEvents & HelperEvents;
export const appInfo = (globalThis as any).__appInfo as
| typeof exposedAppInfo
| null;
export const apis = (globalThis as any).__apis as ClientHandler | null;
export const events = (globalThis as any).__events as ClientEvents | null;
export const apis = (globalThis as any).__apis as ClientHandler | undefined;
export const events = (globalThis as any).__events as ClientEvents | undefined;
export const sharedStorage = (globalThis as any).__sharedStorage as
| typeof exposedSharedStorage
| null;
| SharedStorage
| undefined;
export type { SharedStorage };
export type { UpdateMeta } from '@affine/electron/main/updater/event';
export {

View File

@ -529,6 +529,7 @@ __metadata:
link-preview-js: "npm:^3.0.5"
lodash-es: "npm:^4.17.21"
nanoid: "npm:^5.0.7"
next-themes: "npm:^0.3.0"
react: "npm:^18.2.0"
react-dom: "npm:^18.2.0"
react-router-dom: "npm:^6.22.3"