mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-18 23:07:02 +08:00
feat(electron): multi tabs support (#7440)
use https://www.electronjs.org/docs/latest/api/web-contents-view to serve different tab views added tabs view manager in electron to handle multi-view actions and events. fix AF-1111 fix AF-999 fix PD-1459 fix AF-964 PD-1458
This commit is contained in:
@@ -4,9 +4,12 @@ import '@affine/component/theme/theme.css';
|
||||
import { NotificationCenter } from '@affine/component';
|
||||
import { AffineContext } from '@affine/component/context';
|
||||
import { GlobalLoading } from '@affine/component/global-loading';
|
||||
import { registerAffineCommand } from '@affine/core/commands';
|
||||
import { AppFallback } from '@affine/core/components/affine/app-container';
|
||||
import { configureCommonModules } from '@affine/core/modules';
|
||||
import { configureAppTabsHeaderModule } from '@affine/core/modules/app-tabs-header';
|
||||
import { configureElectronStateStorageImpls } from '@affine/core/modules/storage';
|
||||
import { configureDesktopWorkbenchModule } from '@affine/core/modules/workbench';
|
||||
import {
|
||||
configureBrowserWorkspaceFlavours,
|
||||
configureSqliteWorkspaceEngineStorageProvider,
|
||||
@@ -19,6 +22,7 @@ import {
|
||||
import { Telemetry } from '@affine/core/telemetry';
|
||||
import createEmotionCache from '@affine/core/utils/create-emotion-cache';
|
||||
import { createI18n, setUpLanguage } from '@affine/i18n';
|
||||
import { SettingsIcon } from '@blocksuite/icons/rc';
|
||||
import { CacheProvider } from '@emotion/react';
|
||||
import {
|
||||
Framework,
|
||||
@@ -85,6 +89,8 @@ configureCommonModules(framework);
|
||||
configureElectronStateStorageImpls(framework);
|
||||
configureBrowserWorkspaceFlavours(framework);
|
||||
configureSqliteWorkspaceEngineStorageProvider(framework);
|
||||
configureDesktopWorkbenchModule(framework);
|
||||
configureAppTabsHeaderModule(framework);
|
||||
const frameworkProvider = framework.provider();
|
||||
|
||||
// setup application lifecycle events, and emit application start event
|
||||
@@ -121,3 +127,14 @@ export function App() {
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
registerAffineCommand({
|
||||
id: 'affine:reload',
|
||||
category: 'affine:general',
|
||||
label: 'Reload current tab',
|
||||
icon: <SettingsIcon />,
|
||||
keyBinding: '$mod+R',
|
||||
run() {
|
||||
location.reload();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import '@affine/core/bootstrap/preload';
|
||||
|
||||
import { appConfigProxy } from '@affine/core/hooks/use-app-config-storage';
|
||||
import { performanceLogger } from '@affine/core/shared';
|
||||
import { apis, events } from '@affine/electron-api';
|
||||
import { apis, appInfo, events } from '@affine/electron-api';
|
||||
import {
|
||||
init,
|
||||
reactRouterV6BrowserTracingIntegration,
|
||||
@@ -74,6 +74,23 @@ function main() {
|
||||
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);
|
||||
|
||||
56
packages/frontend/electron/renderer/shell/index.tsx
Normal file
56
packages/frontend/electron/renderer/shell/index.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import 'setimmediate';
|
||||
import '@affine/component/theme/global.css';
|
||||
import '@affine/component/theme/theme.css';
|
||||
|
||||
import { ThemeProvider } from '@affine/component/theme-provider';
|
||||
import { appConfigProxy } from '@affine/core/hooks/use-app-config-storage';
|
||||
import { configureAppTabsHeaderModule } from '@affine/core/modules/app-tabs-header';
|
||||
import { configureElectronStateStorageImpls } from '@affine/core/modules/storage';
|
||||
import { performanceLogger } from '@affine/core/shared';
|
||||
import {
|
||||
configureGlobalStorageModule,
|
||||
Framework,
|
||||
FrameworkRoot,
|
||||
} from '@toeverything/infra';
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import { ShellRoot } from './shell';
|
||||
|
||||
const framework = new Framework();
|
||||
configureGlobalStorageModule(framework);
|
||||
configureElectronStateStorageImpls(framework);
|
||||
configureAppTabsHeaderModule(framework);
|
||||
const frameworkProvider = framework.provider();
|
||||
|
||||
const logger = performanceLogger.namespace('shell');
|
||||
|
||||
function main() {
|
||||
appConfigProxy
|
||||
.getSync()
|
||||
.catch(() => console.error('failed to load app config'));
|
||||
}
|
||||
|
||||
function mountApp() {
|
||||
const root = document.getElementById('app');
|
||||
if (!root) {
|
||||
throw new Error('Root element not found');
|
||||
}
|
||||
logger.info('render app');
|
||||
createRoot(root).render(
|
||||
<StrictMode>
|
||||
<FrameworkRoot framework={frameworkProvider}>
|
||||
<ThemeProvider>
|
||||
<ShellRoot />
|
||||
</ThemeProvider>
|
||||
</FrameworkRoot>
|
||||
</StrictMode>
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
mountApp();
|
||||
} catch (err) {
|
||||
console.error('Failed to bootstrap app', err);
|
||||
}
|
||||
22
packages/frontend/electron/renderer/shell/shell.css.ts
Normal file
22
packages/frontend/electron/renderer/shell/shell.css.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const root = style({
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
opacity: 1,
|
||||
transition: 'opacity 0.1s',
|
||||
background: cssVar('backgroundPrimaryColor'),
|
||||
selectors: {
|
||||
'&[data-active="false"]': {
|
||||
opacity: 0,
|
||||
},
|
||||
'&[data-translucent="true"]': {
|
||||
background: 'transparent',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
globalStyle(`${root}[data-active="false"] *`, {
|
||||
['WebkitAppRegion' as string]: 'no-drag !important',
|
||||
});
|
||||
86
packages/frontend/electron/renderer/shell/shell.tsx
Normal file
86
packages/frontend/electron/renderer/shell/shell.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
|
||||
import { AppTabsHeader } from '@affine/core/modules/app-tabs-header';
|
||||
import { apis, events } from '@affine/electron-api';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import * as styles from './shell.css';
|
||||
|
||||
const useIsShellActive = () => {
|
||||
const [active, setActive] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const unsub = events?.ui.onTabShellViewActiveChange(active => {
|
||||
setActive(active);
|
||||
});
|
||||
return () => {
|
||||
unsub?.();
|
||||
};
|
||||
});
|
||||
|
||||
return active;
|
||||
};
|
||||
|
||||
const useTabsBoundingRect = () => {
|
||||
const [rect, setRect] = useState<{
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}>({
|
||||
x: environment.isDesktop && environment.isMacOs ? 80 : 0,
|
||||
y: 0,
|
||||
width: window.innerWidth,
|
||||
height: 52,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
let unsub: (() => void) | undefined;
|
||||
apis?.ui
|
||||
.getTabsBoundingRect()
|
||||
.then(rect => {
|
||||
if (rect) {
|
||||
setRect(rect);
|
||||
}
|
||||
unsub = events?.ui.onTabsBoundingRectChanged(rect => {
|
||||
if (rect) {
|
||||
setRect(rect);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
return () => {
|
||||
unsub?.();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return rect;
|
||||
};
|
||||
|
||||
export function ShellRoot() {
|
||||
const active = useIsShellActive();
|
||||
const { appSettings } = useAppSettingHelper();
|
||||
const rect = useTabsBoundingRect();
|
||||
const translucent =
|
||||
environment.isDesktop &&
|
||||
environment.isMacOs &&
|
||||
appSettings.enableBlurBackground;
|
||||
return (
|
||||
<div
|
||||
className={styles.root}
|
||||
data-translucent={translucent}
|
||||
data-active={active}
|
||||
>
|
||||
<AppTabsHeader
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: rect.y,
|
||||
left: rect.x,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user