diff --git a/packages/frontend/component/src/components/global-loading/index.css.ts b/packages/frontend/component/src/components/global-loading/index.css.ts new file mode 100644 index 0000000000..aa1181e888 --- /dev/null +++ b/packages/frontend/component/src/components/global-loading/index.css.ts @@ -0,0 +1,29 @@ +import { style } from '@vanilla-extract/css'; + +export const globalLoadingWrapperStyle = style({ + position: 'fixed', + top: 0, + left: 0, + bottom: 0, + right: '100%', + zIndex: 5, + backgroundColor: 'var(--affine-background-modal-color)', + opacity: 0, + transition: 'opacity .3s', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + color: 'var(--affine-processing-color)', + '@media': { + print: { + display: 'none', + zIndex: -1, + }, + }, + selectors: { + '&[data-loading="true"]': { + right: 0, + opacity: 1, + }, + }, +}); diff --git a/packages/frontend/component/src/components/global-loading/index.jotai.ts b/packages/frontend/component/src/components/global-loading/index.jotai.ts new file mode 100644 index 0000000000..20a31c4520 --- /dev/null +++ b/packages/frontend/component/src/components/global-loading/index.jotai.ts @@ -0,0 +1,35 @@ +import { atom } from 'jotai'; +import { nanoid } from 'nanoid'; + +export type GlobalLoadingEvent = { + key?: string; +}; + +const globalLoadingEventsBaseAtom = atom([]); + +export const globalLoadingEventsAtom = atom(get => + get(globalLoadingEventsBaseAtom) +); + +export const resolveGlobalLoadingEventAtom = atom( + null, + (_, set, key: string) => { + set(globalLoadingEventsBaseAtom, globalLoadingEvent => + globalLoadingEvent.filter(notification => notification.key !== key) + ); + } +); + +export const pushGlobalLoadingEventAtom = atom< + null, + [GlobalLoadingEvent], + void +>(null, (_, set, newGlobalLoadingEvent) => { + newGlobalLoadingEvent.key = newGlobalLoadingEvent.key || nanoid(); + + set(globalLoadingEventsBaseAtom, globalLoadingEvents => [ + // push to the top + { ...newGlobalLoadingEvent }, + ...globalLoadingEvents, + ]); +}); diff --git a/packages/frontend/component/src/components/global-loading/index.tsx b/packages/frontend/component/src/components/global-loading/index.tsx new file mode 100644 index 0000000000..b37b3452a9 --- /dev/null +++ b/packages/frontend/component/src/components/global-loading/index.tsx @@ -0,0 +1,34 @@ +import { useAtomValue } from 'jotai'; +import { type ReactNode, useEffect, useState } from 'react'; + +import { Loading } from '../../ui/loading'; +import * as styles from './index.css'; +import { globalLoadingEventsAtom } from './index.jotai'; + +export { + type GlobalLoadingEvent, + pushGlobalLoadingEventAtom, + resolveGlobalLoadingEventAtom, +} from './index.jotai'; + +export function GlobalLoading(): ReactNode { + const globalLoadingEvents = useAtomValue(globalLoadingEventsAtom); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (globalLoadingEvents.length) { + setLoading(true); + } else { + setLoading(false); + } + }, [globalLoadingEvents]); + + if (!globalLoadingEvents.length) { + return null; + } + return ( +
+ +
+ ); +} diff --git a/packages/frontend/core/src/app.tsx b/packages/frontend/core/src/app.tsx index e88920d8cf..be9deb4d55 100644 --- a/packages/frontend/core/src/app.tsx +++ b/packages/frontend/core/src/app.tsx @@ -3,6 +3,7 @@ import '@affine/component/theme/theme.css'; import '@toeverything/components/style.css'; import { AffineContext } from '@affine/component/context'; +import { GlobalLoading } from '@affine/component/global-loading'; import { NotificationCenter } from '@affine/component/notification-center'; import { WorkspaceFallback } from '@affine/component/workspace'; import { CacheProvider } from '@emotion/react'; @@ -62,6 +63,7 @@ export const App = memo(function App() { + {runtimeConfig.enableNotificationCenter && } } diff --git a/packages/frontend/core/src/hooks/affine/use-export-page.ts b/packages/frontend/core/src/hooks/affine/use-export-page.ts index ce0a7498ab..9a181fc267 100644 --- a/packages/frontend/core/src/hooks/affine/use-export-page.ts +++ b/packages/frontend/core/src/hooks/affine/use-export-page.ts @@ -1,9 +1,14 @@ +import { + pushGlobalLoadingEventAtom, + resolveGlobalLoadingEventAtom, +} from '@affine/component/global-loading'; import { pushNotificationAtom } from '@affine/component/notification-center'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import type { PageBlockModel } from '@blocksuite/blocks'; import { ContentParser } from '@blocksuite/blocks/content-parser'; import type { Page } from '@blocksuite/store'; import { useSetAtom } from 'jotai'; +import { nanoid } from 'nanoid'; import { useCallback } from 'react'; type ExportType = 'pdf' | 'html' | 'png' | 'markdown'; @@ -49,10 +54,16 @@ async function exportHandler({ page, type }: ExportHandlerOptions) { export const useExportPage = (page: Page) => { const pushNotification = useSetAtom(pushNotificationAtom); + const pushGlobalLoadingEvent = useSetAtom(pushGlobalLoadingEventAtom); + const resolveGlobalLoadingEvent = useSetAtom(resolveGlobalLoadingEventAtom); const t = useAFFiNEI18N(); const onClickHandler = useCallback( async (type: ExportType) => { + const globalLoadingID = nanoid(); + pushGlobalLoadingEvent({ + key: globalLoadingID, + }); try { await exportHandler({ page, @@ -70,9 +81,17 @@ export const useExportPage = (page: Page) => { message: t['com.affine.export.error.message'](), type: 'error', }); + } finally { + resolveGlobalLoadingEvent(globalLoadingID); } }, - [page, pushNotification, t] + [ + page, + pushGlobalLoadingEvent, + pushNotification, + resolveGlobalLoadingEvent, + t, + ] ); return onClickHandler;