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