Skip to content

Commit

Permalink
WIP: Use ViewTransition API
Browse files Browse the repository at this point in the history
  • Loading branch information
raksooo committed Sep 27, 2024
1 parent 0007522 commit 501d685
Show file tree
Hide file tree
Showing 15 changed files with 184 additions and 608 deletions.
11 changes: 11 additions & 0 deletions gui/assets/css/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,14 @@ body {
transition-duration: 0ms !important;
}
}

::view-transition-image-pair(root) {
isolation: auto;
}

::view-transition-old(root),
::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
display: block;
}
48 changes: 24 additions & 24 deletions gui/src/renderer/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ import KeyboardNavigation from './components/KeyboardNavigation';
import Lang from './components/Lang';
import MacOsScrollbarDetection from './components/MacOsScrollbarDetection';
import { AppContext } from './context';
import History, { ITransitionSpecification, transitions } from './lib/history';
import History, { TransitionType } from './lib/history';
import { loadTranslations } from './lib/load-translations';
import IpcOutput from './lib/logging';
import { RoutePath } from './lib/routes';
Expand Down Expand Up @@ -399,7 +399,7 @@ export default class AppRenderer {
actions.account.loginTooManyDevices();
this.loginState = 'too many devices';

this.history.reset(RoutePath.tooManyDevices, { transition: transitions.push });
this.history.reset(RoutePath.tooManyDevices, { transition: TransitionType.push });
} catch {
log.error('Failed to fetch device list');
actions.account.loginFailed('list-devices');
Expand All @@ -416,7 +416,7 @@ export default class AppRenderer {
this.loginState = 'none';
};

public logout = async (transition = transitions.dismiss) => {
public logout = async (transition = TransitionType.dismiss) => {
try {
this.history.reset(RoutePath.login, { transition });
await IpcRendererEventChannel.account.logout();
Expand All @@ -427,7 +427,7 @@ export default class AppRenderer {
};

public leaveRevokedDevice = async () => {
await this.logout(transitions.pop);
await this.logout(TransitionType.pop);
await this.disconnectTunnel();
};

Expand Down Expand Up @@ -708,38 +708,38 @@ export default class AppRenderer {
// First level contains the possible next locations and the second level contains the
// possible current locations.
const navigationTransitions: Partial<
Record<RoutePath, Partial<Record<RoutePath | '*', ITransitionSpecification>>>
Record<RoutePath, Partial<Record<RoutePath | '*', TransitionType>>>
> = {
[RoutePath.launch]: {
[RoutePath.login]: transitions.pop,
[RoutePath.main]: transitions.pop,
'*': transitions.dismiss,
[RoutePath.login]: TransitionType.pop,
[RoutePath.main]: TransitionType.pop,
'*': TransitionType.dismiss,
},
[RoutePath.login]: {
[RoutePath.launch]: transitions.push,
[RoutePath.main]: transitions.pop,
[RoutePath.deviceRevoked]: transitions.pop,
'*': transitions.dismiss,
[RoutePath.launch]: TransitionType.push,
[RoutePath.main]: TransitionType.pop,
[RoutePath.deviceRevoked]: TransitionType.pop,
'*': TransitionType.dismiss,
},
[RoutePath.main]: {
[RoutePath.launch]: transitions.push,
[RoutePath.login]: transitions.push,
[RoutePath.tooManyDevices]: transitions.push,
'*': transitions.dismiss,
[RoutePath.launch]: TransitionType.push,
[RoutePath.login]: TransitionType.push,
[RoutePath.tooManyDevices]: TransitionType.push,
'*': TransitionType.dismiss,
},
[RoutePath.expired]: {
[RoutePath.launch]: transitions.push,
[RoutePath.login]: transitions.push,
[RoutePath.tooManyDevices]: transitions.push,
'*': transitions.dismiss,
[RoutePath.launch]: TransitionType.push,
[RoutePath.login]: TransitionType.push,
[RoutePath.tooManyDevices]: TransitionType.push,
'*': TransitionType.dismiss,
},
[RoutePath.timeAdded]: {
[RoutePath.expired]: transitions.push,
[RoutePath.redeemVoucher]: transitions.push,
'*': transitions.dismiss,
[RoutePath.expired]: TransitionType.push,
[RoutePath.redeemVoucher]: TransitionType.push,
'*': TransitionType.dismiss,
},
[RoutePath.deviceRevoked]: {
'*': transitions.pop,
'*': TransitionType.pop,
},
};

Expand Down
151 changes: 99 additions & 52 deletions gui/src/renderer/components/AppRouter.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { createRef, useCallback, useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import { flushSync } from 'react-dom';
import { Route, Switch } from 'react-router';

import SelectLocation from '../components/select-location/SelectLocationContainer';
import LoginPage from '../containers/LoginPage';
import { useAppContext } from '../context';
import { ITransitionSpecification, transitions, useHistory } from '../lib/history';
import { TransitionType, useHistory } from '../lib/history';
import { RoutePath } from '../lib/routes';
import Account from './Account';
import ApiAccessMethods from './ApiAccessMethods';
Expand All @@ -21,7 +22,6 @@ import {
} from './ExpiredAccountAddTime';
import ExpiredAccountErrorView from './ExpiredAccountErrorView';
import Filter from './Filter';
import Focus, { IFocusHandle } from './Focus';
import Launch from './Launch';
import MainView from './main-view/MainView';
import OpenVpnSettings from './OpenVpnSettings';
Expand All @@ -34,7 +34,6 @@ import Shadowsocks from './Shadowsocks';
import SplitTunnelingSettings from './SplitTunnelingSettings';
import Support from './Support';
import TooManyDevices from './TooManyDevices';
import TransitionContainer, { TransitionView } from './TransitionContainer';
import UdpOverTcp from './UdpOverTcp';
import UserInterfaceSettings from './UserInterfaceSettings';
import VpnSettings from './VpnSettings';
Expand All @@ -43,67 +42,115 @@ import WireguardSettings from './WireguardSettings';
export default function AppRouter() {
const history = useHistory();
const [currentLocation, setCurrentLocation] = useState(history.location);
const [transition, setTransition] = useState<ITransitionSpecification>(transitions.none);
const { setNavigationHistory } = useAppContext();
const focusRef = createRef<IFocusHandle>();

let unobserveHistory: () => void;

useEffect(() => {
// React throttles updates, so it's impossible to capture the intermediate navigation without
// listening to the history directly.
unobserveHistory = history.listen((location, _, transition) => {
setNavigationHistory(history.asObject);
setCurrentLocation(location);
setTransition(transition);
flushSync(() => {
document
// @ts-ignore
.startViewTransition(() => {
setNavigationHistory(history.asObject);
setCurrentLocation(location);
})
.ready.then(() => animateNavigation(transition));
});
});

return () => unobserveHistory?.();
}, []);

const onNavigation = useCallback(() => {
focusRef.current?.resetFocus();
}, []);

return (
<Focus ref={focusRef}>
<TransitionContainer onTransitionEnd={onNavigation} {...transition}>
<TransitionView routePath={history.location.pathname}>
<Switch key={currentLocation.key} location={currentLocation}>
<Route exact path={RoutePath.launch} component={Launch} />
<Route exact path={RoutePath.login} component={LoginPage} />
<Route exact path={RoutePath.tooManyDevices} component={TooManyDevices} />
<Route exact path={RoutePath.deviceRevoked} component={DeviceRevokedView} />
<Route exact path={RoutePath.main} component={MainView} />
<Route exact path={RoutePath.expired} component={ExpiredAccountErrorView} />
<Route exact path={RoutePath.redeemVoucher} component={VoucherInput} />
<Route exact path={RoutePath.voucherSuccess} component={VoucherVerificationSuccess} />
<Route exact path={RoutePath.timeAdded} component={TimeAdded} />
<Route exact path={RoutePath.setupFinished} component={SetupFinished} />
<Route exact path={RoutePath.account} component={Account} />
<Route exact path={RoutePath.settings} component={Settings} />
<Route exact path={RoutePath.selectLanguage} component={SelectLanguage} />
<Route exact path={RoutePath.userInterfaceSettings} component={UserInterfaceSettings} />
<Route exact path={RoutePath.vpnSettings} component={VpnSettings} />
<Route exact path={RoutePath.wireguardSettings} component={WireguardSettings} />
<Route exact path={RoutePath.daitaSettings} component={DaitaSettings} />
<Route exact path={RoutePath.udpOverTcp} component={UdpOverTcp} />
<Route exact path={RoutePath.shadowsocks} component={Shadowsocks} />
<Route exact path={RoutePath.openVpnSettings} component={OpenVpnSettings} />
<Route exact path={RoutePath.splitTunneling} component={SplitTunnelingSettings} />
<Route exact path={RoutePath.apiAccessMethods} component={ApiAccessMethods} />
<Route exact path={RoutePath.settingsImport} component={SettingsImport} />
<Route exact path={RoutePath.settingsTextImport} component={SettingsTextImport} />
<Route exact path={RoutePath.editApiAccessMethods} component={EditApiAccessMethod} />
<Route exact path={RoutePath.support} component={Support} />
<Route exact path={RoutePath.problemReport} component={ProblemReport} />
<Route exact path={RoutePath.debug} component={Debug} />
<Route exact path={RoutePath.selectLocation} component={SelectLocation} />
<Route exact path={RoutePath.editCustomBridge} component={EditCustomBridge} />
<Route exact path={RoutePath.filter} component={Filter} />
</Switch>
</TransitionView>
</TransitionContainer>
</Focus>
<Switch key={currentLocation.key} location={currentLocation}>
<Route exact path={RoutePath.launch} component={Launch} />
<Route exact path={RoutePath.login} component={LoginPage} />
<Route exact path={RoutePath.tooManyDevices} component={TooManyDevices} />
<Route exact path={RoutePath.deviceRevoked} component={DeviceRevokedView} />
<Route exact path={RoutePath.main} component={MainView} />
<Route exact path={RoutePath.expired} component={ExpiredAccountErrorView} />
<Route exact path={RoutePath.redeemVoucher} component={VoucherInput} />
<Route exact path={RoutePath.voucherSuccess} component={VoucherVerificationSuccess} />
<Route exact path={RoutePath.timeAdded} component={TimeAdded} />
<Route exact path={RoutePath.setupFinished} component={SetupFinished} />
<Route exact path={RoutePath.account} component={Account} />
<Route exact path={RoutePath.settings} component={Settings} />
<Route exact path={RoutePath.selectLanguage} component={SelectLanguage} />
<Route exact path={RoutePath.userInterfaceSettings} component={UserInterfaceSettings} />
<Route exact path={RoutePath.vpnSettings} component={VpnSettings} />
<Route exact path={RoutePath.wireguardSettings} component={WireguardSettings} />
<Route exact path={RoutePath.daitaSettings} component={DaitaSettings} />
<Route exact path={RoutePath.udpOverTcp} component={UdpOverTcp} />
<Route exact path={RoutePath.shadowsocks} component={Shadowsocks} />
<Route exact path={RoutePath.openVpnSettings} component={OpenVpnSettings} />
<Route exact path={RoutePath.splitTunneling} component={SplitTunnelingSettings} />
<Route exact path={RoutePath.apiAccessMethods} component={ApiAccessMethods} />
<Route exact path={RoutePath.settingsImport} component={SettingsImport} />
<Route exact path={RoutePath.settingsTextImport} component={SettingsTextImport} />
<Route exact path={RoutePath.editApiAccessMethods} component={EditApiAccessMethod} />
<Route exact path={RoutePath.support} component={Support} />
<Route exact path={RoutePath.problemReport} component={ProblemReport} />
<Route exact path={RoutePath.debug} component={Debug} />
<Route exact path={RoutePath.selectLocation} component={SelectLocation} />
<Route exact path={RoutePath.editCustomBridge} component={EditCustomBridge} />
<Route exact path={RoutePath.filter} component={Filter} />
</Switch>
);
}

function animateNavigation(transition: TransitionType) {
const oldZIndex =
transition === TransitionType.dismiss || transition === TransitionType.pop ? 100 : undefined;
document.documentElement.animate(
[
{ transform: 'translate(0%, 0%)', zIndex: oldZIndex },
{ transform: oldToTransform(transition), zIndex: oldZIndex },
],
{
duration: 450,
easing: 'ease-in-out',
pseudoElement: '::view-transition-old(root)',
},
);
document.documentElement.animate(
[{ transform: newFromTransform(transition) }, { transform: 'translate(0%, 0%)' }],
{
duration: 450,
easing: 'ease-in-out',
pseudoElement: '::view-transition-new(root)',
},
);
}

function oldToTransform(transition: TransitionType): string {
switch (transition) {
case TransitionType.show:
return 'translateY(0%)';
case TransitionType.dismiss:
return 'translateY(100%)';
case TransitionType.push:
return 'translateX(-50%)';
case TransitionType.pop:
return 'translateX(100%)';
case TransitionType.none:
return '';
}
}

function newFromTransform(transition: TransitionType): string {
switch (transition) {
case TransitionType.show:
return 'translateY(100%)';
case TransitionType.dismiss:
return 'translateY(0%)';
case TransitionType.push:
return 'translateX(100%)';
case TransitionType.pop:
return 'translateX(-50%)';
case TransitionType.none:
return '';
}
}
4 changes: 2 additions & 2 deletions gui/src/renderer/components/ExpiredAccountAddTime.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { formatRelativeDate } from '../../shared/date-helper';
import { messages } from '../../shared/gettext';
import { useAppContext } from '../context';
import useActions from '../lib/actionsHook';
import { transitions, useHistory } from '../lib/history';
import { TransitionType, useHistory } from '../lib/history';
import { generateRoutePath } from '../lib/routeHelpers';
import { RoutePath } from '../lib/routes';
import account from '../redux/account/actions';
Expand Down Expand Up @@ -283,7 +283,7 @@ function useFinishedCallback() {
accountSetupFinished();
}

history.reset(RoutePath.main, { transition: transitions.push });
history.reset(RoutePath.main, { transition: TransitionType.push });
}, [isNewAccount, accountSetupFinished, history]);

return callback;
Expand Down
76 changes: 0 additions & 76 deletions gui/src/renderer/components/Focus.tsx

This file was deleted.

Loading

0 comments on commit 501d685

Please sign in to comment.