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

fix AF-1394
This commit is contained in:
pengx17
2024-10-30 09:16:20 +00:00
parent 50bae9c3e6
commit a791481ac8
81 changed files with 788 additions and 567 deletions
@@ -73,6 +73,7 @@
"async-call-rpc": "^6.4.2", "async-call-rpc": "^6.4.2",
"electron-updater": "^6.2.1", "electron-updater": "^6.2.1",
"link-preview-js": "^3.0.5", "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" "yjs": "patch:yjs@npm%3A13.6.18#~/.yarn/patches/yjs-npm-13.6.18-ad0d5f7c43.patch"
}, },
"build": { "build": {
@@ -7,6 +7,11 @@ import { router } from '@affine/core/desktop/router';
import { configureCommonModules } from '@affine/core/modules'; import { configureCommonModules } from '@affine/core/modules';
import { configureAppTabsHeaderModule } from '@affine/core/modules/app-tabs-header'; import { configureAppTabsHeaderModule } from '@affine/core/modules/app-tabs-header';
import { ValidatorProvider } from '@affine/core/modules/cloud'; 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 { I18nProvider } from '@affine/core/modules/i18n';
import { configureElectronStateStorageImpls } from '@affine/core/modules/storage'; import { configureElectronStateStorageImpls } from '@affine/core/modules/storage';
import { CustomThemeModifier } from '@affine/core/modules/theme-editor'; import { CustomThemeModifier } from '@affine/core/modules/theme-editor';
@@ -21,7 +26,6 @@ import {
configureSqliteWorkspaceEngineStorageProvider, configureSqliteWorkspaceEngineStorageProvider,
} from '@affine/core/modules/workspace-engine'; } from '@affine/core/modules/workspace-engine';
import createEmotionCache from '@affine/core/utils/create-emotion-cache'; import createEmotionCache from '@affine/core/utils/create-emotion-cache';
import { apis, appInfo } from '@affine/electron-api';
import { CacheProvider } from '@emotion/react'; import { CacheProvider } from '@emotion/react';
import { import {
Framework, Framework,
@@ -32,6 +36,8 @@ import {
import { Suspense } from 'react'; import { Suspense } from 'react';
import { RouterProvider } from 'react-router-dom'; import { RouterProvider } from 'react-router-dom';
import { DesktopThemeSync } from './theme-sync';
const desktopWhiteList = [ const desktopWhiteList = [
'/open-app/signin-redirect', '/open-app/signin-redirect',
'/open-app/url', '/open-app/url',
@@ -64,26 +70,38 @@ configureSqliteWorkspaceEngineStorageProvider(framework);
configureSqliteUserspaceStorageProvider(framework); configureSqliteUserspaceStorageProvider(framework);
configureDesktopWorkbenchModule(framework); configureDesktopWorkbenchModule(framework);
configureAppTabsHeaderModule(framework); configureAppTabsHeaderModule(framework);
framework.impl(PopupWindowProvider, { configureFindInPageModule(framework);
open: (url: string) => { configureDesktopApiModule(framework);
apis?.ui.openExternal(url).catch(e => {
console.error('Failed to open external URL', e); 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, { framework.impl(ClientSchemeProvider, p => {
getClientScheme() { const appInfo = p.get(DesktopApiService).appInfo;
return appInfo?.scheme; return {
}, getClientScheme() {
return appInfo?.scheme;
},
};
}); });
framework.impl(ValidatorProvider, { framework.impl(ValidatorProvider, p => {
async validate(_challenge, resource) { const apis = p.get(DesktopApiService).api;
const token = await apis?.ui?.getChallengeResponse(resource); return {
if (!token) { async validate(_challenge, resource) {
throw new Error('Challenge failed'); const token = await apis.handler.ui.getChallengeResponse(resource);
} if (!token) {
return token; throw new Error('Challenge failed');
}, }
return token;
},
};
}); });
const frameworkProvider = framework.provider(); const frameworkProvider = framework.provider();
@@ -100,6 +118,7 @@ export function App() {
<CacheProvider value={cache}> <CacheProvider value={cache}>
<I18nProvider> <I18nProvider>
<AffineContext store={getCurrentStore()}> <AffineContext store={getCurrentStore()}>
<DesktopThemeSync />
<Telemetry /> <Telemetry />
<CustomThemeModifier /> <CustomThemeModifier />
<GlobalLoading /> <GlobalLoading />
@@ -1,21 +1,8 @@
import './setup'; import './setup';
import { appConfigProxy } from '@affine/core/components/hooks/use-app-config-storage'; import { appConfigProxy } from '@affine/core/components/hooks/use-app-config-storage';
import { apis, appInfo, events } from '@affine/electron-api'; import { StrictMode } from 'react';
import {
init,
reactRouterV6BrowserTracingIntegration,
setTags,
} from '@sentry/react';
import { debounce } from 'lodash-es';
import { StrictMode, useEffect } from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import {
createRoutesFromChildren,
matchRoutes,
useLocation,
useNavigationType,
} from 'react-router-dom';
import { App } from './app'; import { App } from './app';
@@ -26,82 +13,6 @@ function main() {
.getSync() .getSync()
.catch(() => console.error('failed to load app config')); .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(); mountApp();
} }
@@ -6,6 +6,7 @@ import {
AppTabsHeader, AppTabsHeader,
configureAppTabsHeaderModule, configureAppTabsHeaderModule,
} from '@affine/core/modules/app-tabs-header'; } from '@affine/core/modules/app-tabs-header';
import { configureDesktopApiModule } from '@affine/core/modules/desktop-api';
import { configureI18nModule, I18nProvider } from '@affine/core/modules/i18n'; import { configureI18nModule, I18nProvider } from '@affine/core/modules/i18n';
import { configureElectronStateStorageImpls } from '@affine/core/modules/storage'; import { configureElectronStateStorageImpls } from '@affine/core/modules/storage';
import { SplitViewFallback } from '@affine/core/modules/workbench/view/split-view/split-view'; import { SplitViewFallback } from '@affine/core/modules/workbench/view/split-view/split-view';
@@ -23,6 +24,7 @@ configureElectronStateStorageImpls(framework);
configureAppTabsHeaderModule(framework); configureAppTabsHeaderModule(framework);
configureAppSidebarModule(framework); configureAppSidebarModule(framework);
configureI18nModule(framework); configureI18nModule(framework);
configureDesktopApiModule(framework);
const frameworkProvider = framework.provider(); const frameworkProvider = framework.provider();
export function App() { export function App() {
@@ -1,28 +1,16 @@
import './setup'; import './setup';
import { apis, events } from '@affine/electron-api'; import { events } from '@affine/electron-api';
import { StrictMode } from 'react'; import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { App } from './app'; import { App } from './app';
async function main() { 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) => { const handleActive = (active: boolean | undefined) => {
document.documentElement.dataset.active = String(active); 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); events?.ui.onTabShellViewActiveChange(handleActive);
mountApp(); mountApp();
} }
@@ -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;
};
@@ -56,6 +56,8 @@ export const appInfo = {
scheme, scheme,
}; };
export type AppInfo = typeof appInfo;
function getMainAPIs() { function getMainAPIs() {
const meta: ExposedMeta = (() => { const meta: ExposedMeta = (() => {
const val = process.argv const val = process.argv
@@ -94,3 +94,5 @@ export const sharedStorage = {
globalState, globalState,
globalCache, globalCache,
}; };
export type SharedStorage = typeof sharedStorage;
@@ -1,33 +1,12 @@
import { apis } from '@affine/electron-api'; import { ThemeProvider as NextThemeProvider } from 'next-themes';
import { ThemeProvider as NextThemeProvider, useTheme } from 'next-themes';
import type { PropsWithChildren } from 'react'; import type { PropsWithChildren } from 'react';
import { memo, useRef } from 'react';
const themes = ['dark', 'light']; 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) => { export const ThemeProvider = ({ children }: PropsWithChildren) => {
return ( return (
<NextThemeProvider themes={themes} enableSystem={true}> <NextThemeProvider themes={themes} enableSystem={true}>
{children} {children}
<DesktopThemeSync />
</NextThemeProvider> </NextThemeProvider>
); );
}; };
@@ -5,7 +5,7 @@ import { appSettingAtom } from '@toeverything/infra';
import type { createStore } from 'jotai'; import type { createStore } from 'jotai';
import type { useTheme } from 'next-themes'; import type { useTheme } from 'next-themes';
import type { EditorSettingService } from '../modules/editor-settting'; import type { EditorSettingService } from '../modules/editor-setting';
import { registerAffineCommand } from './registry'; import { registerAffineCommand } from './registry';
export function registerAffineSettingsCommands({ export function registerAffineSettingsCommands({
@@ -1,6 +1,5 @@
import { notify } from '@affine/component'; import { notify } from '@affine/component';
import { updateReadyAtom } from '@affine/core/components/hooks/use-app-updater'; import { updateReadyAtom } from '@affine/core/components/hooks/use-app-updater';
import { apis } from '@affine/electron-api';
import type { useI18n } from '@affine/i18n'; import type { useI18n } from '@affine/i18n';
import { track } from '@affine/track'; import { track } from '@affine/track';
import { ResetIcon } from '@blocksuite/icons/rc'; import { ResetIcon } from '@blocksuite/icons/rc';
@@ -11,9 +10,11 @@ import { registerAffineCommand } from './registry';
export function registerAffineUpdatesCommands({ export function registerAffineUpdatesCommands({
t, t,
store, store,
quitAndInstall,
}: { }: {
t: ReturnType<typeof useI18n>; t: ReturnType<typeof useI18n>;
store: ReturnType<typeof createStore>; store: ReturnType<typeof createStore>;
quitAndInstall: () => Promise<void>;
}) { }) {
const unsubs: Array<() => void> = []; const unsubs: Array<() => void> = [];
@@ -27,7 +28,7 @@ export function registerAffineUpdatesCommands({
run() { run() {
track.$.cmdk.updates.quitAndInstall(); track.$.cmdk.updates.quitAndInstall();
apis?.updater.quitAndInstall().catch(err => { quitAndInstall().catch(err => {
notify.error({ notify.error({
title: 'Failed to restart to upgrade', title: 'Failed to restart to upgrade',
message: 'Please restart the app manually to upgrade.', message: 'Please restart the app manually to upgrade.',
@@ -1,10 +1,14 @@
import { Button } from '@affine/component'; import { Button } from '@affine/component';
import { SettingRow } from '@affine/component/setting-components'; 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 { ThemeEditorService } from '@affine/core/modules/theme-editor';
import { UrlService } from '@affine/core/modules/url'; import { UrlService } from '@affine/core/modules/url';
import { apis } from '@affine/electron-api';
import { DeleteIcon } from '@blocksuite/icons/rc'; 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 { cssVar } from '@toeverything/theme';
import { useCallback } from 'react'; import { useCallback } from 'react';
@@ -12,14 +16,15 @@ export const ThemeEditorSetting = () => {
const themeEditor = useService(ThemeEditorService); const themeEditor = useService(ThemeEditorService);
const modified = useLiveData(themeEditor.modified$); const modified = useLiveData(themeEditor.modified$);
const urlService = useService(UrlService); const urlService = useService(UrlService);
const desktopApi = useServiceOptional(DesktopApiService);
const open = useCallback(() => { const open = useCallback(() => {
if (BUILD_CONFIG.isElectron) { if (desktopApi) {
apis?.ui.openThemeEditor().catch(console.error); desktopApi.handler.ui.openThemeEditor().catch(console.error);
} else if (BUILD_CONFIG.isMobileWeb || BUILD_CONFIG.isWeb) { } else if (BUILD_CONFIG.isMobileWeb || BUILD_CONFIG.isWeb) {
urlService.openPopupWindow(location.origin + '/theme-editor'); urlService.openPopupWindow(location.origin + '/theme-editor');
} }
}, [urlService]); }, [desktopApi, urlService]);
return ( return (
<SettingRow <SettingRow
@@ -6,7 +6,7 @@ import {
Slider, Slider,
} from '@affine/component'; } from '@affine/component';
import { SettingRow } from '@affine/component/setting-components'; 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 { useI18n } from '@affine/i18n';
import { import {
ConnectorMode, ConnectorMode,
@@ -5,7 +5,7 @@ import {
type RadioItem, type RadioItem,
} from '@affine/component'; } from '@affine/component';
import { SettingRow } from '@affine/component/setting-components'; 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 { useI18n } from '@affine/i18n';
import { LayoutType, MindmapStyle } from '@blocksuite/affine/blocks'; import { LayoutType, MindmapStyle } from '@blocksuite/affine/blocks';
import type { Doc } from '@blocksuite/affine/store'; import type { Doc } from '@blocksuite/affine/store';
@@ -6,7 +6,7 @@ import {
Slider, Slider,
} from '@affine/component'; } from '@affine/component';
import { SettingRow } from '@affine/component/setting-components'; 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 { useI18n } from '@affine/i18n';
import { import {
createEnumMap, createEnumMap,
@@ -1,6 +1,6 @@
import { MenuItem, MenuTrigger, Slider } from '@affine/component'; import { MenuItem, MenuTrigger, Slider } from '@affine/component';
import { SettingRow } from '@affine/component/setting-components'; 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 { useI18n } from '@affine/i18n';
import { LineColor, LineColorMap } from '@blocksuite/affine/blocks'; import { LineColor, LineColorMap } from '@blocksuite/affine/blocks';
import type { Doc } from '@blocksuite/affine/store'; import type { Doc } from '@blocksuite/affine/store';
@@ -6,7 +6,7 @@ import {
Slider, Slider,
} from '@affine/component'; } from '@affine/component';
import { SettingRow } from '@affine/component/setting-components'; 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 { useI18n } from '@affine/i18n';
import type { EditorHost } from '@blocksuite/affine/block-std'; import type { EditorHost } from '@blocksuite/affine/block-std';
import type { import type {
@@ -1,6 +1,6 @@
import { Skeleton } from '@affine/component'; import { Skeleton } from '@affine/component';
import type { EditorSettingSchema } from '@affine/core/modules/editor-settting'; import type { EditorSettingSchema } from '@affine/core/modules/editor-setting';
import { EditorSettingService } from '@affine/core/modules/editor-settting'; import { EditorSettingService } from '@affine/core/modules/editor-setting';
import type { EditorHost } from '@blocksuite/affine/block-std'; import type { EditorHost } from '@blocksuite/affine/block-std';
import { BlockStdScope } from '@blocksuite/affine/block-std'; import { BlockStdScope } from '@blocksuite/affine/block-std';
import type { GfxPrimitiveElementModel } from '@blocksuite/affine/block-std/gfx'; import type { GfxPrimitiveElementModel } from '@blocksuite/affine/block-std/gfx';
@@ -5,7 +5,7 @@ import {
type RadioItem, type RadioItem,
} from '@affine/component'; } from '@affine/component';
import { SettingRow } from '@affine/component/setting-components'; 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 { useI18n } from '@affine/i18n';
import { import {
FontFamily, FontFamily,
@@ -20,7 +20,7 @@ import {
EditorSettingService, EditorSettingService,
type FontFamily, type FontFamily,
fontStyleOptions, fontStyleOptions,
} from '@affine/core/modules/editor-settting'; } from '@affine/core/modules/editor-setting';
import { import {
type FontData, type FontData,
SystemFontFamilyService, SystemFontFamilyService,
@@ -3,7 +3,7 @@ import {
SettingRow, SettingRow,
SettingWrapper, SettingWrapper,
} from '@affine/component/setting-components'; } 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 { useI18n } from '@affine/i18n';
import { useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
import { useCallback } from 'react'; import { useCallback } from 'react';
@@ -3,9 +3,13 @@ import { SettingRow } from '@affine/component/setting-components';
import { Button } from '@affine/component/ui/button'; import { Button } from '@affine/component/ui/button';
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import { useSystemOnline } from '@affine/core/components/hooks/use-system-online'; 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 { useI18n } from '@affine/i18n';
import type { Workspace, WorkspaceMetadata } from '@toeverything/infra'; import {
useService,
type Workspace,
type WorkspaceMetadata,
} from '@toeverything/infra';
import { useState } from 'react'; import { useState } from 'react';
interface ExportPanelProps { interface ExportPanelProps {
@@ -13,7 +17,7 @@ interface ExportPanelProps {
workspace: Workspace | null; workspace: Workspace | null;
} }
export const ExportPanel = ({ export const DesktopExportPanel = ({
workspaceMetadata, workspaceMetadata,
workspace, workspace,
}: ExportPanelProps) => { }: ExportPanelProps) => {
@@ -21,6 +25,7 @@ export const ExportPanel = ({
const t = useI18n(); const t = useI18n();
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const isOnline = useSystemOnline(); const isOnline = useSystemOnline();
const desktopApi = useService(DesktopApiService);
const onExport = useAsyncCallback(async () => { const onExport = useAsyncCallback(async () => {
if (saving || !workspace) { if (saving || !workspace) {
@@ -33,7 +38,7 @@ export const ExportPanel = ({
await workspace.engine.blob.sync(); await workspace.engine.blob.sync();
} }
const result = await apis?.dialog.saveDBFileAs(workspaceId); const result = await desktopApi.handler.dialog.saveDBFileAs(workspaceId);
if (result?.error) { if (result?.error) {
throw new Error(result.error); throw new Error(result.error);
} else if (!result?.canceled) { } else if (!result?.canceled) {
@@ -44,7 +49,7 @@ export const ExportPanel = ({
} finally { } finally {
setSaving(false); setSaving(false);
} }
}, [isOnline, saving, t, workspace, workspaceId]); }, [isOnline, saving, t, workspace, workspaceId, desktopApi]);
return ( return (
<SettingRow name={t['Export']()} desc={t['Export Description']()}> <SettingRow name={t['Export']()} desc={t['Export Description']()}>
@@ -13,7 +13,7 @@ import { useCallback } from 'react';
import { DeleteLeaveWorkspace } from './delete-leave-workspace'; import { DeleteLeaveWorkspace } from './delete-leave-workspace';
import { EnableCloudPanel } from './enable-cloud'; import { EnableCloudPanel } from './enable-cloud';
import { ExportPanel } from './export'; import { DesktopExportPanel } from './export';
import { LabelsPanel } from './labels'; import { LabelsPanel } from './labels';
import { MembersPanel } from './members'; import { MembersPanel } from './members';
import { ProfilePanel } from './profile'; import { ProfilePanel } from './profile';
@@ -71,7 +71,7 @@ export const WorkspaceSettingDetail = ({
<SharingPanel /> <SharingPanel />
{BUILD_CONFIG.isElectron && ( {BUILD_CONFIG.isElectron && (
<SettingWrapper title={t['Storage and Export']()}> <SettingWrapper title={t['Storage and Export']()}>
<ExportPanel <DesktopExportPanel
workspace={workspace} workspace={workspace}
workspaceMetadata={workspaceMetadata} workspaceMetadata={workspaceMetadata}
/> />
@@ -5,7 +5,7 @@ import {
} from '@affine/component'; } from '@affine/component';
import { ServerConfigService } from '@affine/core/modules/cloud'; import { ServerConfigService } from '@affine/core/modules/cloud';
import { EditorService } from '@affine/core/modules/editor'; 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 { JournalService } from '@affine/core/modules/journal';
import { toURLSearchParams } from '@affine/core/modules/navigation'; import { toURLSearchParams } from '@affine/core/modules/navigation';
import { PeekViewService } from '@affine/core/modules/peek-view/services/peek-view'; import { PeekViewService } from '@affine/core/modules/peek-view/services/peek-view';
@@ -2,7 +2,7 @@ import {
AIEdgelessRootBlockSpec, AIEdgelessRootBlockSpec,
AIPageRootBlockSpec, AIPageRootBlockSpec,
} from '@affine/core/blocksuite/presets/ai'; } 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 { mixpanel } from '@affine/track';
import { import {
ConfigExtension, ConfigExtension,
@@ -8,7 +8,7 @@ import {
type useConfirmModal, type useConfirmModal,
} from '@affine/component'; } from '@affine/component';
import type { EditorService } from '@affine/core/modules/editor'; 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 { resolveLinkToDoc } from '@affine/core/modules/navigation';
import type { PeekViewService } from '@affine/core/modules/peek-view'; import type { PeekViewService } from '@affine/core/modules/peek-view';
import { import {
@@ -1,6 +1,6 @@
import { toast } from '@affine/component'; import { toast } from '@affine/component';
import { AppSidebarService } from '@affine/core/modules/app-sidebar'; 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 { WorkbenchService } from '@affine/core/modules/workbench';
import type { DocMode } from '@blocksuite/affine/blocks'; import type { DocMode } from '@blocksuite/affine/blocks';
import type { DocCollection } from '@blocksuite/affine/store'; import type { DocCollection } from '@blocksuite/affine/store';
@@ -4,17 +4,17 @@ import {
} from '@affine/core/commands'; } from '@affine/core/commands';
import { FindInPageService } from '@affine/core/modules/find-in-page/services/find-in-page'; import { FindInPageService } from '@affine/core/modules/find-in-page/services/find-in-page';
import { track } from '@affine/track'; import { track } from '@affine/track';
import { useService } from '@toeverything/infra'; import { useServiceOptional } from '@toeverything/infra';
import { useCallback, useEffect } from 'react'; import { useCallback, useEffect } from 'react';
export function useRegisterFindInPageCommands() { export function useRegisterFindInPageCommands() {
const findInPage = useService(FindInPageService).findInPage; const findInPage = useServiceOptional(FindInPageService)?.findInPage;
const toggleVisible = useCallback(() => { const toggleVisible = useCallback(() => {
// get the selected text in page // get the selected text in page
const selection = window.getSelection(); const selection = window.getSelection();
const selectedText = selection?.toString(); const selectedText = selection?.toString();
findInPage.toggleVisible(selectedText); findInPage?.toggleVisible(selectedText);
}, [findInPage]); }, [findInPage]);
useEffect(() => { useEffect(() => {
@@ -1,3 +1,4 @@
// todo(@pengx17): remove jotai
import { UrlService } from '@affine/core/modules/url'; import { UrlService } from '@affine/core/modules/url';
import type { UpdateMeta } from '@affine/electron-api'; import type { UpdateMeta } from '@affine/electron-api';
import { apis, events } from '@affine/electron-api'; import { apis, events } from '@affine/electron-api';
@@ -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 { JournalService } from '@affine/core/modules/journal';
import { i18nTime } from '@affine/i18n'; import { i18nTime } from '@affine/i18n';
import { track } from '@affine/track'; import { track } from '@affine/track';
@@ -1,9 +1,14 @@
import { AppSidebarService } from '@affine/core/modules/app-sidebar'; 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 { I18nService } from '@affine/core/modules/i18n';
import { UrlService } from '@affine/core/modules/url'; import { UrlService } from '@affine/core/modules/url';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import type { AffineEditorContainer } from '@blocksuite/affine/presets'; 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 { useStore } from 'jotai';
import { useTheme } from 'next-themes'; import { useTheme } from 'next-themes';
import { useEffect } from 'react'; import { useEffect } from 'react';
@@ -21,7 +26,7 @@ import {
} from '../../commands'; } from '../../commands';
import { usePageHelper } from '../../components/blocksuite/block-suite-page-list/utils'; import { usePageHelper } from '../../components/blocksuite/block-suite-page-list/utils';
import { CreateWorkspaceDialogService } from '../../modules/create-workspace'; 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 { CMDKQuickSearchService } from '../../modules/quicksearch/services/cmdk';
import { useActiveBlocksuiteEditor } from './use-block-suite-editor'; import { useActiveBlocksuiteEditor } from './use-block-suite-editor';
import { useNavigateHelper } from './use-navigate-helper'; import { useNavigateHelper } from './use-navigate-helper';
@@ -77,6 +82,9 @@ export function useRegisterWorkspaceCommands() {
const appSidebarService = useService(AppSidebarService); const appSidebarService = useService(AppSidebarService);
const i18n = useService(I18nService).i18n; const i18n = useService(I18nService).i18n;
const quitAndInstall =
useServiceOptional(DesktopApiService)?.handler.updater.quitAndInstall;
useEffect(() => { useEffect(() => {
const unsub = registerCMDKCommand(cmdkQuickSearchService, editor); const unsub = registerCMDKCommand(cmdkQuickSearchService, editor);
@@ -87,15 +95,20 @@ export function useRegisterWorkspaceCommands() {
// register AffineUpdatesCommands // register AffineUpdatesCommands
useEffect(() => { useEffect(() => {
if (!quitAndInstall) {
return;
}
const unsub = registerAffineUpdatesCommands({ const unsub = registerAffineUpdatesCommands({
store, store,
t, t,
quitAndInstall,
}); });
return () => { return () => {
unsub(); unsub();
}; };
}, [store, t]); }, [quitAndInstall, store, t]);
// register AffineNavigationCommands // register AffineNavigationCommands
useEffect(() => { useEffect(() => {
@@ -4,6 +4,7 @@ import {
resolveGlobalLoadingEventAtom, resolveGlobalLoadingEventAtom,
} from '@affine/component/global-loading'; } from '@affine/component/global-loading';
import { SidebarSwitch } from '@affine/core/modules/app-sidebar/views'; import { SidebarSwitch } from '@affine/core/modules/app-sidebar/views';
import { WorkspaceDesktopApiService } from '@affine/core/modules/desktop-api/service';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { type DocMode, ZipTransformer } from '@blocksuite/affine/blocks'; import { type DocMode, ZipTransformer } from '@blocksuite/affine/blocks';
import { import {
@@ -33,7 +34,7 @@ import { Map as YMap } from 'yjs';
import { AIProvider } from '../../blocksuite/presets/ai'; import { AIProvider } from '../../blocksuite/presets/ai';
import { AppTabsHeader } from '../../modules/app-tabs-header'; 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 { NavigationButtons } from '../../modules/navigation';
import { useRegisterNavigationCommands } from '../../modules/navigation/view/use-register-navigation-commands'; import { useRegisterNavigationCommands } from '../../modules/navigation/view/use-register-navigation-commands';
import { QuickSearchContainer } from '../../modules/quicksearch'; import { QuickSearchContainer } from '../../modules/quicksearch';
@@ -179,6 +180,8 @@ export const WorkspaceLayoutProviders = ({ children }: PropsWithChildren) => {
}; };
const DesktopLayout = ({ 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 ( return (
<div className={styles.desktopAppViewContainer}> <div className={styles.desktopAppViewContainer}>
<div className={styles.desktopTabsHeader}> <div className={styles.desktopTabsHeader}>
@@ -11,7 +11,7 @@ import { EditorService } from '../modules/editor';
import { import {
EditorSettingService, EditorSettingService,
fontStyleOptions, fontStyleOptions,
} from '../modules/editor-settting'; } from '../modules/editor-setting';
import { BlockSuiteEditor as Editor } from './blocksuite/block-suite-editor'; import { BlockSuiteEditor as Editor } from './blocksuite/block-suite-editor';
import * as styles from './page-detail-editor.css'; import * as styles from './page-detail-editor.css';
@@ -1,10 +1,11 @@
import { NotificationCenter, notify } from '@affine/component'; 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 { WorkspaceFlavour } from '@affine/env/workspace';
import { import {
GlobalContextService, GlobalContextService,
useLiveData, useLiveData,
useService, useService,
useServiceOptional,
WorkspaceService, WorkspaceService,
WorkspacesService, WorkspacesService,
} from '@toeverything/infra'; } from '@toeverything/infra';
@@ -61,17 +62,16 @@ export const Setting = () => {
[setOpenSettingModalAtom] [setOpenSettingModalAtom]
); );
const desktopApi = useServiceOptional(DesktopApiService);
useEffect(() => { useEffect(() => {
if (BUILD_CONFIG.isElectron) { return desktopApi?.events?.applicationMenu.openAboutPageInSettingModal(() =>
return events?.applicationMenu.openAboutPageInSettingModal(() => setOpenSettingModalAtom({
setOpenSettingModalAtom({ activeTab: 'about',
activeTab: 'about', open: true,
open: true, })
}) );
); }, [desktopApi?.events?.applicationMenu, setOpenSettingModalAtom]);
}
return;
}, [setOpenSettingModalAtom]);
if (!open) { if (!open) {
return null; return null;
@@ -1,37 +1,9 @@
import { apis, events } from '@affine/electron-api'; import { DesktopApiService } from '@affine/core/modules/desktop-api/service';
import { useAtomValue } from 'jotai'; import { useService } from '@toeverything/infra';
import { atomWithObservable } from 'jotai/utils'; import { useCallback, useEffect, useState } from 'react';
import { useCallback } from 'react';
import { combineLatest, map, Observable } from 'rxjs';
import * as style from './style.css'; 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 = ( const minimizeSVG = (
<svg <svg
width="10" width="10"
@@ -93,23 +65,34 @@ const unmaximizedSVG = (
); );
export const WindowsAppControls = () => { export const WindowsAppControls = () => {
const handleMinimizeApp = useCallback(() => { const desktopApi = useService(DesktopApiService);
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 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 ( return (
<div <div
@@ -128,7 +111,7 @@ export const WindowsAppControls = () => {
className={style.windowAppControl} className={style.windowAppControl}
onClick={handleMaximizeApp} onClick={handleMaximizeApp}
> >
{maximized ? unmaximizedSVG : maximizeSVG} {maximized || fullscreen ? unmaximizedSVG : maximizeSVG}
</button> </button>
<button <button
data-type="close" data-type="close"
@@ -21,7 +21,6 @@ import {
import { ExplorerTags } from '@affine/core/modules/explorer/views/sections/tags'; import { ExplorerTags } from '@affine/core/modules/explorer/views/sections/tags';
import { CMDKQuickSearchService } from '@affine/core/modules/quicksearch/services/cmdk'; import { CMDKQuickSearchService } from '@affine/core/modules/quicksearch/services/cmdk';
import { isNewTabTrigger } from '@affine/core/utils'; import { isNewTabTrigger } from '@affine/core/utils';
import { apis, events } from '@affine/electron-api';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { track } from '@affine/track'; import { track } from '@affine/track';
import type { Doc } from '@blocksuite/affine/store'; import type { Doc } from '@blocksuite/affine/store';
@@ -39,7 +38,7 @@ import {
} from '@toeverything/infra'; } from '@toeverything/infra';
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import type { MouseEvent, ReactElement } from 'react'; import type { MouseEvent, ReactElement } from 'react';
import { useCallback, useEffect } from 'react'; import { useCallback } from 'react';
import { WorkbenchService } from '../../modules/workbench'; import { WorkbenchService } from '../../modules/workbench';
import { usePageHelper } from '../blocksuite/block-suite-page-list/utils'; import { usePageHelper } from '../blocksuite/block-suite-page-list/utils';
@@ -105,25 +104,6 @@ export const RootAppSidebar = (): ReactElement => {
[pageHelper] [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 setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
const onOpenSettingModal = useCallback(() => { const onOpenSettingModal = useCallback(() => {
@@ -3,8 +3,12 @@ import {
NotFoundPage, NotFoundPage,
} from '@affine/component/not-found-page'; } from '@affine/component/not-found-page';
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import { apis } from '@affine/electron-api'; import { DesktopApiService } from '@affine/core/modules/desktop-api/service';
import { useLiveData, useService } from '@toeverything/infra'; import {
useLiveData,
useService,
useServiceOptional,
} from '@toeverything/infra';
import type { ReactElement } from 'react'; import type { ReactElement } from 'react';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
@@ -22,6 +26,7 @@ export const PageNotFound = ({
noPermission?: boolean; noPermission?: boolean;
}): ReactElement => { }): ReactElement => {
const authService = useService(AuthService); const authService = useService(AuthService);
const desktopApi = useServiceOptional(DesktopApiService);
const account = useLiveData(authService.session.account$); const account = useLiveData(authService.session.account$);
const { jumpToIndex } = useNavigateHelper(); const { jumpToIndex } = useNavigateHelper();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@@ -41,8 +46,8 @@ export const PageNotFound = ({
}, [authService]); }, [authService]);
useEffect(() => { useEffect(() => {
apis?.ui.pingAppLayoutReady().catch(console.error); desktopApi?.handler.ui.pingAppLayoutReady().catch(console.error);
}, []); }, [desktopApi]);
// not using workbench location or router location deliberately // not using workbench location or router location deliberately
// strip the origin // strip the origin
@@ -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 { WorkspaceFlavour } from '@affine/env/workspace';
import { import {
useLiveData, useLiveData,
useService, useService,
useServiceOptional,
WorkspacesService, WorkspacesService,
} from '@toeverything/infra'; } from '@toeverything/infra';
import { import {
@@ -118,9 +119,11 @@ export const Component = ({
defaultIndexRoute, defaultIndexRoute,
]); ]);
const desktopApi = useServiceOptional(DesktopApiService);
useEffect(() => { useEffect(() => {
apis?.ui.pingAppLayoutReady().catch(console.error); desktopApi?.handler.ui.pingAppLayoutReady().catch(console.error);
}, []); }, [desktopApi]);
useEffect(() => { useEffect(() => {
setCreating(true); setCreating(true);
@@ -1,5 +1,5 @@
import { apis } from '@affine/electron-api'; import { DesktopApiService } from '@affine/core/modules/desktop-api/service';
import { assertExists } from '@blocksuite/affine/global/utils'; import { useServiceOptional } from '@toeverything/infra';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { redirect } from 'react-router-dom'; import { redirect } from 'react-router-dom';
@@ -25,18 +25,18 @@ export const loader = () => {
export const Component = () => { export const Component = () => {
const { jumpToIndex } = useNavigateHelper(); const { jumpToIndex } = useNavigateHelper();
const [, setOnboarding] = useAppConfigStorage('onBoarding'); const [, setOnboarding] = useAppConfigStorage('onBoarding');
const desktopApi = useServiceOptional(DesktopApiService);
const openApp = useCallback(() => { const openApp = useCallback(() => {
if (BUILD_CONFIG.isElectron) { if (BUILD_CONFIG.isElectron) {
assertExists(apis); desktopApi?.handler.ui.handleOpenMainApp().catch(err => {
apis.ui.handleOpenMainApp().catch(err => {
console.log('failed to open main app', err); console.log('failed to open main app', err);
}); });
} else { } else {
jumpToIndex(RouteLogic.REPLACE); jumpToIndex(RouteLogic.REPLACE);
setOnboarding(false); setOnboarding(false);
} }
}, [jumpToIndex, setOnboarding]); }, [jumpToIndex, setOnboarding, desktopApi]);
return <Onboarding onOpenApp={openApp} />; return <Onboarding onOpenApp={openApp} />;
}; };
@@ -53,7 +53,7 @@ export const Component = (): ReactElement => {
match && match &&
match.params.docId && match.params.docId &&
match.params.workspaceId && 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 => workbenchRoutes.find(route =>
matchPath(route.path, '/' + match.params.docId) matchPath(route.path, '/' + match.params.docId)
)?.path === '/:pageId' )?.path === '/:pageId'
@@ -2,7 +2,7 @@ import { getBaseFontStyleOptions } from '@affine/core/components/affine/setting-
import { import {
EditorSettingService, EditorSettingService,
type FontFamily, type FontFamily,
} from '@affine/core/modules/editor-settting'; } from '@affine/core/modules/editor-setting';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
@@ -1,9 +1,10 @@
import { type Framework } from '@toeverything/infra'; import { type Framework } from '@toeverything/infra';
import { DesktopApiService } from '../desktop-api';
import { AppTabsHeaderService } from './services/app-tabs-header-service'; import { AppTabsHeaderService } from './services/app-tabs-header-service';
export { AppTabsHeader } from './views/app-tabs-header'; export { AppTabsHeader } from './views/app-tabs-header';
export function configureAppTabsHeaderModule(framework: Framework) { export function configureAppTabsHeaderModule(framework: Framework) {
framework.service(AppTabsHeaderService); framework.service(AppTabsHeaderService, [DesktopApiService]);
} }
@@ -1,24 +1,25 @@
import { apis, events } from '@affine/electron-api';
import { LiveData, Service } from '@toeverything/infra'; import { LiveData, Service } from '@toeverything/infra';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import type { ClientEvents, DesktopApiService } from '../../desktop-api';
export type TabStatus = Parameters< export type TabStatus = Parameters<
Parameters<NonNullable<typeof events>['ui']['onTabsStatusChange']>[0] Parameters<NonNullable<ClientEvents>['ui']['onTabsStatusChange']>[0]
>[0][number]; >[0][number];
export class AppTabsHeaderService extends Service { export class AppTabsHeaderService extends Service {
constructor() { constructor(private readonly desktopApi: DesktopApiService) {
super(); super();
} }
tabsStatus$ = LiveData.from<TabStatus[]>( tabsStatus$ = LiveData.from<TabStatus[]>(
new Observable(subscriber => { new Observable(subscriber => {
let unsub: (() => void) | undefined; let unsub: (() => void) | undefined;
apis?.ui this.desktopApi.handler.ui
.getTabsStatus() .getTabsStatus()
.then(tabs => { .then(tabs => {
subscriber.next(tabs); subscriber.next(tabs);
unsub = events?.ui.onTabsStatusChange(tabs => { unsub = this.desktopApi.events.ui.onTabsStatusChange(tabs => {
subscriber.next(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 ( onAddDocTab = async (
docId: string, docId: string,
targetTabId?: string, targetTabId?: string,
edge?: 'left' | 'right' edge?: 'left' | 'right'
) => { ) => {
await apis?.ui.addTab({ await this.desktopApi.handler.ui.addTab({
view: { view: {
path: { path: {
pathname: '/' + docId, pathname: '/' + docId,
@@ -60,7 +61,7 @@ export class AppTabsHeaderService extends Service {
targetTabId?: string, targetTabId?: string,
edge?: 'left' | 'right' edge?: 'left' | 'right'
) => { ) => {
await apis?.ui.addTab({ await this.desktopApi.handler.ui.addTab({
view: { view: {
path: { path: {
pathname: '/tag/' + tagId, pathname: '/tag/' + tagId,
@@ -76,7 +77,7 @@ export class AppTabsHeaderService extends Service {
targetTabId?: string, targetTabId?: string,
edge?: 'left' | 'right' edge?: 'left' | 'right'
) => { ) => {
await apis?.ui.addTab({ await this.desktopApi.handler.ui.addTab({
view: { view: {
path: { path: {
pathname: '/collection/' + collectionId, 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 { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import { useCatchEventCallback } from '@affine/core/components/hooks/use-catch-event-hook'; import { useCatchEventCallback } from '@affine/core/components/hooks/use-catch-event-hook';
import type { AffineDNDData } from '@affine/core/types/dnd'; import type { AffineDNDData } from '@affine/core/types/dnd';
import { apis, events } from '@affine/electron-api';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { track } from '@affine/track'; import { track } from '@affine/track';
import { CloseIcon, PlusIcon, RightSidebarIcon } from '@blocksuite/icons/rc'; import { CloseIcon, PlusIcon, RightSidebarIcon } from '@blocksuite/icons/rc';
@@ -31,6 +30,7 @@ import {
} from 'react'; } from 'react';
import { AppSidebarService } from '../../app-sidebar'; import { AppSidebarService } from '../../app-sidebar';
import { DesktopApiService } from '../../desktop-api';
import { iconNameToIcon } from '../../workbench/constants'; import { iconNameToIcon } from '../../workbench/constants';
import { DesktopStateSynchronizer } from '../../workbench/services/desktop-state-synchronizer'; import { DesktopStateSynchronizer } from '../../workbench/services/desktop-state-synchronizer';
import { import {
@@ -274,17 +274,18 @@ const WorkbenchTab = ({
}; };
const useIsFullScreen = () => { const useIsFullScreen = () => {
const desktopApi = useServiceOptional(DesktopApiService);
const [fullScreen, setFullScreen] = useState(false); const [fullScreen, setFullScreen] = useState(false);
useEffect(() => { useEffect(() => {
apis?.ui desktopApi?.handler.ui
.isFullScreen() .isFullScreen()
.then(setFullScreen) .then(setFullScreen)
.then(() => { .then(() => {
events?.ui.onFullScreen(setFullScreen); desktopApi?.events.ui.onFullScreen(setFullScreen);
}) })
.catch(console.error); .catch(console.error);
}, []); }, [desktopApi?.events.ui, desktopApi?.handler.ui]);
return fullScreen; return fullScreen;
}; };
@@ -309,6 +310,8 @@ export const AppTabsHeader = ({
const isWindowsDesktop = BUILD_CONFIG.isElectron && environment.isWindows; const isWindowsDesktop = BUILD_CONFIG.isElectron && environment.isWindows;
const fullScreen = useIsFullScreen(); const fullScreen = useIsFullScreen();
const desktopApi = useService(DesktopApiService);
const tabsHeaderService = useService(AppTabsHeaderService); const tabsHeaderService = useService(AppTabsHeaderService);
const tabs = useLiveData(tabsHeaderService.tabsStatus$); const tabs = useLiveData(tabsHeaderService.tabsStatus$);
@@ -328,9 +331,9 @@ export const AppTabsHeader = ({
useEffect(() => { useEffect(() => {
if (mode === 'app') { if (mode === 'app') {
apis?.ui.pingAppLayoutReady().catch(console.error); desktopApi.handler.ui.pingAppLayoutReady().catch(console.error);
} }
}, [mode]); }, [mode, desktopApi]);
const onDrop = useAsyncCallback( const onDrop = useAsyncCallback(
async (data: DropTargetDropEvent<AffineDNDData>, targetId?: string) => { async (data: DropTargetDropEvent<AffineDNDData>, targetId?: string) => {
@@ -1,8 +1,5 @@
import { notify } from '@affine/component';
import { AIProvider } from '@affine/core/blocksuite/presets/ai'; import { AIProvider } from '@affine/core/blocksuite/presets/ai';
import { apis, events } from '@affine/electron-api';
import type { OAuthProviderType } from '@affine/graphql'; import type { OAuthProviderType } from '@affine/graphql';
import { I18n } from '@affine/i18n';
import { track } from '@affine/track'; import { track } from '@affine/track';
import { import {
ApplicationFocused, ApplicationFocused,
@@ -78,33 +75,6 @@ export class AuthService extends Service {
private onApplicationStart() { private onApplicationStart() {
this.session.revalidate(); 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() { 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 { authAtom } from '@affine/core/components/atoms';
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import { DebugLogger } from '@affine/debug'; import { DebugLogger } from '@affine/debug';
import { apis } from '@affine/electron-api';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { track } from '@affine/track'; import { track } from '@affine/track';
@@ -12,6 +11,7 @@ import {
FeatureFlagService, FeatureFlagService,
useLiveData, useLiveData,
useService, useService,
useServiceOptional,
WorkspacesService, WorkspacesService,
} from '@toeverything/infra'; } from '@toeverything/infra';
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
@@ -20,6 +20,7 @@ import { useCallback, useLayoutEffect, useState } from 'react';
import { AuthService } from '../../../modules/cloud'; import { AuthService } from '../../../modules/cloud';
import { _addLocalWorkspace } from '../../../modules/workspace-engine'; import { _addLocalWorkspace } from '../../../modules/workspace-engine';
import { buildShowcaseWorkspace } from '../../../utils/first-app-data'; import { buildShowcaseWorkspace } from '../../../utils/first-app-data';
import { DesktopApiService } from '../../desktop-api';
import { CreateWorkspaceDialogService } from '../services/dialog'; import { CreateWorkspaceDialogService } from '../services/dialog';
import * as styles from './dialog.css'; import * as styles from './dialog.css';
@@ -162,6 +163,7 @@ const CreateWorkspaceDialog = () => {
const t = useI18n(); const t = useI18n();
const workspacesService = useService(WorkspacesService); const workspacesService = useService(WorkspacesService);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const electronApi = useServiceOptional(DesktopApiService);
// TODO(@Peng): maybe refactor using xstate? // TODO(@Peng): maybe refactor using xstate?
useLayoutEffect(() => { useLayoutEffect(() => {
@@ -173,11 +175,11 @@ const CreateWorkspaceDialog = () => {
// after it is done, it will effectively add a new workspace to app-data folder // 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 // so after that, we will be able to load it via importLocalWorkspace
(async () => { (async () => {
if (!apis) { if (!electronApi) {
return; return;
} }
logger.info('load db file'); logger.info('load db file');
const result = await apis.dialog.loadDBFile(); const result = await electronApi.handler.dialog.loadDBFile();
if (result.workspaceId && !canceled) { if (result.workspaceId && !canceled) {
_addLocalWorkspace(result.workspaceId); _addLocalWorkspace(result.workspaceId);
workspacesService.list.revalidate(); workspacesService.list.revalidate();
@@ -201,7 +203,7 @@ const CreateWorkspaceDialog = () => {
return () => { return () => {
canceled = true; canceled = true;
}; };
}, [createWorkspaceDialogService, mode, t, workspacesService]); }, [createWorkspaceDialogService, electronApi, mode, t, workspacesService]);
const onConfirmName = useAsyncCallback( const onConfirmName = useAsyncCallback(
async (name: string, workspaceFlavour: WorkspaceFlavour) => { 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 { DebugLogger } from '@affine/debug';
import { apis } from '@affine/electron-api';
import { Entity, LiveData } from '@toeverything/infra'; import { Entity, LiveData } from '@toeverything/infra';
import { import {
debounceTime, debounceTime,
@@ -10,6 +9,8 @@ import {
tap, tap,
} from 'rxjs'; } from 'rxjs';
import type { DesktopApiService } from '../../desktop-api';
const logger = new DebugLogger('affine:find-in-page'); const logger = new DebugLogger('affine:find-in-page');
export class FindInPage extends Entity { export class FindInPage extends Entity {
@@ -38,10 +39,10 @@ export class FindInPage extends Entity {
let findNext = true; let findNext = true;
return this.direction$.pipe( return this.direction$.pipe(
switchMap(direction => { switchMap(direction => {
if (apis?.findInPage) { if (this.electronApi?.handler?.findInPage) {
this.isSearching$.next(true); this.isSearching$.next(true);
const currentId = ++searchId; const currentId = ++searchId;
return apis?.findInPage return this.electronApi.handler.findInPage
.find(searchText, { .find(searchText, {
forward: direction === 'forward', forward: direction === 'forward',
findNext, findNext,
@@ -69,7 +70,7 @@ export class FindInPage extends Entity {
null null
); );
constructor() { constructor(private readonly electronApi: DesktopApiService) {
super(); super();
// TODO(@Peng): hide on navigation // TODO(@Peng): hide on navigation
} }
@@ -112,6 +113,6 @@ export class FindInPage extends Entity {
clear() { clear() {
logger.debug('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 type { Framework } from '@toeverything/infra';
import { DesktopApiService } from '../desktop-api';
import { FindInPage } from './entities/find-in-page'; import { FindInPage } from './entities/find-in-page';
import { FindInPageService } from './services/find-in-page'; import { FindInPageService } from './services/find-in-page';
export function configureFindInPageModule(framework: Framework) { export function configureFindInPageModule(framework: Framework) {
framework.service(FindInPageService).entity(FindInPage); framework.service(FindInPageService).entity(FindInPage, [DesktopApiService]);
} }
+1 -3
View File
@@ -10,10 +10,9 @@ import { configureDocInfoModule } from './doc-info';
import { configureDocLinksModule } from './doc-link'; import { configureDocLinksModule } from './doc-link';
import { configureDocsSearchModule } from './docs-search'; import { configureDocsSearchModule } from './docs-search';
import { configureEditorModule } from './editor'; import { configureEditorModule } from './editor';
import { configureEditorSettingModule } from './editor-settting'; import { configureEditorSettingModule } from './editor-setting';
import { configureExplorerModule } from './explorer'; import { configureExplorerModule } from './explorer';
import { configureFavoriteModule } from './favorite'; import { configureFavoriteModule } from './favorite';
import { configureFindInPageModule } from './find-in-page';
import { configureI18nModule } from './i18n'; import { configureI18nModule } from './i18n';
import { configureImportTemplateModule } from './import-template'; import { configureImportTemplateModule } from './import-template';
import { configureJournalModule } from './journal'; import { configureJournalModule } from './journal';
@@ -43,7 +42,6 @@ export function configureCommonModules(framework: Framework) {
configureShareDocsModule(framework); configureShareDocsModule(framework);
configureShareSettingModule(framework); configureShareSettingModule(framework);
configureTelemetryModule(framework); configureTelemetryModule(framework);
configureFindInPageModule(framework);
configurePeekViewModule(framework); configurePeekViewModule(framework);
configureDocDisplayMetaModule(framework); configureDocDisplayMetaModule(framework);
configureQuickSearchModule(framework); configureQuickSearchModule(framework);
@@ -13,7 +13,7 @@ import {
useState, useState,
} from 'react'; } from 'react';
import { EditorSettingService } from '../../editor-settting'; import { EditorSettingService } from '../../editor-setting';
import type { PeekViewAnimation, PeekViewMode } from '../entities/peek-view'; import type { PeekViewAnimation, PeekViewMode } from '../entities/peek-view';
import * as styles from './modal-container.css'; 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 type { DocProps, DocsService } from '@toeverything/infra';
import { Service } from '@toeverything/infra'; import { Service } from '@toeverything/infra';
import { EditorSettingService } from '../../editor-settting'; import { EditorSettingService } from '../../editor-setting';
import type { WorkbenchService } from '../../workbench'; import type { WorkbenchService } from '../../workbench';
import { CollectionsQuickSearchSession } from '../impls/collections'; import { CollectionsQuickSearchSession } from '../impls/collections';
import { CommandsQuickSearchSession } from '../impls/commands'; import { CommandsQuickSearchSession } from '../impls/commands';
@@ -1,58 +1,66 @@
import { sharedStorage } from '@affine/electron-api';
import type { GlobalCache, GlobalState } from '@toeverything/infra'; import type { GlobalCache, GlobalState } from '@toeverything/infra';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion import type { DesktopApiService } from '../../desktop-api';
const ensureSharedStorage = sharedStorage!;
export class ElectronGlobalState implements GlobalState { export class ElectronGlobalState implements GlobalState {
constructor(private readonly electronApi: DesktopApiService) {}
keys(): string[] { keys(): string[] {
return ensureSharedStorage.globalState.keys(); return this.electronApi.sharedStorage.globalState.keys();
} }
get<T>(key: string): T | undefined { get<T>(key: string): T | undefined {
return ensureSharedStorage.globalState.get(key); return this.electronApi.sharedStorage.globalState.get(key);
} }
watch<T>(key: string) { watch<T>(key: string) {
return new Observable<T | undefined>(subscriber => { return new Observable<T | undefined>(subscriber => {
const unsubscribe = ensureSharedStorage.globalState.watch<T>(key, i => { const unsubscribe = this.electronApi.sharedStorage.globalState.watch<T>(
subscriber.next(i); key,
}); i => {
subscriber.next(i);
}
);
return () => unsubscribe(); return () => unsubscribe();
}); });
} }
set<T>(key: string, value: T): void { set<T>(key: string, value: T): void {
ensureSharedStorage.globalState.set(key, value); this.electronApi.sharedStorage.globalState.set(key, value);
} }
del(key: string): void { del(key: string): void {
ensureSharedStorage.globalState.del(key); this.electronApi.sharedStorage.globalState.del(key);
} }
clear(): void { clear(): void {
ensureSharedStorage.globalState.clear(); this.electronApi.sharedStorage.globalState.clear();
} }
} }
export class ElectronGlobalCache implements GlobalCache { export class ElectronGlobalCache implements GlobalCache {
constructor(private readonly electronApi: DesktopApiService) {}
keys(): string[] { keys(): string[] {
return ensureSharedStorage.globalCache.keys(); return this.electronApi.sharedStorage.globalCache.keys();
} }
get<T>(key: string): T | undefined { get<T>(key: string): T | undefined {
return ensureSharedStorage.globalCache.get(key); return this.electronApi.sharedStorage.globalCache.get(key);
} }
watch<T>(key: string) { watch<T>(key: string) {
return new Observable<T | undefined>(subscriber => { return new Observable<T | undefined>(subscriber => {
const unsubscribe = ensureSharedStorage.globalCache.watch<T>(key, i => { const unsubscribe = this.electronApi.sharedStorage.globalCache.watch<T>(
subscriber.next(i); key,
}); i => {
subscriber.next(i);
}
);
return () => unsubscribe(); return () => unsubscribe();
}); });
} }
set<T>(key: string, value: T): void { set<T>(key: string, value: T): void {
ensureSharedStorage.globalCache.set(key, value); this.electronApi.sharedStorage.globalCache.set(key, value);
} }
del(key: string): void { del(key: string): void {
ensureSharedStorage.globalCache.del(key); this.electronApi.sharedStorage.globalCache.del(key);
} }
clear(): void { clear(): void {
ensureSharedStorage.globalCache.clear(); this.electronApi.sharedStorage.globalCache.clear();
} }
} }
@@ -1,5 +1,6 @@
import { type Framework, GlobalCache, GlobalState } from '@toeverything/infra'; import { type Framework, GlobalCache, GlobalState } from '@toeverything/infra';
import { DesktopApiService } from '../desktop-api';
import { ElectronGlobalCache, ElectronGlobalState } from './impls/electron'; import { ElectronGlobalCache, ElectronGlobalState } from './impls/electron';
import { import {
LocalStorageGlobalCache, LocalStorageGlobalCache,
@@ -12,6 +13,6 @@ export function configureLocalStorageStateStorageImpls(framework: Framework) {
} }
export function configureElectronStateStorageImpls(framework: Framework) { export function configureElectronStateStorageImpls(framework: Framework) {
framework.impl(GlobalCache, ElectronGlobalCache); framework.impl(GlobalCache, ElectronGlobalCache, [DesktopApiService]);
framework.impl(GlobalState, ElectronGlobalState); framework.impl(GlobalState, ElectronGlobalState, [DesktopApiService]);
} }
@@ -1,4 +1,3 @@
import { apis } from '@affine/electron-api';
import type { import type {
ByteKV, ByteKV,
ByteKVBehavior, ByteKVBehavior,
@@ -8,6 +7,8 @@ import type {
} from '@toeverything/infra'; } from '@toeverything/infra';
import { AsyncLock } from '@toeverything/infra'; import { AsyncLock } from '@toeverything/infra';
import type { DesktopApiService } from '../../desktop-api';
class BroadcastChannelDocEventBus implements DocEventBus { class BroadcastChannelDocEventBus implements DocEventBus {
senderChannel = new BroadcastChannel('user-db:' + this.userId); senderChannel = new BroadcastChannel('user-db:' + this.userId);
constructor(private readonly userId: string) {} constructor(private readonly userId: string) {}
@@ -29,22 +30,26 @@ class BroadcastChannelDocEventBus implements DocEventBus {
} }
export class SqliteUserspaceDocStorage implements DocStorage { export class SqliteUserspaceDocStorage implements DocStorage {
constructor(private readonly userId: string) {} constructor(
private readonly userId: string,
private readonly electronApi: DesktopApiService
) {}
eventBus = new BroadcastChannelDocEventBus(this.userId); eventBus = new BroadcastChannelDocEventBus(this.userId);
readonly doc = new Doc(this.userId); readonly doc = new Doc(this.userId, this.electronApi);
readonly syncMetadata = new SyncMetadataKV(this.userId); readonly syncMetadata = new SyncMetadataKV(this.userId, this.electronApi);
readonly serverClock = new ServerClockKV(this.userId); readonly serverClock = new ServerClockKV(this.userId, this.electronApi);
} }
type DocType = DocStorage['doc']; type DocType = DocStorage['doc'];
class Doc implements DocType { class Doc implements DocType {
lock = new AsyncLock(); lock = new AsyncLock();
constructor(private readonly userId: string) { apis = this.electronApi.api.handler;
if (!apis?.db) {
throw new Error('sqlite datasource is not available'); constructor(
} private readonly userId: string,
} private readonly electronApi: DesktopApiService
) {}
async transaction<T>( async transaction<T>(
cb: (transaction: ByteKVBehavior) => Promise<T> cb: (transaction: ByteKVBehavior) => Promise<T>
@@ -58,10 +63,7 @@ class Doc implements DocType {
} }
async get(docId: string) { async get(docId: string) {
if (!apis?.db) { const update = await this.apis.db.getDocAsUpdates(
throw new Error('sqlite datasource is not available');
}
const update = await apis.db.getDocAsUpdates(
'userspace', 'userspace',
this.userId, this.userId,
docId docId
@@ -82,10 +84,7 @@ class Doc implements DocType {
} }
async set(docId: string, data: Uint8Array) { async set(docId: string, data: Uint8Array) {
if (!apis?.db) { await this.apis.db.applyDocUpdate('userspace', this.userId, data, docId);
throw new Error('sqlite datasource is not available');
}
await apis.db.applyDocUpdate('userspace', this.userId, data, docId);
} }
clear(): void | Promise<void> { clear(): void | Promise<void> {
@@ -93,93 +92,68 @@ class Doc implements DocType {
} }
async del(docId: string) { async del(docId: string) {
if (!apis?.db) { await this.apis.db.deleteDoc('userspace', this.userId, docId);
throw new Error('sqlite datasource is not available');
}
await apis.db.deleteDoc('userspace', this.userId, docId);
} }
} }
class SyncMetadataKV implements ByteKV { 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> { transaction<T>(cb: (behavior: ByteKVBehavior) => Promise<T>): Promise<T> {
return cb(this); return cb(this);
} }
get(key: string): Uint8Array | null | Promise<Uint8Array | null> { get(key: string): Uint8Array | null | Promise<Uint8Array | null> {
if (!apis?.db) { return this.apis.db.getSyncMetadata('userspace', this.userId, key);
throw new Error('sqlite datasource is not available');
}
return apis.db.getSyncMetadata('userspace', this.userId, key);
} }
set(key: string, data: Uint8Array): void | Promise<void> { set(key: string, data: Uint8Array): void | Promise<void> {
if (!apis?.db) { return this.apis.db.setSyncMetadata('userspace', this.userId, key, data);
throw new Error('sqlite datasource is not available');
}
return apis.db.setSyncMetadata('userspace', this.userId, key, data);
} }
keys(): string[] | Promise<string[]> { keys(): string[] | Promise<string[]> {
if (!apis?.db) { return this.apis.db.getSyncMetadataKeys('userspace', this.userId);
throw new Error('sqlite datasource is not available');
}
return apis.db.getSyncMetadataKeys('userspace', this.userId);
} }
del(key: string): void | Promise<void> { del(key: string): void | Promise<void> {
if (!apis?.db) { return this.apis.db.delSyncMetadata('userspace', this.userId, key);
throw new Error('sqlite datasource is not available');
}
return apis.db.delSyncMetadata('userspace', this.userId, key);
} }
clear(): void | Promise<void> { clear(): void | Promise<void> {
if (!apis?.db) { return this.apis.db.clearSyncMetadata('userspace', this.userId);
throw new Error('sqlite datasource is not available');
}
return apis.db.clearSyncMetadata('userspace', this.userId);
} }
} }
class ServerClockKV implements ByteKV { 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> { transaction<T>(cb: (behavior: ByteKVBehavior) => Promise<T>): Promise<T> {
return cb(this); return cb(this);
} }
get(key: string): Uint8Array | null | Promise<Uint8Array | null> { get(key: string): Uint8Array | null | Promise<Uint8Array | null> {
if (!apis?.db) { return this.apis.db.getServerClock('userspace', this.userId, key);
throw new Error('sqlite datasource is not available');
}
return apis.db.getServerClock('userspace', this.userId, key);
} }
set(key: string, data: Uint8Array): void | Promise<void> { set(key: string, data: Uint8Array): void | Promise<void> {
if (!apis?.db) { return this.apis.db.setServerClock('userspace', this.userId, key, data);
throw new Error('sqlite datasource is not available');
}
return apis.db.setServerClock('userspace', this.userId, key, data);
} }
keys(): string[] | Promise<string[]> { keys(): string[] | Promise<string[]> {
if (!apis?.db) { return this.apis.db.getServerClockKeys('userspace', this.userId);
throw new Error('sqlite datasource is not available');
}
return apis.db.getServerClockKeys('userspace', this.userId);
} }
del(key: string): void | Promise<void> { del(key: string): void | Promise<void> {
if (!apis?.db) { return this.apis.db.delServerClock('userspace', this.userId, key);
throw new Error('sqlite datasource is not available');
}
return apis.db.delServerClock('userspace', this.userId, key);
} }
clear(): void | Promise<void> { clear(): void | Promise<void> {
if (!apis?.db) { return this.apis.db.clearServerClock('userspace', this.userId);
throw new Error('sqlite datasource is not available');
}
return apis.db.clearServerClock('userspace', this.userId);
} }
} }
@@ -3,6 +3,7 @@ export { UserspaceService as UserDBService } from './services/userspace';
import type { Framework } from '@toeverything/infra'; import type { Framework } from '@toeverything/infra';
import { AuthService, WebSocketService } from '../cloud'; import { AuthService, WebSocketService } from '../cloud';
import { DesktopApiService } from '../desktop-api';
import { CurrentUserDB } from './entities/current-user-db'; import { CurrentUserDB } from './entities/current-user-db';
import { UserDB } from './entities/user-db'; import { UserDB } from './entities/user-db';
import { UserDBEngine } from './entities/user-db-engine'; import { UserDBEngine } from './entities/user-db-engine';
@@ -32,9 +33,9 @@ export function configureIndexedDBUserspaceStorageProvider(
} }
export function configureSqliteUserspaceStorageProvider(framework: Framework) { export function configureSqliteUserspaceStorageProvider(framework: Framework) {
framework.impl(UserspaceStorageProvider, { framework.impl(UserspaceStorageProvider, p => ({
getDocStorage(userId: string) { 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; show?: boolean;
} = {} } = {}
) { ) {
this.newTabHandler({ this.newTabHandler.handle({
basename: this.basename$.value, basename: this.basename$.value,
to, to,
show: show ?? true, show: show ?? true,
@@ -16,6 +16,7 @@ import {
WorkspaceScope, WorkspaceScope,
} from '@toeverything/infra'; } from '@toeverything/infra';
import { DesktopApiService } from '../desktop-api';
import { SidebarTab } from './entities/sidebar-tab'; import { SidebarTab } from './entities/sidebar-tab';
import { View } from './entities/view'; import { View } from './entities/view';
import { Workbench } from './entities/workbench'; import { Workbench } from './entities/workbench';
@@ -59,7 +60,10 @@ export function configureDesktopWorkbenchModule(services: Framework) {
.scope(WorkspaceScope) .scope(WorkspaceScope)
.impl(WorkbenchDefaultState, DesktopWorkbenchDefaultState, [ .impl(WorkbenchDefaultState, DesktopWorkbenchDefaultState, [
GlobalStateService, GlobalStateService,
DesktopApiService,
]) ])
.impl(WorkbenchNewTabHandler, () => DesktopWorkbenchNewTabHandler) .impl(WorkbenchNewTabHandler, DesktopWorkbenchNewTabHandler, [
.service(DesktopStateSynchronizer, [WorkbenchService]); DesktopApiService,
])
.service(DesktopStateSynchronizer, [WorkbenchService, DesktopApiService]);
} }
@@ -1,13 +1,16 @@
import { apis, appInfo, events } from '@affine/electron-api';
import { LiveData, Service } from '@toeverything/infra'; import { LiveData, Service } from '@toeverything/infra';
import type { DesktopApiService } from '../../desktop-api';
import type { WorkbenchService } from '../../workbench'; import type { WorkbenchService } from '../../workbench';
/** /**
* Synchronize workbench state with state stored in main process * Synchronize workbench state with state stored in main process
*/ */
export class DesktopStateSynchronizer extends Service { export class DesktopStateSynchronizer extends Service {
constructor(private readonly workbenchService: WorkbenchService) { constructor(
private readonly workbenchService: WorkbenchService,
private readonly electronApi: DesktopApiService
) {
super(); super();
this.startSync(); this.startSync();
} }
@@ -18,8 +21,9 @@ export class DesktopStateSynchronizer extends Service {
} }
const workbench = this.workbenchService.workbench; const workbench = this.workbenchService.workbench;
const appInfo = this.electronApi.appInfo;
events?.ui.onTabAction(event => { this.electronApi.events.ui.onTabAction(event => {
if ( if (
event.type === 'open-in-split-view' && event.type === 'open-in-split-view' &&
event.payload.tabId === appInfo?.viewId 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) { if (tabId === appInfo?.viewId) {
workbench.sidebarOpen$.next(!workbench.sidebarOpen$.value); workbench.sidebarOpen$.next(!workbench.sidebarOpen$.value);
} }
@@ -74,11 +78,11 @@ export class DesktopStateSynchronizer extends Service {
}; };
}); });
}).subscribe(views => { }).subscribe(views => {
if (!apis || !appInfo?.viewId) { if (!appInfo?.viewId) {
return; return;
} }
apis.ui this.electronApi.handler.ui
.updateWorkbenchMeta(appInfo.viewId, { .updateWorkbenchMeta(appInfo.viewId, {
views, views,
}) })
@@ -86,11 +90,11 @@ export class DesktopStateSynchronizer extends Service {
}); });
workbench.activeViewIndex$.subscribe(activeViewIndex => { workbench.activeViewIndex$.subscribe(activeViewIndex => {
if (!apis || !appInfo?.viewId) { if (!appInfo?.viewId) {
return; return;
} }
apis.ui this.electronApi.handler.ui
.updateWorkbenchMeta(appInfo.viewId, { .updateWorkbenchMeta(appInfo.viewId, {
activeViewIndex: activeViewIndex, activeViewIndex: activeViewIndex,
}) })
@@ -98,11 +102,11 @@ export class DesktopStateSynchronizer extends Service {
}); });
workbench.basename$.subscribe(basename => { workbench.basename$.subscribe(basename => {
if (!apis || !appInfo?.viewId) { if (!appInfo?.viewId) {
return; return;
} }
apis.ui this.electronApi.handler.ui
.updateWorkbenchMeta(appInfo.viewId, { .updateWorkbenchMeta(appInfo.viewId, {
basename: basename, basename: basename,
}) })
@@ -1,37 +1,40 @@
import { apis } from '@affine/electron-api'; import { createIdentifier, Service } from '@toeverything/infra';
import { createIdentifier } from '@toeverything/infra';
import { parsePath, type To } from 'history'; import { parsePath, type To } from 'history';
export type WorkbenchNewTabHandler = (option: { import type { DesktopApiService } from '../../desktop-api';
basename: string;
to: To; export type WorkbenchNewTabHandler = {
show: boolean; handle: (option: { basename: string; to: To; show: boolean }) => void;
}) => void; };
export const WorkbenchNewTabHandler = createIdentifier<WorkbenchNewTabHandler>( export const WorkbenchNewTabHandler = createIdentifier<WorkbenchNewTabHandler>(
'WorkbenchNewTabHandler' 'WorkbenchNewTabHandler'
); );
export const BrowserWorkbenchNewTabHandler: WorkbenchNewTabHandler = ({ export const BrowserWorkbenchNewTabHandler: WorkbenchNewTabHandler = {
basename, handle: ({ basename, to }) => {
to, const link =
}) => { basename +
const link = (typeof to === 'string' ? to : `${to.pathname}${to.search}${to.hash}`);
basename + window.open(link, '_blank');
(typeof to === 'string' ? to : `${to.pathname}${to.search}${to.hash}`); },
window.open(link, '_blank');
}; };
export const DesktopWorkbenchNewTabHandler: WorkbenchNewTabHandler = ({ export class DesktopWorkbenchNewTabHandler
basename, extends Service
to, implements WorkbenchNewTabHandler
}) => { {
const path = typeof to === 'string' ? parsePath(to) : to; constructor(private readonly electronApi: DesktopApiService) {
apis?.ui super();
.addTab({ }
basename, handle({ basename, to }: { basename: string; to: To }) {
view: { path }, const path = typeof to === 'string' ? parsePath(to) : to;
show: false, this.electronApi.api.handler.ui
}) .addTab({
.catch(console.error); 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 type { GlobalStateService } from '@toeverything/infra';
import { createIdentifier, Service } from '@toeverything/infra'; import { createIdentifier, Service } from '@toeverything/infra';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import type { DesktopApiService, TabViewsMetaSchema } from '../../desktop-api';
import type { ViewIconName } from '../constants'; import type { ViewIconName } from '../constants';
export type WorkbenchDefaultState = { export type WorkbenchDefaultState = {
@@ -34,7 +34,10 @@ export class DesktopWorkbenchDefaultState
extends Service extends Service
implements WorkbenchDefaultState implements WorkbenchDefaultState
{ {
constructor(private readonly globalStateService: GlobalStateService) { constructor(
private readonly globalStateService: GlobalStateService,
private readonly electronApi: DesktopApiService
) {
super(); super();
} }
@@ -45,8 +48,9 @@ export class DesktopWorkbenchDefaultState
); );
return ( return (
tabViewsMeta?.workbenches.find(w => w.id === appInfo?.viewId) || tabViewsMeta?.workbenches.find(
InMemoryWorkbenchDefaultState w => w.id === this.electronApi.appInfo.viewId
) || InMemoryWorkbenchDefaultState
); );
} }
@@ -1,24 +1,28 @@
import { apis } from '@affine/electron-api'; import type { DesktopApiService } from '@affine/core/modules/desktop-api';
import { assertExists } from '@blocksuite/affine/global/utils';
import type { BlobStorage } from '@toeverything/infra'; import type { BlobStorage } from '@toeverything/infra';
import { bufferToBlob } from '../../utils/buffer-to-blob'; import { bufferToBlob } from '../../utils/buffer-to-blob';
export class SqliteBlobStorage implements BlobStorage { export class SqliteBlobStorage implements BlobStorage {
constructor(private readonly workspaceId: string) {} constructor(
private readonly workspaceId: string,
private readonly electronApi: DesktopApiService
) {}
name = 'sqlite'; name = 'sqlite';
readonly = false; readonly = false;
async get(key: string) { async get(key: string) {
assertExists(apis); const buffer = await this.electronApi.handler.db.getBlob(
const buffer = await apis.db.getBlob('workspace', this.workspaceId, key); 'workspace',
this.workspaceId,
key
);
if (buffer) { if (buffer) {
return bufferToBlob(buffer); return bufferToBlob(buffer);
} }
return null; return null;
} }
async set(key: string, value: Blob) { async set(key: string, value: Blob) {
assertExists(apis); await this.electronApi.handler.db.addBlob(
await apis.db.addBlob(
'workspace', 'workspace',
this.workspaceId, this.workspaceId,
key, key,
@@ -27,11 +31,16 @@ export class SqliteBlobStorage implements BlobStorage {
return key; return key;
} }
delete(key: string) { delete(key: string) {
assertExists(apis); return this.electronApi.handler.db.deleteBlob(
return apis.db.deleteBlob('workspace', this.workspaceId, key); 'workspace',
this.workspaceId,
key
);
} }
list() { list() {
assertExists(apis); return this.electronApi.handler.db.getBlobKeys(
return apis.db.getBlobKeys('workspace', this.workspaceId); '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 type { ByteKV, ByteKVBehavior, DocStorage } from '@toeverything/infra';
import { AsyncLock } from '@toeverything/infra'; import { AsyncLock } from '@toeverything/infra';
import { BroadcastChannelDocEventBus } from './doc-broadcast-channel'; import { BroadcastChannelDocEventBus } from './doc-broadcast-channel';
export class SqliteDocStorage implements DocStorage { export class SqliteDocStorage implements DocStorage {
constructor(private readonly workspaceId: string) {} constructor(
private readonly workspaceId: string,
private readonly electronApi: DesktopApiService
) {}
eventBus = new BroadcastChannelDocEventBus(this.workspaceId); eventBus = new BroadcastChannelDocEventBus(this.workspaceId);
readonly doc = new Doc(this.workspaceId); readonly doc = new Doc(this.workspaceId, this.electronApi);
readonly syncMetadata = new SyncMetadataKV(this.workspaceId); readonly syncMetadata = new SyncMetadataKV(
readonly serverClock = new ServerClockKV(this.workspaceId); this.workspaceId,
this.electronApi
);
readonly serverClock = new ServerClockKV(this.workspaceId, this.electronApi);
} }
type DocType = DocStorage['doc']; type DocType = DocStorage['doc'];
class Doc implements DocType { class Doc implements DocType {
lock = new AsyncLock(); lock = new AsyncLock();
constructor(private readonly workspaceId: string) { apis = this.electronApi.handler;
if (!apis?.db) { constructor(
throw new Error('sqlite datasource is not available'); private readonly workspaceId: string,
} private readonly electronApi: DesktopApiService
} ) {}
async transaction<T>( async transaction<T>(
cb: (transaction: ByteKVBehavior) => Promise<T> cb: (transaction: ByteKVBehavior) => Promise<T>
@@ -34,10 +40,7 @@ class Doc implements DocType {
} }
async get(docId: string) { async get(docId: string) {
if (!apis?.db) { const update = await this.apis.db.getDocAsUpdates(
throw new Error('sqlite datasource is not available');
}
const update = await apis.db.getDocAsUpdates(
'workspace', 'workspace',
this.workspaceId, this.workspaceId,
docId docId
@@ -58,10 +61,12 @@ class Doc implements DocType {
} }
async set(docId: string, data: Uint8Array) { async set(docId: string, data: Uint8Array) {
if (!apis?.db) { await this.apis.db.applyDocUpdate(
throw new Error('sqlite datasource is not available'); 'workspace',
} this.workspaceId,
await apis.db.applyDocUpdate('workspace', this.workspaceId, data, docId); data,
docId
);
} }
clear(): void | Promise<void> { clear(): void | Promise<void> {
@@ -69,93 +74,78 @@ class Doc implements DocType {
} }
async del(docId: string) { async del(docId: string) {
if (!apis?.db) { await this.apis.db.deleteDoc('workspace', this.workspaceId, docId);
throw new Error('sqlite datasource is not available');
}
await apis.db.deleteDoc('workspace', this.workspaceId, docId);
} }
} }
class SyncMetadataKV implements ByteKV { 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> { transaction<T>(cb: (behavior: ByteKVBehavior) => Promise<T>): Promise<T> {
return cb(this); return cb(this);
} }
get(key: string): Uint8Array | null | Promise<Uint8Array | null> { get(key: string): Uint8Array | null | Promise<Uint8Array | null> {
if (!apis?.db) { return this.apis.db.getSyncMetadata('workspace', this.workspaceId, key);
throw new Error('sqlite datasource is not available');
}
return apis.db.getSyncMetadata('workspace', this.workspaceId, key);
} }
set(key: string, data: Uint8Array): void | Promise<void> { set(key: string, data: Uint8Array): void | Promise<void> {
if (!apis?.db) { return this.apis.db.setSyncMetadata(
throw new Error('sqlite datasource is not available'); 'workspace',
} this.workspaceId,
return apis.db.setSyncMetadata('workspace', this.workspaceId, key, data); key,
data
);
} }
keys(): string[] | Promise<string[]> { keys(): string[] | Promise<string[]> {
if (!apis?.db) { return this.apis.db.getSyncMetadataKeys('workspace', this.workspaceId);
throw new Error('sqlite datasource is not available');
}
return apis.db.getSyncMetadataKeys('workspace', this.workspaceId);
} }
del(key: string): void | Promise<void> { del(key: string): void | Promise<void> {
if (!apis?.db) { return this.apis.db.delSyncMetadata('workspace', this.workspaceId, key);
throw new Error('sqlite datasource is not available');
}
return apis.db.delSyncMetadata('workspace', this.workspaceId, key);
} }
clear(): void | Promise<void> { clear(): void | Promise<void> {
if (!apis?.db) { return this.apis.db.clearSyncMetadata('workspace', this.workspaceId);
throw new Error('sqlite datasource is not available');
}
return apis.db.clearSyncMetadata('workspace', this.workspaceId);
} }
} }
class ServerClockKV implements ByteKV { 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> { transaction<T>(cb: (behavior: ByteKVBehavior) => Promise<T>): Promise<T> {
return cb(this); return cb(this);
} }
get(key: string): Uint8Array | null | Promise<Uint8Array | null> { get(key: string): Uint8Array | null | Promise<Uint8Array | null> {
if (!apis?.db) { return this.apis.db.getServerClock('workspace', this.workspaceId, key);
throw new Error('sqlite datasource is not available');
}
return apis.db.getServerClock('workspace', this.workspaceId, key);
} }
set(key: string, data: Uint8Array): void | Promise<void> { set(key: string, data: Uint8Array): void | Promise<void> {
if (!apis?.db) { return this.apis.db.setServerClock(
throw new Error('sqlite datasource is not available'); 'workspace',
} this.workspaceId,
return apis.db.setServerClock('workspace', this.workspaceId, key, data); key,
data
);
} }
keys(): string[] | Promise<string[]> { keys(): string[] | Promise<string[]> {
if (!apis?.db) { return this.apis.db.getServerClockKeys('workspace', this.workspaceId);
throw new Error('sqlite datasource is not available');
}
return apis.db.getServerClockKeys('workspace', this.workspaceId);
} }
del(key: string): void | Promise<void> { del(key: string): void | Promise<void> {
if (!apis?.db) { return this.apis.db.delServerClock('workspace', this.workspaceId, key);
throw new Error('sqlite datasource is not available');
}
return apis.db.delServerClock('workspace', this.workspaceId, key);
} }
clear(): void | Promise<void> { clear(): void | Promise<void> {
if (!apis?.db) { return this.apis.db.clearServerClock('workspace', this.workspaceId);
throw new Error('sqlite datasource is not available');
}
return apis.db.clearServerClock('workspace', this.workspaceId);
} }
} }
@@ -1,5 +1,4 @@
import { DebugLogger } from '@affine/debug'; import { DebugLogger } from '@affine/debug';
import { apis } from '@affine/electron-api';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { DocCollection } from '@blocksuite/affine/store'; import { DocCollection } from '@blocksuite/affine/store';
import type { import type {
@@ -20,6 +19,7 @@ import { nanoid } from 'nanoid';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { applyUpdate, encodeStateAsUpdate } from 'yjs'; import { applyUpdate, encodeStateAsUpdate } from 'yjs';
import { DesktopApiService } from '../../desktop-api';
import type { WorkspaceEngineStorageProvider } from '../providers/engine'; import type { WorkspaceEngineStorageProvider } from '../providers/engine';
import { BroadcastChannelAwarenessConnection } from './engine/awareness-broadcast-channel'; import { BroadcastChannelAwarenessConnection } from './engine/awareness-broadcast-channel';
import { StaticBlobStorage } from './engine/blob-static'; import { StaticBlobStorage } from './engine/blob-static';
@@ -72,8 +72,10 @@ export class LocalWorkspaceFlavourProvider
async deleteWorkspace(id: string): Promise<void> { async deleteWorkspace(id: string): Promise<void> {
setLocalWorkspaceIds(ids => ids.filter(x => x !== id)); setLocalWorkspaceIds(ids => ids.filter(x => x !== id));
if (BUILD_CONFIG.isElectron && apis) { const electronApi = this.framework.getOptional(DesktopApiService);
await apis.workspace.delete(id);
if (BUILD_CONFIG.isElectron && electronApi) {
await electronApi.handler.workspace.delete(id);
} }
// notify all browser tabs, so they can update their workspace list // notify all browser tabs, so they can update their workspace list
@@ -10,6 +10,7 @@ import {
WorkspaceFlavourProvider, WorkspaceFlavourProvider,
} from '@toeverything/infra'; } from '@toeverything/infra';
import { DesktopApiService } from '../desktop-api';
import { CloudWorkspaceFlavourProviderService } from './impls/cloud'; import { CloudWorkspaceFlavourProviderService } from './impls/cloud';
import { IndexedDBBlobStorage } from './impls/engine/blob-indexeddb'; import { IndexedDBBlobStorage } from './impls/engine/blob-indexeddb';
import { SqliteBlobStorage } from './impls/engine/blob-sqlite'; import { SqliteBlobStorage } from './impls/engine/blob-sqlite';
@@ -57,13 +58,16 @@ export function configureIndexedDBWorkspaceEngineStorageProvider(
export function configureSqliteWorkspaceEngineStorageProvider( export function configureSqliteWorkspaceEngineStorageProvider(
framework: Framework framework: Framework
) { ) {
framework.impl(WorkspaceEngineStorageProvider, { framework.impl(WorkspaceEngineStorageProvider, p => {
getDocStorage(workspaceId: string) { const electronApi = p.get(DesktopApiService);
return new SqliteDocStorage(workspaceId); return {
}, getDocStorage(workspaceId: string) {
getBlobStorage(workspaceId: string) { return new SqliteDocStorage(workspaceId, electronApi);
return new SqliteBlobStorage(workspaceId); },
}, getBlobStorage(workspaceId: string) {
return new SqliteBlobStorage(workspaceId, electronApi);
},
};
}); });
} }
+9 -7
View File
@@ -7,13 +7,13 @@ import type {
handlers as mainHandlers, handlers as mainHandlers,
} from '@affine/electron/main/exposed'; } from '@affine/electron/main/exposed';
import type { appInfo as exposedAppInfo } from '@affine/electron/preload/electron-api'; 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 MainHandlers = typeof mainHandlers;
type HelperHandlers = typeof helperHandlers; type HelperHandlers = typeof helperHandlers;
type HelperEvents = typeof helperEvents; type HelperEvents = typeof helperEvents;
type MainEvents = typeof mainEvents; type MainEvents = typeof mainEvents;
type ClientHandler = { export type ClientHandler = {
[namespace in keyof MainHandlers]: { [namespace in keyof MainHandlers]: {
[method in keyof MainHandlers[namespace]]: MainHandlers[namespace][method] extends ( [method in keyof MainHandlers[namespace]]: MainHandlers[namespace][method] extends (
arg0: any, arg0: any,
@@ -27,17 +27,19 @@ type ClientHandler = {
: never; : never;
}; };
} & HelperHandlers; } & HelperHandlers;
type ClientEvents = MainEvents & HelperEvents; export type ClientEvents = MainEvents & HelperEvents;
export const appInfo = (globalThis as any).__appInfo as export const appInfo = (globalThis as any).__appInfo as
| typeof exposedAppInfo | typeof exposedAppInfo
| null; | null;
export const apis = (globalThis as any).__apis as ClientHandler | null; export const apis = (globalThis as any).__apis as ClientHandler | undefined;
export const events = (globalThis as any).__events as ClientEvents | null; export const events = (globalThis as any).__events as ClientEvents | undefined;
export const sharedStorage = (globalThis as any).__sharedStorage as export const sharedStorage = (globalThis as any).__sharedStorage as
| typeof exposedSharedStorage | SharedStorage
| null; | undefined;
export type { SharedStorage };
export type { UpdateMeta } from '@affine/electron/main/updater/event'; export type { UpdateMeta } from '@affine/electron/main/updater/event';
export { export {
+1
View File
@@ -529,6 +529,7 @@ __metadata:
link-preview-js: "npm:^3.0.5" link-preview-js: "npm:^3.0.5"
lodash-es: "npm:^4.17.21" lodash-es: "npm:^4.17.21"
nanoid: "npm:^5.0.7" nanoid: "npm:^5.0.7"
next-themes: "npm:^0.3.0"
react: "npm:^18.2.0" react: "npm:^18.2.0"
react-dom: "npm:^18.2.0" react-dom: "npm:^18.2.0"
react-router-dom: "npm:^6.22.3" react-router-dom: "npm:^6.22.3"