From 1efc1d0f5b9614db46ce85d67ed912c3fd5fe62b Mon Sep 17 00:00:00 2001 From: pengx17 Date: Mon, 29 Jul 2024 11:05:22 +0000 Subject: [PATCH] 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 --- .../common/infra/src/app-config-storage.ts | 9 +- packages/common/infra/src/atom/settings.ts | 1 + .../common/infra/src/framework/core/index.ts | 2 +- .../auth-components/onboarding-page.tsx | 26 +- .../frontend/component/src/theme/global.css | 14 + .../src/ui/notification/notification-card.tsx | 5 +- .../component/src/ui/scrollbar/scrollbar.tsx | 6 +- .../src/ui/scrollbar/use-has-scroll-top.tsx | 48 +- .../ai/chat-panel/chat-panel-messages.ts | 2 +- .../affine/page-properties/styles.css.ts | 2 +- .../src/components/app-sidebar/index.css.ts | 12 +- .../src/components/app-sidebar/index.jotai.ts | 2 +- .../core/src/components/app-sidebar/index.tsx | 13 +- .../app-sidebar/sidebar-containers/index.tsx | 8 +- .../app-sidebar/sidebar-header/index.tsx | 8 +- .../core/src/components/page-list/list.tsx | 8 +- .../components/page-list/page-header.css.ts | 2 +- .../src/components/pure/header/style.css.tsx | 1 + .../src/components/root-app-sidebar/index.tsx | 259 +++-- .../src/components/workspace/index.css.ts | 25 +- .../core/src/components/workspace/index.tsx | 22 +- .../core/src/hooks/use-navigate-helper.ts | 3 +- .../frontend/core/src/layouts/styles.css.ts | 45 + .../core/src/layouts/workspace-layout.tsx | 169 +-- .../core/src/modules/app-tabs-header/index.ts | 9 + .../services/app-tabs-header-service.ts | 53 + .../app-tabs-header/views/app-tabs-header.tsx | 230 ++++ .../app-tabs-header/views/styles.css.ts | 178 ++++ packages/frontend/core/src/modules/index.ts | 2 - .../core/src/modules/navigation/utils.ts | 13 +- .../src/modules/workbench/entities/view.ts | 9 +- .../modules/workbench/entities/workbench.ts | 27 +- .../core/src/modules/workbench/index.ts | 39 +- .../services/desktop-state-synchronizer.ts | 227 ++++ .../services/workbench-view-state.ts | 60 ++ .../modules/workbench/view/desktop-adapter.ts | 2 - .../workbench/view/route-container.css.ts | 8 - .../workbench/view/route-container.tsx | 23 +- .../view/sidebar/sidebar-container.css.ts | 5 +- .../view/sidebar/sidebar-container.tsx | 12 +- .../view/sidebar/sidebar-header.css.ts | 11 - .../workbench/view/sidebar/sidebar-header.tsx | 27 +- .../modules/workbench/view/workbench-link.tsx | 24 +- .../workbench/view/workbench-root.css.ts | 2 +- .../modules/workbench/view/workbench-root.tsx | 4 +- .../impls/engine/doc-sqlite.ts | 6 +- packages/frontend/core/src/pages/404.tsx | 9 + packages/frontend/core/src/pages/auth.tsx | 9 +- packages/frontend/core/src/pages/index.tsx | 9 + .../workspace/detail-page/detail-page.css.ts | 11 +- .../workspace/detail-page/detail-page.tsx | 12 +- .../detail-page/use-header-responsive.ts | 5 +- packages/frontend/electron-api/src/index.ts | 6 + packages/frontend/electron/renderer/app.tsx | 17 + packages/frontend/electron/renderer/index.tsx | 19 +- .../electron/renderer/shell/index.tsx | 56 + .../electron/renderer/shell/shell.css.ts | 22 + .../electron/renderer/shell/shell.tsx | 86 ++ .../src/main/application-menu/create.ts | 67 +- .../frontend/electron/src/main/constants.ts | 2 + .../frontend/electron/src/main/deep-link.ts | 2 +- packages/frontend/electron/src/main/events.ts | 31 +- .../frontend/electron/src/main/handlers.ts | 2 +- .../frontend/electron/src/main/main-window.ts | 318 ------ .../frontend/electron/src/main/protocol.ts | 2 +- .../src/main/shared-storage/json-file.ts | 2 +- .../frontend/electron/src/main/ui/events.ts | 20 + .../frontend/electron/src/main/ui/handlers.ts | 75 +- .../frontend/electron/src/main/ui/subject.ts | 1 + .../src/main/windows-manager/index.ts | 6 + .../src/main/windows-manager/launcher.ts | 7 +- .../src/main/windows-manager/main-window.ts | 247 +++++ .../main/{ => windows-manager}/onboarding.ts | 12 +- .../windows-manager/tab-views-meta-schema.ts | 45 + .../src/main/windows-manager/tab-views.ts | 999 ++++++++++++++++++ .../electron/src/preload/electron-api.ts | 11 +- packages/frontend/web/src/app.tsx | 2 + .../web/src/polyfill/set-immediate.ts | 1 + tests/affine-desktop-cloud/package.json | 2 +- tests/affine-desktop/e2e/basic.spec.ts | 90 +- tests/affine-desktop/package.json | 2 +- tests/affine-desktop/playwright.config.ts | 1 + tests/kit/electron.ts | 179 ++-- tools/cli/src/bin/build.ts | 4 +- tools/cli/src/bin/dev.ts | 5 +- tools/cli/src/config/index.ts | 2 +- tools/cli/src/webpack/config.ts | 3 + tools/cli/src/webpack/webpack.config.ts | 41 +- 88 files changed, 3160 insertions(+), 945 deletions(-) create mode 100644 packages/frontend/core/src/layouts/styles.css.ts create mode 100644 packages/frontend/core/src/modules/app-tabs-header/index.ts create mode 100644 packages/frontend/core/src/modules/app-tabs-header/services/app-tabs-header-service.ts create mode 100644 packages/frontend/core/src/modules/app-tabs-header/views/app-tabs-header.tsx create mode 100644 packages/frontend/core/src/modules/app-tabs-header/views/styles.css.ts create mode 100644 packages/frontend/core/src/modules/workbench/services/desktop-state-synchronizer.ts create mode 100644 packages/frontend/core/src/modules/workbench/services/workbench-view-state.ts create mode 100644 packages/frontend/electron/renderer/shell/index.tsx create mode 100644 packages/frontend/electron/renderer/shell/shell.css.ts create mode 100644 packages/frontend/electron/renderer/shell/shell.tsx delete mode 100644 packages/frontend/electron/src/main/main-window.ts create mode 100644 packages/frontend/electron/src/main/windows-manager/index.ts create mode 100644 packages/frontend/electron/src/main/windows-manager/main-window.ts rename packages/frontend/electron/src/main/{ => windows-manager}/onboarding.ts (91%) create mode 100644 packages/frontend/electron/src/main/windows-manager/tab-views-meta-schema.ts create mode 100644 packages/frontend/electron/src/main/windows-manager/tab-views.ts create mode 100644 packages/frontend/web/src/polyfill/set-immediate.ts diff --git a/packages/common/infra/src/app-config-storage.ts b/packages/common/infra/src/app-config-storage.ts index 36b5698e59..5db0127d2d 100644 --- a/packages/common/infra/src/app-config-storage.ts +++ b/packages/common/infra/src/app-config-storage.ts @@ -1,11 +1,12 @@ import { z } from 'zod'; -const _appConfigSchema = z.object({ +export const appConfigSchema = z.object({ /** whether to show onboarding first */ onBoarding: z.boolean().optional().default(true), }); -export type AppConfigSchema = z.infer; -export const defaultAppConfig = _appConfigSchema.parse({}); + +export type AppConfigSchema = z.infer; +export const defaultAppConfig = appConfigSchema.parse({}); const _storage: Record = {}; let _inMemoryId = 0; @@ -48,7 +49,7 @@ class Storage { } get(): T; - get(key: keyof T): T[keyof T]; + get(key: K): T[K]; /** * get config, if key is provided, return the value of the key * @param key diff --git a/packages/common/infra/src/atom/settings.ts b/packages/common/infra/src/atom/settings.ts index e0ce75301c..6778e82152 100644 --- a/packages/common/infra/src/atom/settings.ts +++ b/packages/common/infra/src/atom/settings.ts @@ -105,6 +105,7 @@ export function setupEditorFlags(docCollection: DocCollection) { type SetStateAction = Value | ((prev: Value) => Value); +// todo(@pengx17): use global state instead const appSettingEffect = atomEffect(get => { const settings = get(appSettingBaseAtom); // some values in settings should be synced into electron side diff --git a/packages/common/infra/src/framework/core/index.ts b/packages/common/infra/src/framework/core/index.ts index c1cce58426..b0edde95c1 100644 --- a/packages/common/infra/src/framework/core/index.ts +++ b/packages/common/infra/src/framework/core/index.ts @@ -8,4 +8,4 @@ export { Framework } from './framework'; export { createIdentifier } from './identifier'; export type { ResolveOptions } from './provider'; export { FrameworkProvider } from './provider'; -export type { GeneralIdentifier } from './types'; +export type { GeneralIdentifier, Identifier } from './types'; diff --git a/packages/frontend/component/src/components/auth-components/onboarding-page.tsx b/packages/frontend/component/src/components/auth-components/onboarding-page.tsx index d0166240c3..1fa11ec63f 100644 --- a/packages/frontend/component/src/components/auth-components/onboarding-page.tsx +++ b/packages/frontend/component/src/components/auth-components/onboarding-page.tsx @@ -100,11 +100,9 @@ export const ScrollableLayout = ({ export const OnboardingPage = ({ user, onOpenAffine, - windowControl, }: { user: User; onOpenAffine: () => void; - windowControl?: React.ReactNode; }) => { const location = useLocation(); const navigate = useNavigate(); @@ -150,19 +148,16 @@ export const OnboardingPage = ({ return ( - {isWindowsDesktop ? windowControl : null} - - + } isMacosDesktop={isMacosDesktop} isWindowsDesktop={isWindowsDesktop} @@ -265,7 +260,6 @@ export const OnboardingPage = ({ } return ( diff --git a/packages/frontend/component/src/theme/global.css b/packages/frontend/component/src/theme/global.css index 33b73419a7..1907c62220 100644 --- a/packages/frontend/component/src/theme/global.css +++ b/packages/frontend/component/src/theme/global.css @@ -288,6 +288,20 @@ textarea -webkit-app-region: no-drag; } +html[data-active='false'] { + opacity: 0; + transition: opacity 0.2s 0.1s; +} + +html[data-active='true']:has([data-blur-background='true']) { + opacity: 1; + transition: opacity 0.2s; +} + +html[data-active='false'] * { + -webkit-app-region: no-drag !important; +} + html, body { height: 100%; diff --git a/packages/frontend/component/src/ui/notification/notification-card.tsx b/packages/frontend/component/src/ui/notification/notification-card.tsx index 0217ebd59f..59d0971fc1 100644 --- a/packages/frontend/component/src/ui/notification/notification-card.tsx +++ b/packages/frontend/component/src/ui/notification/notification-card.tsx @@ -80,7 +80,10 @@ export const NotificationCard = ({ notification }: NotificationCardProps) => { data-float={!!thumb} className={clsx(styles.headAlignWrapper, styles.closeButton)} > - + diff --git a/packages/frontend/component/src/ui/scrollbar/scrollbar.tsx b/packages/frontend/component/src/ui/scrollbar/scrollbar.tsx index 2d2291428a..137c8bf284 100644 --- a/packages/frontend/component/src/ui/scrollbar/scrollbar.tsx +++ b/packages/frontend/component/src/ui/scrollbar/scrollbar.tsx @@ -1,7 +1,6 @@ import * as ScrollArea from '@radix-ui/react-scroll-area'; import clsx from 'clsx'; import type { PropsWithChildren } from 'react'; -import { useRef } from 'react'; import * as styles from './index.css'; import { useHasScrollTop } from './use-has-scroll-top'; @@ -24,8 +23,7 @@ export const ScrollableContainer = ({ viewPortClassName, scrollBarClassName, }: PropsWithChildren) => { - const ref = useRef(null); - const hasScrollTop = useHasScrollTop(ref); + const [setContainer, hasScrollTop] = useHasScrollTop(); return (
{children}
diff --git a/packages/frontend/component/src/ui/scrollbar/use-has-scroll-top.tsx b/packages/frontend/component/src/ui/scrollbar/use-has-scroll-top.tsx index b5f5cace49..eb0f97225f 100644 --- a/packages/frontend/component/src/ui/scrollbar/use-has-scroll-top.tsx +++ b/packages/frontend/component/src/ui/scrollbar/use-has-scroll-top.tsx @@ -1,31 +1,27 @@ -import type { RefObject } from 'react'; -import { useEffect, useState } from 'react'; +import { debounce } from 'lodash-es'; +import { useMemo, useState } from 'react'; -export function useHasScrollTop(ref: RefObject | null) { +export function useHasScrollTop() { const [hasScrollTop, setHasScrollTop] = useState(false); - - useEffect(() => { - if (!ref?.current) { - return; - } - - const container = ref.current; - - function updateScrollTop() { - if (container) { - setTimeout(() => { - const hasScrollTop = container.scrollTop > 0; - setHasScrollTop(hasScrollTop); - }); - } - } - - container.addEventListener('scroll', updateScrollTop); - updateScrollTop(); - return () => { - container.removeEventListener('scroll', updateScrollTop); + const containerRefFn = useMemo(() => { + let unsub: (() => void) | null = null; + return (container: HTMLElement | null) => { + unsub?.(); + const updateScrollTop = debounce(() => { + if (container) { + setTimeout(() => { + const hasScrollTop = container.scrollTop > 0; + setHasScrollTop(hasScrollTop); + }); + } + }, 50); + container?.addEventListener('scroll', updateScrollTop); + updateScrollTop(); + unsub = () => { + container?.removeEventListener('scroll', updateScrollTop); + }; }; - }, [ref]); + }, []); - return hasScrollTop; + return [containerRefFn, hasScrollTop] as const; } diff --git a/packages/frontend/core/src/blocksuite/presets/ai/chat-panel/chat-panel-messages.ts b/packages/frontend/core/src/blocksuite/presets/ai/chat-panel/chat-panel-messages.ts index fb6bffe189..6e55c41b9d 100644 --- a/packages/frontend/core/src/blocksuite/presets/ai/chat-panel/chat-panel-messages.ts +++ b/packages/frontend/core/src/blocksuite/presets/ai/chat-panel/chat-panel-messages.ts @@ -168,7 +168,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) { private _renderAIOnboarding() { return this.isLoading || - !this.host.doc.awarenessStore.getFlag('enable_ai_onboarding') + !this.host?.doc.awarenessStore.getFlag('enable_ai_onboarding') ? nothing : html`
{ + // 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 ( <>