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