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:
pengx17
2024-07-29 11:05:22 +00:00
parent 622715d2f3
commit 1efc1d0f5b
88 changed files with 3160 additions and 945 deletions
@@ -98,7 +98,7 @@ export const tableHeaderTimestamp = style({
export const tableHeaderDivider = style({
height: 0,
borderTop: `1px solid ${cssVar('borderColor')}`,
borderTop: `0.5px solid ${cssVar('borderColor')}`,
width: '100%',
margin: '8px 0',
});
@@ -1,5 +1,5 @@
import { cssVar } from '@toeverything/theme';
import { globalStyle, style } from '@vanilla-extract/css';
import { style } from '@vanilla-extract/css';
export const floatingMaxWidth = 768;
export const navWrapperStyle = style({
paddingBottom: '8px',
@@ -11,7 +11,7 @@ export const navWrapperStyle = style({
},
selectors: {
'&[data-has-border=true]': {
borderRight: `1px solid ${cssVar('borderColor')}`,
borderRight: `0.5px solid ${cssVar('borderColor')}`,
},
'&[data-is-floating="true"]': {
backgroundColor: cssVar('backgroundPrimaryColor'),
@@ -45,14 +45,6 @@ export const navHeaderStyle = style({
['WebkitAppRegion' as string]: 'drag',
});
globalStyle(
`html[data-fullscreen="false"]
${navHeaderStyle}[data-is-macos-electron="true"]`,
{
paddingLeft: '90px',
}
);
export const navBodyStyle = style({
flex: '1 1 auto',
height: 'calc(100% - 52px)',
@@ -2,7 +2,7 @@ import { atom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
export const APP_SIDEBAR_OPEN = 'app-sidebar-open';
export const isMobile = window.innerWidth < 768;
export const isMobile = window.innerWidth < 768 && !environment.isDesktop;
export const appSidebarOpenAtom = atomWithStorage(APP_SIDEBAR_OPEN, !isMobile);
export const appSidebarFloatingAtom = atom(isMobile);
@@ -41,7 +41,6 @@ const MIN_WIDTH = 248;
export function AppSidebar({
children,
clientBorder,
translucentUI,
}: AppSidebarProps): ReactElement {
const [open, setOpen] = useAtom(appSidebarOpenAtom);
const [width, setWidth] = useAtom(appSidebarWidthAtom);
@@ -49,6 +48,11 @@ export function AppSidebar({
const [resizing, setResizing] = useAtom(appSidebarResizingAtom);
useEffect(() => {
// do not float app sidebar on desktop
if (environment.isDesktop) {
return;
}
function onResize() {
const isFloatingMaxWidth = window.matchMedia(
`(max-width: ${floatingMaxWidth}px)`
@@ -75,10 +79,8 @@ export function AppSidebar({
};
}, [open, setFloating, setOpen, width]);
const hasRightBorder = !environment.isDesktop && !clientBorder;
const isMacosDesktop = environment.isDesktop && environment.isMacOs;
const hasRightBorder =
!environment.isDesktop || (!clientBorder && !translucentUI);
return (
<>
<ResizePanel
@@ -96,12 +98,13 @@ export function AppSidebar({
resizeHandleOffset={clientBorder ? 8 : 0}
resizeHandleVerticalPadding={clientBorder ? 16 : 0}
data-transparent
data-open={open}
data-has-border={hasRightBorder}
data-testid="app-sidebar-wrapper"
data-is-macos-electron={isMacosDesktop}
>
<nav className={navStyle} data-testid="app-sidebar">
<SidebarHeader />
{!environment.isDesktop && <SidebarHeader />}
<div className={navBodyStyle} data-testid="sliderBar-inner">
{children}
</div>
@@ -1,8 +1,7 @@
import { useHasScrollTop } from '@affine/component';
import * as ScrollArea from '@radix-ui/react-scroll-area';
import clsx from 'clsx';
import type { PropsWithChildren } from 'react';
import { useRef } from 'react';
import { type PropsWithChildren } from 'react';
import * as styles from './index.css';
@@ -11,8 +10,7 @@ export function SidebarContainer({ children }: PropsWithChildren) {
}
export function SidebarScrollableContainer({ children }: PropsWithChildren) {
const ref = useRef<HTMLDivElement>(null);
const hasScrollTop = useHasScrollTop(ref);
const [setContainer, hasScrollTop] = useHasScrollTop();
return (
<ScrollArea.Root className={styles.scrollableContainerRoot}>
<div
@@ -21,7 +19,7 @@ export function SidebarScrollableContainer({ children }: PropsWithChildren) {
/>
<ScrollArea.Viewport
className={clsx([styles.scrollableViewport])}
ref={ref}
ref={setContainer}
>
<div className={clsx([styles.scrollableContainer])}>{children}</div>
</ScrollArea.Viewport>
@@ -1,6 +1,5 @@
import { useAtomValue } from 'jotai';
import { NavigationButtons } from '../../../modules/navigation';
import { navHeaderStyle } from '../index.css';
import { appSidebarOpenAtom } from '../index.jotai';
import { SidebarSwitch } from './sidebar-switch';
@@ -9,13 +8,8 @@ export const SidebarHeader = () => {
const open = useAtomValue(appSidebarOpenAtom);
return (
<div
className={navHeaderStyle}
data-open={open}
data-is-macos-electron={environment.isDesktop && environment.isMacOs}
>
<div className={navHeaderStyle} data-open={open}>
<SidebarSwitch show={open} />
<NavigationButtons />
</div>
);
};
@@ -7,7 +7,6 @@ import {
useCallback,
useEffect,
useImperativeHandle,
useRef,
} from 'react';
import { usePageHeaderColsDef } from './header-col-def';
@@ -156,8 +155,7 @@ export const ListScrollContainer = forwardRef<
HTMLDivElement,
PropsWithChildren<ListScrollContainerProps>
>(({ className, children, style }, ref) => {
const containerRef = useRef<HTMLDivElement | null>(null);
const hasScrollTop = useHasScrollTop(containerRef);
const [setContainer, hasScrollTop] = useHasScrollTop();
const setNodeRef = useCallback(
(r: HTMLDivElement) => {
@@ -168,9 +166,9 @@ export const ListScrollContainer = forwardRef<
ref.current = r;
}
}
containerRef.current = r;
return setContainer(r);
},
[ref]
[ref, setContainer]
);
return (
@@ -19,7 +19,7 @@ export const tableHeader = style({
transform: 'translateY(-0.5px)', // fix sticky look through issue
});
globalStyle(`[data-has-scroll-top=true] ${tableHeader}`, {
boxShadow: `0 1px ${cssVar('borderColor')}`,
boxShadow: `0 0.5px ${cssVar('borderColor')}`,
});
export const headerTitleSelectionIconWrapper = style({
display: 'flex',
@@ -56,6 +56,7 @@ export const headerSideContainer = style({
export const windowAppControlsWrapper = style({
display: 'flex',
flexShrink: 0,
});
export const windowAppControl = style({
@@ -1,4 +1,6 @@
import { openSettingModalAtom } from '@affine/core/atoms';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
import {
ExplorerCollections,
ExplorerFavorites,
@@ -7,17 +9,19 @@ import {
ExplorerOrganize,
} from '@affine/core/modules/explorer';
import { ExplorerTags } from '@affine/core/modules/explorer/views/sections/tags';
import { CMDKQuickSearchService } from '@affine/core/modules/quicksearch/services/cmdk';
import { TelemetryWorkspaceContextService } from '@affine/core/modules/telemetry/services/telemetry';
import { pathGenerator } from '@affine/core/shared';
import { mixpanel } from '@affine/core/utils';
import { apis, events } from '@affine/electron-api';
import { useI18n } from '@affine/i18n';
import { FolderIcon, SettingsIcon } from '@blocksuite/icons/rc';
import type { Doc } from '@blocksuite/store';
import type { Workspace } from '@toeverything/infra';
import { useLiveData, useService } from '@toeverything/infra';
import { useAtomValue } from 'jotai';
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
import { useAtomValue, useSetAtom } from 'jotai';
import type { ReactElement } from 'react';
import { memo, useEffect } from 'react';
import { useCallback, useEffect } from 'react';
import { useAppSettingHelper } from '../../hooks/affine/use-app-setting-helper';
import { WorkbenchService } from '../../modules/workbench';
@@ -33,6 +37,7 @@ import {
SidebarContainer,
SidebarScrollableContainer,
} from '../app-sidebar';
import { usePageHelper } from '../blocksuite/block-suite-page-list/utils';
import { WorkspaceSelector } from '../workspace-selector';
import ImportPage from './import-page';
import {
@@ -64,127 +69,147 @@ export type RootAppSidebarProps = {
* This is for the whole affine app sidebar.
* This component wraps the app sidebar in `@affine/component` with logic and data.
*
* @todo(himself65): rewrite all styled component into @vanilla-extract/css
*/
export const RootAppSidebar = memo(
({
currentWorkspace,
openPage,
createPage,
paths,
onOpenQuickSearchModal,
onOpenSettingModal,
}: RootAppSidebarProps): ReactElement => {
const currentWorkspaceId = currentWorkspace.id;
const { appSettings } = useAppSettingHelper();
const docCollection = currentWorkspace.docCollection;
const t = useI18n();
const currentPath = useLiveData(
useService(WorkbenchService).workbench.location$.map(
location => location.pathname
)
);
export const RootAppSidebar = (): ReactElement => {
const currentWorkspace = useService(WorkspaceService).workspace;
const currentWorkspaceId = currentWorkspace.id;
const { openPage } = useNavigateHelper();
const { appSettings } = useAppSettingHelper();
const docCollection = currentWorkspace.docCollection;
const t = useI18n();
const currentPath = useLiveData(
useService(WorkbenchService).workbench.location$.map(
location => location.pathname
)
);
const cmdkQuickSearchService = useService(CMDKQuickSearchService);
const onOpenQuickSearchModal = useCallback(() => {
cmdkQuickSearchService.toggle();
mixpanel.track('QuickSearchOpened', {
segment: 'navigation panel',
control: 'search button',
});
}, [cmdkQuickSearchService]);
const telemetry = useService(TelemetryWorkspaceContextService);
const telemetry = useService(TelemetryWorkspaceContextService);
const allPageActive = currentPath === '/all';
const allPageActive = currentPath === '/all';
const onClickNewPage = useAsyncCallback(async () => {
const page = createPage();
page.load();
openPage(page.id);
mixpanel.track('DocCreated', {
page: telemetry.getPageContext(),
segment: 'navigation panel',
module: 'bottom button',
control: 'new doc button',
category: 'page',
type: 'doc',
const pageHelper = usePageHelper(currentWorkspace.docCollection);
const createPage = useCallback(() => {
return pageHelper.createPage();
}, [pageHelper]);
const onClickNewPage = useAsyncCallback(async () => {
const page = createPage();
page.load();
openPage(currentWorkspaceId, page.id);
mixpanel.track('DocCreated', {
page: telemetry.getPageContext(),
segment: 'navigation panel',
module: 'bottom button',
control: 'new doc button',
category: 'page',
type: 'doc',
});
}, [createPage, currentWorkspaceId, openPage, telemetry]);
// Listen to the "New Page" action from the menu
useEffect(() => {
if (environment.isDesktop) {
return events?.applicationMenu.onNewPageAction(onClickNewPage);
}
return;
}, [onClickNewPage]);
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
const onOpenSettingModal = useCallback(() => {
setOpenSettingModalAtom({
activeTab: 'appearance',
open: true,
});
mixpanel.track('SettingsViewed', {
// page:
segment: 'navigation panel',
module: 'general list',
control: 'settings button',
});
}, [setOpenSettingModalAtom]);
const sidebarOpen = useAtomValue(appSidebarOpenAtom);
useEffect(() => {
if (environment.isDesktop) {
apis?.ui.handleSidebarVisibilityChange(sidebarOpen).catch(err => {
console.error(err);
});
}, [createPage, openPage, telemetry]);
}
}, [sidebarOpen]);
// Listen to the "New Page" action from the menu
useEffect(() => {
if (environment.isDesktop) {
return events?.applicationMenu.onNewPageAction(onClickNewPage);
}
return;
}, [onClickNewPage]);
const sidebarOpen = useAtomValue(appSidebarOpenAtom);
useEffect(() => {
if (environment.isDesktop) {
apis?.ui.handleSidebarVisibilityChange(sidebarOpen).catch(err => {
console.error(err);
});
}
}, [sidebarOpen]);
return (
<AppSidebar
clientBorder={appSettings.clientBorder}
translucentUI={appSettings.enableBlurBackground}
>
<SidebarContainer>
<div className={workspaceAndUserWrapper}>
<div className={workspaceWrapper}>
<WorkspaceSelector />
</div>
<UserInfo />
return (
<AppSidebar
clientBorder={appSettings.clientBorder}
translucentUI={appSettings.enableBlurBackground}
>
<SidebarContainer>
<div className={workspaceAndUserWrapper}>
<div className={workspaceWrapper}>
<WorkspaceSelector />
</div>
<div className={quickSearchAndNewPage}>
<QuickSearchInput
className={quickSearch}
data-testid="slider-bar-quick-search-button"
onClick={onOpenQuickSearchModal}
/>
<AddPageButton onClick={onClickNewPage} />
</div>
<MenuLinkItem
icon={<FolderIcon />}
active={allPageActive}
to={paths.all(currentWorkspaceId)}
>
<span data-testid="all-pages">
{t['com.affine.workspaceSubPath.all']()}
</span>
</MenuLinkItem>
<AppSidebarJournalButton
docCollection={currentWorkspace.docCollection}
<UserInfo />
</div>
<div className={quickSearchAndNewPage}>
<QuickSearchInput
className={quickSearch}
data-testid="slider-bar-quick-search-button"
onClick={onOpenQuickSearchModal}
/>
<MenuItem
data-testid="slider-bar-workspace-setting-button"
icon={<SettingsIcon />}
onClick={onOpenSettingModal}
>
<span data-testid="settings-modal-trigger">
{t['com.affine.settingSidebar.title']()}
</span>
</MenuItem>
</SidebarContainer>
<SidebarScrollableContainer>
{runtimeConfig.enableNewFavorite && <ExplorerFavorites />}
{runtimeConfig.enableOrganize && <ExplorerOrganize />}
{runtimeConfig.enableNewFavorite && <ExplorerMigrationFavorites />}
{runtimeConfig.enableOldFavorite && (
<ExplorerOldFavorites defaultCollapsed />
)}
<ExplorerCollections defaultCollapsed />
<ExplorerTags defaultCollapsed />
<CategoryDivider label={t['com.affine.rootAppSidebar.others']()} />
{/* fixme: remove the following spacer */}
<div style={{ height: '4px' }} />
<div style={{ padding: '0 8px' }}>
<TrashButton />
<ImportPage docCollection={docCollection} />
</div>
</SidebarScrollableContainer>
<SidebarContainer>
{environment.isDesktop ? <UpdaterButton /> : <AppDownloadButton />}
</SidebarContainer>
</AppSidebar>
);
}
);
<AddPageButton onClick={onClickNewPage} />
</div>
<MenuLinkItem
icon={<FolderIcon />}
active={allPageActive}
to={pathGenerator.all(currentWorkspaceId)}
>
<span data-testid="all-pages">
{t['com.affine.workspaceSubPath.all']()}
</span>
</MenuLinkItem>
<AppSidebarJournalButton
docCollection={currentWorkspace.docCollection}
/>
<MenuItem
data-testid="slider-bar-workspace-setting-button"
icon={<SettingsIcon />}
onClick={onOpenSettingModal}
>
<span data-testid="settings-modal-trigger">
{t['com.affine.settingSidebar.title']()}
</span>
</MenuItem>
</SidebarContainer>
<SidebarScrollableContainer>
{runtimeConfig.enableNewFavorite && <ExplorerFavorites />}
{runtimeConfig.enableOrganize && <ExplorerOrganize />}
{runtimeConfig.enableNewFavorite && <ExplorerMigrationFavorites />}
{runtimeConfig.enableOldFavorite && (
<ExplorerOldFavorites defaultCollapsed />
)}
<ExplorerCollections defaultCollapsed />
<ExplorerTags defaultCollapsed />
<CategoryDivider label={t['com.affine.rootAppSidebar.others']()} />
{/* fixme: remove the following spacer */}
<div style={{ height: '4px' }} />
<div style={{ padding: '0 8px' }}>
<TrashButton />
<ImportPage docCollection={docCollection} />
</div>
</SidebarScrollableContainer>
<SidebarContainer>
{environment.isDesktop ? <UpdaterButton /> : <AppDownloadButton />}
</SidebarContainer>
</AppSidebar>
);
};
RootAppSidebar.displayName = 'memo(RootAppSidebar)';
@@ -4,9 +4,8 @@ export const appStyle = style({
width: '100%',
position: 'relative',
height: '100vh',
display: 'flex',
flexGrow: '1',
flexDirection: 'row',
display: 'flex',
backgroundColor: cssVar('backgroundPrimaryColor'),
selectors: {
'&[data-is-resizing="true"]': {
@@ -42,11 +41,11 @@ globalStyle(`html[data-theme="dark"] ${appStyle}`, {
},
},
});
export const mainContainerStyle = style({
position: 'relative',
zIndex: 0,
// it will create stacking context to limit layer of child elements and be lower than after auto zIndex
width: 0,
width: '100%',
display: 'flex',
flex: 1,
overflow: 'clip',
@@ -71,14 +70,16 @@ export const mainContainerStyle = style({
'&[data-client-border="true"][data-side-bar-open="true"]': {
marginLeft: 0,
},
'&[data-client-border="true"]:before': {
content: '""',
position: 'absolute',
height: '8px',
width: '100%',
top: '-8px',
left: 0,
['WebkitAppRegion' as string]: 'drag',
'&[data-client-border="true"][data-is-desktop="true"]': {
marginTop: 0,
},
'&[data-client-border="false"][data-is-desktop="true"][data-side-bar-open="true"]':
{
borderTopLeftRadius: 6,
},
'&[data-client-border="false"][data-is-desktop="true"]': {
borderTop: `0.5px solid ${cssVar('borderColor')}`,
borderLeft: `0.5px solid ${cssVar('borderColor')}`,
},
'&[data-transparent=true]': {
backgroundColor: 'transparent',
@@ -1,3 +1,4 @@
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
import {
DocsService,
GlobalContextService,
@@ -26,45 +27,44 @@ export const AppContainer = ({
...rest
}: WorkspaceRootProps) => {
const noisyBackground = useNoisyBackground && environment.isDesktop;
const blurBackground = environment.isDesktop && useBlurBackground;
return (
<div
{...rest}
className={clsx(appStyle, {
'noisy-background': noisyBackground,
'blur-background': environment.isDesktop && useBlurBackground,
'blur-background': blurBackground,
})}
data-noise-background={noisyBackground}
data-is-resizing={resizing}
data-blur-background={blurBackground}
>
{children}
</div>
);
};
export interface MainContainerProps extends HTMLAttributes<HTMLDivElement> {
clientBorder?: boolean;
}
export interface MainContainerProps extends HTMLAttributes<HTMLDivElement> {}
export const MainContainer = forwardRef<
HTMLDivElement,
PropsWithChildren<MainContainerProps>
>(function MainContainer(
{ className, children, clientBorder, ...props },
ref
): ReactElement {
>(function MainContainer({ className, children, ...props }, ref): ReactElement {
const appSideBarOpen = useAtomValue(appSidebarOpenAtom);
const { appSettings } = useAppSettingHelper();
return (
<div
{...props}
className={clsx(mainContainerStyle, className)}
data-is-macos={environment.isDesktop && environment.isMacOs}
data-is-desktop={environment.isDesktop}
data-transparent={false}
data-client-border={clientBorder}
data-client-border={appSettings.clientBorder}
data-side-bar-open={appSideBarOpen}
data-testid="main-container"
ref={ref}
>
{children}
<div className={mainContainerStyle}>{children}</div>
</div>
);
});