fix(electron): fix tab view blink issue on open new tab (#7748)

fix AF-1197
This commit is contained in:
pengx17 2024-08-06 15:57:40 +00:00
parent 744cc542de
commit c2cf331ff7
No known key found for this signature in database
GPG Key ID: 23F23D9E8B3971ED
10 changed files with 105 additions and 84 deletions

View File

@ -3,13 +3,13 @@ import {
NotFoundPage,
} from '@affine/component/not-found-page';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { apis } from '@affine/electron-api';
import { useLiveData, useService } from '@toeverything/infra';
import type { ReactElement } from 'react';
import { useCallback, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { SignOutModal } from '../components/affine/sign-out-modal';
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
import { AppTabsHeader } from '../modules/app-tabs-header';
import { AuthService } from '../modules/cloud';
import { SignIn } from './sign-in';
@ -37,15 +37,12 @@ export const PageNotFound = ({
await authService.signOut();
}, [authService]);
useEffect(() => {
apis?.ui.pingAppLayoutReady().catch(console.error);
}, []);
return (
<>
{environment.isDesktop ? (
<AppTabsHeader
style={{
paddingLeft: environment.isMacOs ? 80 : 0,
}}
/>
) : null}
{noPermission ? (
<NoPermissionOrNotFound
user={account}

View File

@ -1,4 +1,5 @@
import { Menu } from '@affine/component/ui/menu';
import { apis } from '@affine/electron-api';
import { WorkspaceFlavour } from '@affine/env/workspace';
import {
useLiveData,
@ -22,7 +23,6 @@ import {
import { AppFallback } from '../components/affine/app-container';
import { UserWithWorkspaceList } from '../components/pure/workspace-slider-bar/user-with-workspace-list';
import { useNavigateHelper } from '../hooks/use-navigate-helper';
import { AppTabsHeader } from '../modules/app-tabs-header';
import { AuthService } from '../modules/cloud';
import { WorkspaceSubPath } from '../shared';
@ -118,6 +118,10 @@ export const Component = () => {
navigating,
]);
useEffect(() => {
apis?.ui.pingAppLayoutReady().catch(console.error);
}, []);
useEffect(() => {
setCreating(true);
createFirstAppData(workspacesService)
@ -148,13 +152,6 @@ export const Component = () => {
// TODO(@eyhn): We need a no workspace page
return (
<>
{environment.isDesktop ? (
<AppTabsHeader
style={{
paddingLeft: environment.isMacOs ? 80 : 0,
}}
/>
) : null}
<div
style={{
position: 'fixed',

View File

@ -0,0 +1,16 @@
html,
html[data-active='false'] {
opacity: 0;
}
html[data-active='false'] {
transition: opacity 0.01s 0.2s;
}
html[data-active='false'] * {
-webkit-app-region: no-drag !important;
}
html[data-active='true'] {
opacity: 1;
}

View File

@ -2,6 +2,7 @@ import 'setimmediate';
import '@affine/component/theme/global.css';
import '@affine/component/theme/theme.css';
import '@affine/core/bootstrap/preload';
import './index.css';
import { ThemeProvider } from '@affine/component/theme-provider';
import { configureAppTabsHeaderModule } from '@affine/core/modules/app-tabs-header';
@ -41,11 +42,15 @@ async function main() {
const handleFullscreen = (fullscreen: boolean | undefined) => {
document.documentElement.dataset.fullscreen = String(fullscreen);
};
const handleActive = (active: boolean | undefined) => {
document.documentElement.dataset.active = String(active);
};
apis?.ui.isMaximized().then(handleMaximized).catch(console.error);
apis?.ui.isFullScreen().then(handleFullscreen).catch(console.error);
events?.ui.onMaximized(handleMaximized);
events?.ui.onFullScreen(handleFullscreen);
events?.ui.onTabShellViewActiveChange(handleActive);
await loadLanguage();
mountApp();

View File

@ -1,25 +1,16 @@
import { cssVar } from '@toeverything/theme';
import { createVar, globalStyle, style } from '@vanilla-extract/css';
import { createVar, style } from '@vanilla-extract/css';
export const sidebarOffsetVar = createVar();
export const root = style({
width: '100vw',
height: '100vh',
opacity: 1,
display: 'flex',
transition: 'opacity 0.1s',
background: cssVar('backgroundPrimaryColor'),
selectors: {
'&[data-active="false"]': {
opacity: 0,
},
'&[data-translucent="true"]': {
background: 'transparent',
},
},
});
globalStyle(`${root}[data-active="false"] *`, {
['WebkitAppRegion' as string]: 'no-drag !important',
});

View File

@ -1,38 +1,16 @@
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
import { AppTabsHeader } from '@affine/core/modules/app-tabs-header';
import { events } from '@affine/electron-api';
import { useEffect, useState } from 'react';
import * as styles from './shell.css';
const useIsShellActive = () => {
const [active, setActive] = useState(false);
useEffect(() => {
const unsub = events?.ui.onTabShellViewActiveChange(active => {
setActive(active);
});
return () => {
unsub?.();
};
});
return active;
};
export function ShellRoot() {
const active = useIsShellActive();
const { appSettings } = useAppSettingHelper();
const translucent =
environment.isDesktop &&
environment.isMacOs &&
appSettings.enableBlurBackground;
return (
<div
className={styles.root}
data-translucent={translucent}
data-active={active}
>
<div className={styles.root} data-translucent={translucent}>
<AppTabsHeader mode="shell" />
</div>
);

View File

@ -19,6 +19,7 @@ function getActiveWindows() {
}
export function registerEvents() {
const unsubs: (() => void)[] = [];
// register events
for (const [namespace, namespaceEvents] of Object.entries(allEvents)) {
for (const [key, eventRegister] of Object.entries(namespaceEvents)) {
@ -52,14 +53,15 @@ export function registerEvents() {
});
});
});
app.on('before-quit', () => {
// subscription on quit sometimes crashes the app
try {
unsubscribe();
} catch (err) {
logger.error('unsubscribe error', err);
}
});
unsubs.push(unsubscribe);
}
}
app.on('before-quit', () => {
// subscription on quit sometimes crashes the app
try {
unsubs.forEach(unsub => unsub());
} catch (err) {
logger.error('unsubscribe error', err);
}
});
}

View File

@ -62,7 +62,7 @@ export const showTabContextMenu = async (tabId: string, viewIndex: number) => {
},
},
...(workbenches.length > 0
...(workbenches.length > 1
? ([
{ type: 'separator' },
{

View File

@ -140,6 +140,10 @@ export class WebContentViewsManager {
readonly tabViewsMeta$ = TabViewsMetaState.$;
readonly appTabsUIReady$ = new BehaviorSubject(new Set<string>());
get appTabsUIReady() {
return this.appTabsUIReady$.value;
}
// all web views
readonly webViewsMap$ = new BehaviorSubject(
new Map<string, WebContentsView>()
@ -251,8 +255,12 @@ export class WebContentViewsManager {
}
setTabUIReady = (tabId: string) => {
this.appTabsUIReady$.next(new Set([...this.appTabsUIReady$.value, tabId]));
this.appTabsUIReady$.next(new Set([...this.appTabsUIReady, tabId]));
this.reorderViews();
const view = this.tabViewsMap.get(tabId);
if (view) {
this.resizeView(view);
}
};
getViewIdFromWebContentsId = (id: number) => {
@ -460,10 +468,6 @@ export class WebContentViewsManager {
showTab = async (id: string): Promise<WebContentsView | undefined> => {
if (this.activeWorkbenchId !== id) {
// todo: this will cause the shell view to be on top and flickers the screen
// this.appTabsUIReady$.next(
// new Set([...this.appTabsUIReady$.value].filter(key => key !== id))
// );
this.patchTabViewsMeta({
activeWorkbenchId: id,
});
@ -601,26 +605,59 @@ export class WebContentViewsManager {
// if tab ui of the current active view is not ready,
// make sure shell view is on top
const activeView = this.activeWorkbenchView;
const ready = this.activeWorkbenchId
? this.appTabsUIReady$.value.has(this.activeWorkbenchId)
: false;
// inactive < active view (not ready) < shell < active view (ready)
const getScore = (view: View) => {
if (view === this.shellView) {
return 2;
}
if (view === activeView) {
return ready ? 3 : 1;
}
return 0;
const getViewId = (view: View) => {
return [...this.tabViewsMap.entries()].find(
([_, v]) => v === view
)?.[0];
};
[...this.tabViewsMap.values()]
.toSorted((a, b) => getScore(a) - getScore(b))
.forEach((view, index) => {
this.mainWindow?.contentView.addChildView(view, index);
});
const isViewReady = (view: View) => {
if (view === this.shellView) {
return true;
}
const id = getViewId(view);
return id ? this.appTabsUIReady.has(id) : false;
};
// 2: active view (ready)
// 1: shell
// 0: inactive view (ready)
// -1 inactive view (not ready)
// -1 active view (not ready)
const getScore = (view: View) => {
if (view === this.shellView) {
return 1;
}
const viewReady = isViewReady(view);
if (view === activeView) {
return viewReady ? 2 : -1;
} else {
return viewReady ? 0 : -1;
}
};
const sorted = [...this.tabViewsMap.entries()]
.map(([id, view]) => {
return {
id,
view,
score: getScore(view),
};
})
.filter(({ score }) => score >= 0)
.toSorted((a, b) => a.score - b.score);
// remove inactive views
this.mainWindow?.contentView.children.forEach(view => {
if (!isViewReady(view)) {
this.mainWindow?.contentView.removeChildView(view);
}
});
sorted.forEach(({ view }, idx) => {
this.mainWindow?.contentView.addChildView(view, idx);
});
}
};

View File

@ -140,10 +140,8 @@ export const test = base.extend<{
await use(electronApp);
console.log('Cleaning up...');
const pages = electronApp.windows();
for (let i = 0; i < pages.length; i++) {
const page = pages[i];
for (const page of pages) {
await page.close();
console.log(`Closed page ${i + 1}/${pages.length}`);
}
await electronApp.close();
await removeWithRetry(clonedDist);