fix(electron): fix tab view blink issue on open new tab (#7748)
fix AF-1197
This commit is contained in:
@@ -3,13 +3,13 @@ import {
|
|||||||
NotFoundPage,
|
NotFoundPage,
|
||||||
} from '@affine/component/not-found-page';
|
} from '@affine/component/not-found-page';
|
||||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
|
import { apis } from '@affine/electron-api';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import type { ReactElement } from 'react';
|
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 { SignOutModal } from '../components/affine/sign-out-modal';
|
||||||
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
|
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||||
import { AppTabsHeader } from '../modules/app-tabs-header';
|
|
||||||
import { AuthService } from '../modules/cloud';
|
import { AuthService } from '../modules/cloud';
|
||||||
import { SignIn } from './sign-in';
|
import { SignIn } from './sign-in';
|
||||||
|
|
||||||
@@ -37,15 +37,12 @@ export const PageNotFound = ({
|
|||||||
await authService.signOut();
|
await authService.signOut();
|
||||||
}, [authService]);
|
}, [authService]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
apis?.ui.pingAppLayoutReady().catch(console.error);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{environment.isDesktop ? (
|
|
||||||
<AppTabsHeader
|
|
||||||
style={{
|
|
||||||
paddingLeft: environment.isMacOs ? 80 : 0,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
{noPermission ? (
|
{noPermission ? (
|
||||||
<NoPermissionOrNotFound
|
<NoPermissionOrNotFound
|
||||||
user={account}
|
user={account}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Menu } from '@affine/component/ui/menu';
|
import { Menu } from '@affine/component/ui/menu';
|
||||||
|
import { apis } from '@affine/electron-api';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import {
|
import {
|
||||||
useLiveData,
|
useLiveData,
|
||||||
@@ -22,7 +23,6 @@ import {
|
|||||||
import { AppFallback } from '../components/affine/app-container';
|
import { AppFallback } from '../components/affine/app-container';
|
||||||
import { UserWithWorkspaceList } from '../components/pure/workspace-slider-bar/user-with-workspace-list';
|
import { UserWithWorkspaceList } from '../components/pure/workspace-slider-bar/user-with-workspace-list';
|
||||||
import { useNavigateHelper } from '../hooks/use-navigate-helper';
|
import { useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||||
import { AppTabsHeader } from '../modules/app-tabs-header';
|
|
||||||
import { AuthService } from '../modules/cloud';
|
import { AuthService } from '../modules/cloud';
|
||||||
import { WorkspaceSubPath } from '../shared';
|
import { WorkspaceSubPath } from '../shared';
|
||||||
|
|
||||||
@@ -118,6 +118,10 @@ export const Component = () => {
|
|||||||
navigating,
|
navigating,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
apis?.ui.pingAppLayoutReady().catch(console.error);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCreating(true);
|
setCreating(true);
|
||||||
createFirstAppData(workspacesService)
|
createFirstAppData(workspacesService)
|
||||||
@@ -148,13 +152,6 @@ export const Component = () => {
|
|||||||
// TODO(@eyhn): We need a no workspace page
|
// TODO(@eyhn): We need a no workspace page
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{environment.isDesktop ? (
|
|
||||||
<AppTabsHeader
|
|
||||||
style={{
|
|
||||||
paddingLeft: environment.isMacOs ? 80 : 0,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
|
|||||||
16
packages/frontend/electron/renderer/shell/index.css
Normal file
16
packages/frontend/electron/renderer/shell/index.css
Normal 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;
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import 'setimmediate';
|
|||||||
import '@affine/component/theme/global.css';
|
import '@affine/component/theme/global.css';
|
||||||
import '@affine/component/theme/theme.css';
|
import '@affine/component/theme/theme.css';
|
||||||
import '@affine/core/bootstrap/preload';
|
import '@affine/core/bootstrap/preload';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
import { ThemeProvider } from '@affine/component/theme-provider';
|
import { ThemeProvider } from '@affine/component/theme-provider';
|
||||||
import { configureAppTabsHeaderModule } from '@affine/core/modules/app-tabs-header';
|
import { configureAppTabsHeaderModule } from '@affine/core/modules/app-tabs-header';
|
||||||
@@ -41,11 +42,15 @@ async function main() {
|
|||||||
const handleFullscreen = (fullscreen: boolean | undefined) => {
|
const handleFullscreen = (fullscreen: boolean | undefined) => {
|
||||||
document.documentElement.dataset.fullscreen = String(fullscreen);
|
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.isMaximized().then(handleMaximized).catch(console.error);
|
||||||
apis?.ui.isFullScreen().then(handleFullscreen).catch(console.error);
|
apis?.ui.isFullScreen().then(handleFullscreen).catch(console.error);
|
||||||
events?.ui.onMaximized(handleMaximized);
|
events?.ui.onMaximized(handleMaximized);
|
||||||
events?.ui.onFullScreen(handleFullscreen);
|
events?.ui.onFullScreen(handleFullscreen);
|
||||||
|
events?.ui.onTabShellViewActiveChange(handleActive);
|
||||||
|
|
||||||
await loadLanguage();
|
await loadLanguage();
|
||||||
mountApp();
|
mountApp();
|
||||||
|
|||||||
@@ -1,25 +1,16 @@
|
|||||||
import { cssVar } from '@toeverything/theme';
|
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 sidebarOffsetVar = createVar();
|
||||||
|
|
||||||
export const root = style({
|
export const root = style({
|
||||||
width: '100vw',
|
width: '100vw',
|
||||||
height: '100vh',
|
height: '100vh',
|
||||||
opacity: 1,
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
transition: 'opacity 0.1s',
|
|
||||||
background: cssVar('backgroundPrimaryColor'),
|
background: cssVar('backgroundPrimaryColor'),
|
||||||
selectors: {
|
selectors: {
|
||||||
'&[data-active="false"]': {
|
|
||||||
opacity: 0,
|
|
||||||
},
|
|
||||||
'&[data-translucent="true"]': {
|
'&[data-translucent="true"]': {
|
||||||
background: 'transparent',
|
background: 'transparent',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
globalStyle(`${root}[data-active="false"] *`, {
|
|
||||||
['WebkitAppRegion' as string]: 'no-drag !important',
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,38 +1,16 @@
|
|||||||
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
|
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
|
||||||
import { AppTabsHeader } from '@affine/core/modules/app-tabs-header';
|
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';
|
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() {
|
export function ShellRoot() {
|
||||||
const active = useIsShellActive();
|
|
||||||
const { appSettings } = useAppSettingHelper();
|
const { appSettings } = useAppSettingHelper();
|
||||||
const translucent =
|
const translucent =
|
||||||
environment.isDesktop &&
|
environment.isDesktop &&
|
||||||
environment.isMacOs &&
|
environment.isMacOs &&
|
||||||
appSettings.enableBlurBackground;
|
appSettings.enableBlurBackground;
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={styles.root} data-translucent={translucent}>
|
||||||
className={styles.root}
|
|
||||||
data-translucent={translucent}
|
|
||||||
data-active={active}
|
|
||||||
>
|
|
||||||
<AppTabsHeader mode="shell" />
|
<AppTabsHeader mode="shell" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ function getActiveWindows() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function registerEvents() {
|
export function registerEvents() {
|
||||||
|
const unsubs: (() => void)[] = [];
|
||||||
// register events
|
// register events
|
||||||
for (const [namespace, namespaceEvents] of Object.entries(allEvents)) {
|
for (const [namespace, namespaceEvents] of Object.entries(allEvents)) {
|
||||||
for (const [key, eventRegister] of Object.entries(namespaceEvents)) {
|
for (const [key, eventRegister] of Object.entries(namespaceEvents)) {
|
||||||
@@ -52,14 +53,15 @@ export function registerEvents() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
unsubs.push(unsubscribe);
|
||||||
|
}
|
||||||
|
}
|
||||||
app.on('before-quit', () => {
|
app.on('before-quit', () => {
|
||||||
// subscription on quit sometimes crashes the app
|
// subscription on quit sometimes crashes the app
|
||||||
try {
|
try {
|
||||||
unsubscribe();
|
unsubs.forEach(unsub => unsub());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('unsubscribe error', err);
|
logger.error('unsubscribe error', err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export const showTabContextMenu = async (tabId: string, viewIndex: number) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
...(workbenches.length > 0
|
...(workbenches.length > 1
|
||||||
? ([
|
? ([
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -140,6 +140,10 @@ export class WebContentViewsManager {
|
|||||||
readonly tabViewsMeta$ = TabViewsMetaState.$;
|
readonly tabViewsMeta$ = TabViewsMetaState.$;
|
||||||
readonly appTabsUIReady$ = new BehaviorSubject(new Set<string>());
|
readonly appTabsUIReady$ = new BehaviorSubject(new Set<string>());
|
||||||
|
|
||||||
|
get appTabsUIReady() {
|
||||||
|
return this.appTabsUIReady$.value;
|
||||||
|
}
|
||||||
|
|
||||||
// all web views
|
// all web views
|
||||||
readonly webViewsMap$ = new BehaviorSubject(
|
readonly webViewsMap$ = new BehaviorSubject(
|
||||||
new Map<string, WebContentsView>()
|
new Map<string, WebContentsView>()
|
||||||
@@ -251,8 +255,12 @@ export class WebContentViewsManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setTabUIReady = (tabId: string) => {
|
setTabUIReady = (tabId: string) => {
|
||||||
this.appTabsUIReady$.next(new Set([...this.appTabsUIReady$.value, tabId]));
|
this.appTabsUIReady$.next(new Set([...this.appTabsUIReady, tabId]));
|
||||||
this.reorderViews();
|
this.reorderViews();
|
||||||
|
const view = this.tabViewsMap.get(tabId);
|
||||||
|
if (view) {
|
||||||
|
this.resizeView(view);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
getViewIdFromWebContentsId = (id: number) => {
|
getViewIdFromWebContentsId = (id: number) => {
|
||||||
@@ -460,10 +468,6 @@ export class WebContentViewsManager {
|
|||||||
|
|
||||||
showTab = async (id: string): Promise<WebContentsView | undefined> => {
|
showTab = async (id: string): Promise<WebContentsView | undefined> => {
|
||||||
if (this.activeWorkbenchId !== id) {
|
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({
|
this.patchTabViewsMeta({
|
||||||
activeWorkbenchId: id,
|
activeWorkbenchId: id,
|
||||||
});
|
});
|
||||||
@@ -601,25 +605,58 @@ export class WebContentViewsManager {
|
|||||||
// if tab ui of the current active view is not ready,
|
// if tab ui of the current active view is not ready,
|
||||||
// make sure shell view is on top
|
// make sure shell view is on top
|
||||||
const activeView = this.activeWorkbenchView;
|
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 getViewId = (view: View) => {
|
||||||
const getScore = (view: View) => {
|
return [...this.tabViewsMap.entries()].find(
|
||||||
if (view === this.shellView) {
|
([_, v]) => v === view
|
||||||
return 2;
|
)?.[0];
|
||||||
}
|
|
||||||
if (view === activeView) {
|
|
||||||
return ready ? 3 : 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
[...this.tabViewsMap.values()]
|
const isViewReady = (view: View) => {
|
||||||
.toSorted((a, b) => getScore(a) - getScore(b))
|
if (view === this.shellView) {
|
||||||
.forEach((view, index) => {
|
return true;
|
||||||
this.mainWindow?.contentView.addChildView(view, index);
|
}
|
||||||
|
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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -140,10 +140,8 @@ export const test = base.extend<{
|
|||||||
await use(electronApp);
|
await use(electronApp);
|
||||||
console.log('Cleaning up...');
|
console.log('Cleaning up...');
|
||||||
const pages = electronApp.windows();
|
const pages = electronApp.windows();
|
||||||
for (let i = 0; i < pages.length; i++) {
|
for (const page of pages) {
|
||||||
const page = pages[i];
|
|
||||||
await page.close();
|
await page.close();
|
||||||
console.log(`Closed page ${i + 1}/${pages.length}`);
|
|
||||||
}
|
}
|
||||||
await electronApp.close();
|
await electronApp.close();
|
||||||
await removeWithRetry(clonedDist);
|
await removeWithRetry(clonedDist);
|
||||||
|
|||||||
Reference in New Issue
Block a user