refactor(electron): tab title/icon update logic (#7675)

fix AF-1122
fix AF-1136
This commit is contained in:
pengx17
2024-08-01 16:43:18 +00:00
parent e60b2d64e5
commit 07409b8a91
20 changed files with 261 additions and 217 deletions

View File

@@ -7,21 +7,8 @@ import {
import { appSidebarWidthAtom } from '@affine/core/components/app-sidebar/index.jotai';
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { DesktopStateSynchronizer } from '@affine/core/modules/workbench/services/desktop-state-synchronizer';
import type { WorkbenchMeta } from '@affine/electron-api';
import { apis, events } from '@affine/electron-api';
import {
AllDocsIcon,
CloseIcon,
DeleteIcon,
EdgelessIcon,
PageIcon,
PlusIcon,
RightSidebarIcon,
TagIcon,
TodayIcon,
ViewLayersIcon,
} from '@blocksuite/icons/rc';
import { CloseIcon, PlusIcon, RightSidebarIcon } from '@blocksuite/icons/rc';
import {
useLiveData,
useService,
@@ -38,25 +25,14 @@ import {
useState,
} from 'react';
import { iconNameToIcon } from '../../workbench/constants';
import { DesktopStateSynchronizer } from '../../workbench/services/desktop-state-synchronizer';
import {
AppTabsHeaderService,
type TabStatus,
} from '../services/app-tabs-header-service';
import * as styles from './styles.css';
type ModuleName = NonNullable<WorkbenchMeta['views'][0]['moduleName']>;
const moduleNameToIcon = {
all: <AllDocsIcon />,
collection: <ViewLayersIcon />,
doc: <PageIcon />,
page: <PageIcon />,
edgeless: <EdgelessIcon />,
journal: <TodayIcon />,
tag: <TagIcon />,
trash: <DeleteIcon />,
} satisfies Record<ModuleName, ReactNode>;
const WorkbenchTab = ({
workbench,
active: tabActive,
@@ -66,6 +42,7 @@ const WorkbenchTab = ({
active: boolean;
tabsLength: number;
}) => {
useServiceOptional(DesktopStateSynchronizer);
const tabsHeaderService = useService(AppTabsHeaderService);
const activeViewIndex = workbench.activeViewIndex ?? 0;
const onContextMenu = useAsyncCallback(
@@ -115,7 +92,7 @@ const WorkbenchTab = ({
>
<div className={styles.labelIcon}>
{workbench.ready || !workbench.loaded ? (
moduleNameToIcon[view.moduleName ?? 'all']
iconNameToIcon[view.iconName ?? 'allDocs']
) : (
<Loading />
)}
@@ -194,8 +171,6 @@ export const AppTabsHeader = ({
await tabsHeaderService.onToggleRightSidebar();
}, [tabsHeaderService]);
useServiceOptional(DesktopStateSynchronizer);
useEffect(() => {
if (mode === 'app') {
apis?.ui.pingAppLayoutReady().catch(console.error);

View File

@@ -80,7 +80,7 @@ export const tab = style({
boxShadow: cssVar('shadow1'),
},
'&[data-pinned="false"]': {
paddingRight: 8,
paddingRight: 20,
},
'&[data-pinned="true"]': {
flexShrink: 0,

View File

@@ -0,0 +1,23 @@
import {
AllDocsIcon,
DeleteIcon,
EdgelessIcon,
PageIcon,
TagIcon,
TodayIcon,
ViewLayersIcon,
} from '@blocksuite/icons/rc';
import type { ReactNode } from 'react';
export const iconNameToIcon = {
allDocs: <AllDocsIcon />,
collection: <ViewLayersIcon />,
doc: <PageIcon />,
page: <PageIcon />,
edgeless: <EdgelessIcon />,
journal: <TodayIcon />,
tag: <TagIcon />,
trash: <DeleteIcon />,
} satisfies Record<string, ReactNode>;
export type ViewIconName = keyof typeof iconNameToIcon;

View File

@@ -3,12 +3,15 @@ import type { Location, To } from 'history';
import { Observable } from 'rxjs';
import { createNavigableHistory } from '../../../utils/navigable-history';
import type { ViewIconName } from '../constants';
import { ViewScope } from '../scopes/view';
import { SidebarTab } from './sidebar-tab';
export class View extends Entity<{
id: string;
defaultLocation?: To | undefined;
title?: string;
icon?: ViewIconName;
}> {
scope = this.framework.createScope(ViewScope, {
view: this as View,
@@ -70,6 +73,10 @@ export class View extends Entity<{
size$ = new LiveData(100);
title$ = new LiveData(this.props.title ?? '');
icon$ = new LiveData(this.props.icon ?? 'allDocs');
push(path: To) {
this.history.push(path);
}
@@ -105,4 +112,12 @@ export class View extends Entity<{
activeSidebarTab(id: string | null) {
this._activeSidebarTabId$.next(id);
}
setTitle(title: string) {
this.title$.next(title);
}
setIcon(icon: ViewIconName) {
this.icon$.next(icon);
}
}

View File

@@ -3,17 +3,16 @@ export { ViewScope } from './scopes/view';
export { WorkbenchService } from './services/workbench';
export { useIsActiveView } from './view/use-is-active-view';
export { ViewBody, ViewHeader, ViewSidebarTab } from './view/view-islands';
export { ViewIcon, ViewTitle } from './view/view-meta';
export { WorkbenchLink } from './view/workbench-link';
export { WorkbenchRoot } from './view/workbench-root';
import {
DocsService,
type Framework,
GlobalStateService,
WorkspaceScope,
} from '@toeverything/infra';
import { WorkspacePropertiesAdapter } from '../properties';
import { SidebarTab } from './entities/sidebar-tab';
import { View } from './entities/view';
import { Workbench } from './entities/workbench';
@@ -52,9 +51,5 @@ export function configureDesktopWorkbenchModule(services: Framework) {
.impl(WorkbenchDefaultState, DesktopWorkbenchDefaultState, [
GlobalStateService,
])
.service(DesktopStateSynchronizer, [
WorkbenchService,
WorkspacePropertiesAdapter,
DocsService,
]);
.service(DesktopStateSynchronizer, [WorkbenchService]);
}

View File

@@ -1,35 +1,13 @@
import {
apis,
appInfo,
events,
type WorkbenchViewMeta,
} from '@affine/electron-api';
import { I18n, type I18nKeys, i18nTime } from '@affine/i18n';
import type { DocsService } from '@toeverything/infra';
import { Service } from '@toeverything/infra';
import { combineLatest, filter, map, of, switchMap } from 'rxjs';
import { apis, appInfo, events } from '@affine/electron-api';
import { LiveData, Service } from '@toeverything/infra';
import { resolveRouteLinkMeta } from '../../navigation';
import type { RouteModulePath } from '../../navigation/utils';
import type { WorkspacePropertiesAdapter } from '../../properties';
import type { WorkbenchService } from '../../workbench';
const routeModuleToI18n = {
all: 'All pages',
collection: 'Collections',
tag: 'Tags',
trash: 'Trash',
} satisfies Record<RouteModulePath, I18nKeys>;
/**
* Synchronize workbench state with state stored in main process
*/
export class DesktopStateSynchronizer extends Service {
constructor(
private readonly workbenchService: WorkbenchService,
private readonly workspaceProperties: WorkspacePropertiesAdapter,
private readonly docsService: DocsService
) {
constructor(private readonly workbenchService: WorkbenchService) {
super();
this.startSync();
}
@@ -80,70 +58,31 @@ export class DesktopStateSynchronizer extends Service {
// sync workbench state with main process
// also fill tab view meta with title & moduleName
this.workspaceProperties.workspace.engine.rootDocState$
.pipe(
filter(v => v.ready),
switchMap(() => workbench.views$),
switchMap(views => {
return combineLatest(
views.map(view =>
view.location$.map(location => {
return {
view,
location,
};
})
)
);
}),
map(viewLocations => {
if (!apis || !appInfo?.viewId) {
return;
}
const viewMetas = viewLocations.map(({ view, location }) => {
return {
id: view.id,
path: location,
};
});
return viewMetas.map(viewMeta => this.fillTabViewMeta(viewMeta));
}),
filter(v => !!v),
switchMap(viewMetas => {
return this.docsService.list.docs$.pipe(
switchMap(docs => {
return combineLatest(
viewMetas.map(vm => {
return (
docs
.find(doc => doc.id === vm.docId)
?.mode$.asObservable() ?? of('page')
).pipe(
map(mode => ({
...vm,
moduleName:
vm.moduleName === 'page' ? mode : vm.moduleName,
}))
);
})
);
})
);
})
)
.subscribe(viewMetas => {
if (!apis || !appInfo?.viewId) {
return;
}
apis.ui
.updateWorkbenchMeta(appInfo.viewId, {
views: viewMetas,
})
.catch(console.error);
LiveData.computed(get => {
return get(workbench.views$).map(view => {
const location = get(view.location$);
return {
id: view.id,
title: get(view.title$),
iconName: get(view.icon$),
path: {
pathname: location.pathname,
search: location.search,
hash: location.hash,
},
};
});
}).subscribe(views => {
if (!apis || !appInfo?.viewId) {
return;
}
apis.ui
.updateWorkbenchMeta(appInfo.viewId, {
views,
})
.catch(console.error);
});
workbench.activeViewIndex$.subscribe(activeViewIndex => {
if (!apis || !appInfo?.viewId) {
@@ -169,59 +108,4 @@ export class DesktopStateSynchronizer extends Service {
.catch(console.error);
});
};
private toFullUrl(
basename: string,
location: { hash?: string; pathname?: string; search?: string }
) {
return basename + location.pathname + location.search + location.hash;
}
// fill tab view meta with title & moduleName
private fillTabViewMeta(
view: WorkbenchViewMeta
): WorkbenchViewMeta & { docId?: string } {
if (!view.path) {
return view;
}
const url = this.toFullUrl(
this.workbenchService.workbench.basename$.value,
view.path
);
const linkMeta = resolveRouteLinkMeta(url);
if (!linkMeta) {
return view;
}
const journalString =
linkMeta.moduleName === 'doc'
? this.workspaceProperties.getJournalPageDateString(linkMeta.docId)
: undefined;
const isJournal = !!journalString;
const title = (() => {
// todo: resolve more module types like collections?
if (linkMeta?.moduleName === 'doc') {
if (journalString) {
return i18nTime(journalString, { absolute: { accuracy: 'day' } });
}
return (
this.workspaceProperties.workspace.docCollection.meta.getDocMeta(
linkMeta.docId
)?.title || I18n['Untitled']()
);
} else {
return I18n[routeModuleToI18n[linkMeta.moduleName]]();
}
})();
return {
...view,
title: title,
docId: linkMeta.docId,
moduleName: isJournal ? 'journal' : linkMeta.moduleName,
};
}
}

View File

@@ -21,6 +21,7 @@ export function useBindWorkbenchToDesktopRouter(
basename: string
) {
const browserLocation = useLocation();
useEffect(() => {
const newLocation = browserLocationToViewLocation(
browserLocation,
@@ -36,6 +37,7 @@ export function useBindWorkbenchToDesktopRouter(
) {
return;
}
workbench.open(newLocation);
}, [basename, browserLocation, workbench]);
}

View File

@@ -0,0 +1,29 @@
import { useServiceOptional } from '@toeverything/infra';
import { useEffect } from 'react';
import type { ViewIconName } from '../constants';
import { ViewService } from '../services/view';
export const ViewTitle = ({ title }: { title: string }) => {
const view = useServiceOptional(ViewService)?.view;
useEffect(() => {
if (view) {
view.setTitle(title);
}
}, [title, view]);
return null;
};
export const ViewIcon = ({ icon }: { icon: ViewIconName }) => {
const view = useServiceOptional(ViewService)?.view;
useEffect(() => {
if (view) {
view.setIcon(icon);
}
}, [icon, view]);
return null;
};

View File

@@ -6,6 +6,10 @@ import {
VirtualizedCollectionList,
} from '@affine/core/components/page-list';
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
import {
ViewIcon,
ViewTitle,
} from '@affine/core/modules/workbench/view/view-meta';
import { useI18n } from '@affine/i18n';
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
import { nanoid } from 'nanoid';
@@ -55,6 +59,8 @@ export const AllCollection = () => {
return (
<>
<ViewTitle title={t['Collections']()} />
<ViewIcon icon="collection" />
<ViewHeader>
<AllCollectionHeader
showCreateNew={!hideHeaderCreateNew}

View File

@@ -6,6 +6,7 @@ import {
import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta';
import { performanceRenderLogger } from '@affine/core/shared';
import type { Filter } from '@affine/env/filter';
import { useI18n } from '@affine/i18n';
import {
GlobalContextService,
useService,
@@ -17,6 +18,8 @@ import {
useIsActiveView,
ViewBody,
ViewHeader,
ViewIcon,
ViewTitle,
} from '../../../modules/workbench';
import { EmptyPageList } from '../page-list-empty';
import * as styles from './all-page.css';
@@ -47,8 +50,12 @@ export const AllPage = () => {
return;
}, [globalContext, isActiveView]);
const t = useI18n();
return (
<>
<ViewTitle title={t['All pages']()} />
<ViewIcon icon="allDocs" />
<ViewHeader>
<AllPageHeader
showCreateNew={!hideHeaderCreateNew}

View File

@@ -5,10 +5,16 @@ import {
import { CreateOrEditTag } from '@affine/core/components/page-list/tags/create-tag';
import type { TagMeta } from '@affine/core/components/page-list/types';
import { DeleteTagConfirmModal, TagService } from '@affine/core/modules/tag';
import { useI18n } from '@affine/i18n';
import { useLiveData, useService } from '@toeverything/infra';
import { useCallback, useState } from 'react';
import { ViewBody, ViewHeader } from '../../../modules/workbench';
import {
ViewBody,
ViewHeader,
ViewIcon,
ViewTitle,
} from '../../../modules/workbench';
import { EmptyTagList } from '../page-list-empty';
import * as styles from './all-tag.css';
import { AllTagHeader } from './header';
@@ -54,8 +60,12 @@ export const AllTag = () => {
[setOpen, setSelectedTagIds]
);
const t = useI18n();
return (
<>
<ViewTitle title={t['Tags']()} />
<ViewIcon icon="tag" />
<ViewHeader>
<AllTagHeader />
</ViewHeader>

View File

@@ -29,6 +29,8 @@ import {
useIsActiveView,
ViewBody,
ViewHeader,
ViewIcon,
ViewTitle,
} from '../../../modules/workbench';
import { WorkspaceSubPath } from '../../../shared';
import * as styles from './collection.css';
@@ -155,6 +157,8 @@ const Placeholder = ({ collection }: { collection: Collection }) => {
return (
<>
<ViewTitle title={collection.name} />
<ViewIcon icon="collection" />
<ViewHeader>
<div
style={{

View File

@@ -13,9 +13,16 @@ import { PageHeaderMenuButton } from '@affine/core/components/blocksuite/block-s
import { DetailPageHeaderPresentButton } from '@affine/core/components/blocksuite/block-suite-header/present/detail-header-present-button';
import { EditorModeSwitch } from '@affine/core/components/blocksuite/block-suite-mode-switch';
import { useRegisterCopyLinkCommands } from '@affine/core/hooks/affine/use-register-copy-link-commands';
import { useDocCollectionPageTitle } from '@affine/core/hooks/use-block-suite-workspace-page-title';
import { useJournalInfoHelper } from '@affine/core/hooks/use-journal';
import { ViewIcon, ViewTitle } from '@affine/core/modules/workbench';
import type { Doc } from '@blocksuite/store';
import { type Workspace } from '@toeverything/infra';
import {
DocService,
useLiveData,
useService,
type Workspace,
} from '@toeverything/infra';
import { useAtom, useAtomValue } from 'jotai';
import { forwardRef, useCallback, useEffect, useRef, useState } from 'react';
@@ -68,8 +75,11 @@ export function JournalPageHeader({ page, workspace }: PageHeaderProps) {
const { hideShare, hideToday } =
useDetailPageHeaderResponsive(containerWidth);
const title = useDocCollectionPageTitle(workspace.docCollection, page?.id);
return (
<Header className={styles.header} ref={containerRef}>
<ViewTitle title={title} />
<ViewIcon icon="journal" />
<EditorModeSwitch
docCollection={workspace.docCollection}
pageId={page?.id}
@@ -115,8 +125,15 @@ export function NormalPageHeader({ page, workspace }: PageHeaderProps) {
const onRename = useCallback(() => {
setTimeout(() => titleInputHandleRef.current?.triggerEdit());
}, []);
const title = useDocCollectionPageTitle(workspace.docCollection, page?.id);
const doc = useService(DocService).doc;
const currentMode = useLiveData(doc.mode$);
return (
<Header className={styles.header} ref={containerRef}>
<ViewTitle title={title} />
<ViewIcon icon={currentMode ?? 'page'} />
<EditorModeSwitch
docCollection={workspace.docCollection}
pageId={page?.id}

View File

@@ -8,6 +8,8 @@ import {
useIsActiveView,
ViewBody,
ViewHeader,
ViewIcon,
ViewTitle,
} from '@affine/core/modules/workbench';
import {
GlobalContextService,
@@ -39,6 +41,7 @@ export const TagDetail = ({ tagId }: { tagId?: string }) => {
}, [pageIds, pageMetas]);
const isActiveView = useIsActiveView();
const tagName = useLiveData(currentTag?.value$);
useEffect(() => {
if (isActiveView && currentTag) {
@@ -59,6 +62,8 @@ export const TagDetail = ({ tagId }: { tagId?: string }) => {
return (
<>
<ViewTitle title={tagName ?? 'Untitled'} />
<ViewIcon icon="tag" />
<ViewHeader>
<TagDetailHeader />
</ViewHeader>

View File

@@ -14,7 +14,13 @@ import {
} from '@toeverything/infra';
import { useEffect } from 'react';
import { useIsActiveView, ViewBody, ViewHeader } from '../../modules/workbench';
import {
useIsActiveView,
ViewBody,
ViewHeader,
ViewIcon,
ViewTitle,
} from '../../modules/workbench';
import { EmptyPageList } from './page-list-empty';
import * as styles from './trash-page.css';
@@ -56,8 +62,11 @@ export const TrashPage = () => {
return;
}, [globalContextService.globalContext.isTrash, isActiveView]);
const t = useI18n();
return (
<>
<ViewTitle title={t['Trash']()} />
<ViewIcon icon={'trash'} />
<ViewHeader>
<TrashHeader />
</ViewHeader>