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