feat: update button enhancements (#2401)

This commit is contained in:
Peng Xiao
2023-05-17 16:58:14 +08:00
committed by GitHub
parent 1498ee405b
commit 2e0ccb53ec
13 changed files with 427 additions and 125 deletions

View File

@@ -13,11 +13,12 @@ export const root = style({
cursor: 'pointer',
padding: '0 12px',
position: 'relative',
transition: 'all 0.3s ease',
selectors: {
'&:hover': {
background: 'var(--affine-hover-color)',
background: 'var(--affine-white-60)',
},
'&:before': {
'&[data-has-update="true"]:before': {
content: "''",
position: 'absolute',
top: '-3px',
@@ -30,6 +31,9 @@ export const root = style({
opacity: 1,
transition: '0.3s ease',
},
'&[data-disabled="true"]': {
pointerEvents: 'none',
},
},
vars: {
'--svg-dot-animation': `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 122 116'%3E%3Cpath id='b' stroke='%23fff' stroke-linecap='round' stroke-width='0' d='M17.9256 115C17.434 111.774 13.1701 104.086 13.4282 95.6465C13.6862 87.207 18.6628 76.0721 17.9256 64.3628C17.1883 52.6535 8.7772 35.9512 9.00452 25.3907C9.23185 14.8302 16.2114 5.06512 17.9256 1'/%3E%3Cpath id='d' stroke='%23fff' stroke-linecap='round' stroke-width='0' d='M84.1628 115C85.2376 112.055 94.5618 98.8394 93.9975 91.1338C93.4332 83.4281 82.5505 73.2615 84.1628 62.5704C85.775 51.8793 96.4803 35.4248 95.9832 25.7826C95.4861 16.1404 87.9113 4.71163 84.1628 1'/%3E%3Cpath id='f' stroke='%23fff' stroke-linecap='round' stroke-width='0' d='M37.0913 115C37.9604 111.921 44.4347 99.4545 45.3816 92.9773C48.9305 68.7011 35.7877 73.9552 37.0913 62.7781C38.3949 51.6011 47.3889 36.9895 46.9869 26.9091C46.585 16.8286 40.1222 4.88034 37.0913 1'/%3E%3Cpath id='h' stroke='%23fff' stroke-linecap='round' stroke-width='0' d='M112.443 115C111.698 112.235 108.25 106.542 107.715 93.7582C107.241 82.4286 107.229 83.9543 112.443 66.1429C116.085 44.0408 100.661 42.5908 101.006 33.539C101.35 24.4871 109.843 4.48439 112.443 1'/%3E%3Cg%3E%3Ccircle r='1.5' fill='rgba(96, 70, 254, 0.3)'%3E%3CanimateMotion dur='10s' repeatCount='indefinite'%3E%3Cmpath href='%23b' /%3E%3C/animateMotion%3E%3C/circle%3E%3C/g%3E%3Cg%3E%3Ccircle r='1' fill='rgba(96, 70, 254, 0.3)' fill-opacity='1' shape-rendering='crispEdges'%3E%3CanimateMotion dur='8s' repeatCount='indefinite'%3E%3Cmpath href='%23d' /%3E%3C/animateMotion%3E%3C/circle%3E%3C/g%3E%3Cg%3E%3Ccircle r='.5' fill='rgba(96, 70, 254, 0.3)' fill-opacity='1' shape-rendering='crispEdges'%3E%3CanimateMotion dur='4s' repeatCount='indefinite'%3E%3Cmpath href='%23f' /%3E%3C/animateMotion%3E%3C/circle%3E%3C/g%3E%3Cg%3E%3Ccircle r='.8' fill='rgba(96, 70, 254, 0.3)' fill-opacity='1' shape-rendering='crispEdges'%3E%3CanimateMotion dur='6s' repeatCount='indefinite'%3E%3Cmpath href='%23h' /%3E%3C/animateMotion%3E%3C/circle%3E%3C/g%3E%3C/svg%3E")`,
@@ -42,39 +46,35 @@ export const icon = style({
fontSize: '24px',
});
export const particles = style({
background: `var(--svg-dot-animation), var(--svg-dot-animation)`,
backgroundRepeat: 'no-repeat, repeat',
backgroundPosition: 'center, center top 100%',
backgroundSize: '100%, 130%',
WebkitMaskImage:
'linear-gradient(to top, transparent, black, black, transparent)',
width: '100%',
height: '100%',
export const closeIcon = style({
position: 'absolute',
left: 0,
});
export const particlesBefore = style({
content: '""',
display: 'block',
position: 'absolute',
width: '100%',
height: '100%',
background: `var(--svg-dot-animation), var(--svg-dot-animation), var(--svg-dot-animation)`,
backgroundRepeat: 'no-repeat, repeat, repeat',
backgroundPosition: 'center, center top 100%, center center',
backgroundSize: '100% 120%, 150%, 120%',
filter: 'blur(1px)',
willChange: 'filter',
top: '4px',
right: '4px',
height: '14px',
width: '14px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: 'var(--affine-shadow-1)',
color: 'var(--affine-text-secondary-color)',
backgroundColor: 'var(--affine-background-primary-color)',
fontSize: '14px',
cursor: 'pointer',
transition: '0.1s',
borderRadius: '50%',
zIndex: 1,
selectors: {
'&:hover': {
transform: 'scale(1.1)',
},
},
});
export const installLabel = style({
display: 'flex',
justifyContent: 'flex-start',
alignItems: 'center',
width: '100%',
height: '100%',
flex: 1,
fontSize: 'var(--affine-font-sm)',
whiteSpace: 'nowrap',
});
@@ -82,6 +82,7 @@ export const installLabel = style({
export const installLabelNormal = style([
installLabel,
{
justifyContent: 'space-between',
selectors: {
[`${root}:hover &`]: {
display: 'none',
@@ -102,31 +103,111 @@ export const installLabelHover = style([
},
]);
export const halo = style({
overflow: 'hidden',
export const updateAvailableWrapper = style({
display: 'flex',
flexDirection: 'column',
position: 'relative',
width: '100%',
height: '100%',
padding: '8px 0',
});
export const versionLabel = style({
padding: '0 6px',
color: 'var(--affine-text-secondary-color)',
background: 'var(--affine-background-primary-color)',
fontSize: '10px',
lineHeight: '18px',
borderRadius: '4px',
});
export const whatsNewLabel = style({
display: 'flex',
alignItems: 'center',
width: '100%',
height: '100%',
fontSize: 'var(--affine-font-sm)',
whiteSpace: 'nowrap',
});
export const progress = style({
position: 'relative',
width: '100%',
height: '4px',
borderRadius: '12px',
background: 'var(--affine-black-10)',
});
export const progressInner = style({
position: 'absolute',
top: 0,
left: 0,
height: '100%',
borderRadius: '12px',
background: 'var(--affine-primary-color)',
transition: '0.1s',
});
export const particles = style({
background: `var(--svg-dot-animation), var(--svg-dot-animation)`,
backgroundRepeat: 'no-repeat, repeat',
backgroundPosition: 'center, center top 100%',
backgroundSize: '100%, 130%',
WebkitMaskImage:
'linear-gradient(to top, transparent, black, black, transparent)',
width: '100%',
height: '100%',
position: 'absolute',
left: 0,
pointerEvents: 'none',
});
export const particlesBefore = style({
content: '""',
display: 'block',
position: 'absolute',
width: '100%',
height: '100%',
background: `var(--svg-dot-animation), var(--svg-dot-animation), var(--svg-dot-animation)`,
backgroundRepeat: 'no-repeat, repeat, repeat',
backgroundPosition: 'center, center top 100%, center center',
backgroundSize: '100% 120%, 150%, 120%',
filter: 'blur(1px)',
willChange: 'filter',
pointerEvents: 'none',
});
export const halo = style({
overflow: 'hidden',
position: 'absolute',
inset: 0,
':before': {
content: '""',
display: 'block',
width: '60%',
height: '40%',
inset: 0,
position: 'absolute',
top: '80%',
left: '50%',
background:
'linear-gradient(180deg, rgba(50, 26, 206, 0.1) 10%, rgba(50, 26, 206, 0.35) 30%, rgba(84, 56, 255, 1) 50%)',
filter: 'blur(10px) saturate(1.2)',
transform: 'translateX(-50%) translateY(calc(0 * 1%)) scale(0)',
transition: '0.3s ease',
willChange: 'filter',
willChange: 'filter, transform',
transform: 'translateY(100%) scale(0.6)',
background:
'radial-gradient(ellipse 60% 80% at bottom, rgba(50, 26, 206, 0.35), transparent)',
},
':after': {
content: '""',
display: 'block',
inset: 0,
position: 'absolute',
filter: 'blur(10px) saturate(1.2)',
transition: '0.1s ease',
willChange: 'filter, transform',
transform: 'translateY(100%) scale(0.6)',
background:
'radial-gradient(ellipse 30% 45% at bottom, rgba(50, 26, 206, 0.6), transparent)',
},
selectors: {
'&:hover:before': {
transform: 'translateX(-50%) translateY(calc(-70 * 1%)) scale(1)',
'&:hover:before, &:hover:after': {
transform: 'translateY(0) scale(1)',
},
},
});

View File

@@ -0,0 +1,71 @@
import { getEnvironment } from '@affine/env/config';
import { atomWithObservable, atomWithStorage } from 'jotai/utils';
import { Observable } from 'rxjs';
// todo: move to utils?
function rpcToObservable<
T,
H extends () => Promise<T>,
E extends (callback: (t: T) => void) => () => void
>(
initialValue: T,
{
event,
handler,
onSubscribe,
}: {
event?: E;
handler?: H;
onSubscribe?: () => void;
}
) {
return new Observable<T>(subscriber => {
subscriber.next(initialValue);
const environment = getEnvironment();
onSubscribe?.();
if (typeof window === 'undefined' || !environment.isDesktop || !event) {
subscriber.complete();
return () => {};
}
handler?.().then(t => {
subscriber.next(t);
});
return event(t => {
subscriber.next(t);
});
});
}
type InferTFromEvent<E> = E extends (
callback: (t: infer T) => void
) => () => void
? T
: never;
type UpdateMeta = InferTFromEvent<typeof window.events.updater.onUpdateReady>;
export const updateReadyAtom = atomWithObservable(() => {
return rpcToObservable(null as UpdateMeta | null, {
event: window.events?.updater.onUpdateReady,
});
});
export const updateAvailableAtom = atomWithObservable(() => {
return rpcToObservable(null as UpdateMeta | null, {
event: window.events?.updater.onUpdateAvailable,
onSubscribe: () => {
window.apis?.updater.checkForUpdatesAndNotify();
},
});
});
export const downloadProgressAtom = atomWithObservable<number>(() => {
return rpcToObservable(0, {
event: window.events?.updater.onDownloadProgress,
});
});
export const changelogCheckedAtom = atomWithStorage<Record<string, boolean>>(
'affine:client-changelog-checked',
{}
);

View File

@@ -1,16 +0,0 @@
import type { Meta, StoryFn } from '@storybook/react';
import { AppUpdaterButton } from '.';
export default {
title: 'Components/AppSidebar/AppUpdaterButton',
component: AppUpdaterButton,
} satisfies Meta;
export const Default: StoryFn = () => {
return (
<main style={{ width: '240px' }}>
<AppUpdaterButton />
</main>
);
};

View File

@@ -1,35 +1,167 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ResetIcon } from '@blocksuite/icons';
import { CloseIcon, NewIcon, ResetIcon } from '@blocksuite/icons';
import clsx from 'clsx';
import { atom, useAtomValue, useSetAtom } from 'jotai';
import { startTransition } from 'react';
import * as styles from './index.css';
import {
changelogCheckedAtom,
downloadProgressAtom,
updateAvailableAtom,
updateReadyAtom,
} from './index.jotai';
interface AddPageButtonProps {
className?: string;
style?: React.CSSProperties;
}
const currentVersionAtom = atom(async () => {
if (typeof window === 'undefined') {
return null;
}
const currentVersion = await window.apis?.updater.currentVersion();
return currentVersion;
});
const currentChangelogUnreadAtom = atom(async get => {
if (typeof window === 'undefined') {
return false;
}
const mapping = get(changelogCheckedAtom);
const currentVersion = await get(currentVersionAtom);
if (currentVersion) {
return !mapping[currentVersion];
}
return false;
});
// Although it is called an input, it is actually a button.
export function AppUpdaterButton({ className, style }: AddPageButtonProps) {
const t = useAFFiNEI18N();
const currentChangelogUnread = useAtomValue(currentChangelogUnreadAtom);
const updateReady = useAtomValue(updateReadyAtom);
const updateAvailable = useAtomValue(updateAvailableAtom);
const currentVersion = useAtomValue(currentVersionAtom);
const downloadProgress = useAtomValue(downloadProgressAtom);
const onReadOrDismissChangelog = useSetAtom(changelogCheckedAtom);
const onReadOrDismissCurrentChangelog = (visit: boolean) => {
if (visit) {
window.open(
`https://github.com/toeverything/AFFiNE/releases/tag/v${currentVersion}`,
'_blank'
);
}
startTransition(() =>
onReadOrDismissChangelog(mapping => {
return {
...mapping,
[currentVersion!]: true,
};
})
);
};
if (!updateAvailable && !currentChangelogUnread) {
return null;
}
return (
<button
style={style}
className={clsx([styles.root, className])}
data-has-update={updateAvailable ? 'true' : 'false'}
data-disabled={updateAvailable?.allowAutoUpdate && !updateReady}
onClick={() => {
window.apis?.updater.updateClient();
if (updateReady) {
window.apis?.updater.quitAndInstall();
} else if (updateAvailable?.allowAutoUpdate) {
// wait for download to finish
} else if (updateAvailable || currentChangelogUnread) {
onReadOrDismissCurrentChangelog(true);
}
}}
>
{updateAvailable &&
(updateAvailable.allowAutoUpdate
? renderUpdateAvailableAllowAutoUpdate()
: renderUpdateAvailableNotAllowAutoUpdate())}
{!updateAvailable && currentChangelogUnread && renderWhatsNew()}
<div className={styles.particles} aria-hidden="true"></div>
<span className={styles.halo} aria-hidden="true"></span>
<div className={clsx([styles.installLabelNormal])}>
<span>{t['Update Available']()}</span>
</div>
<div className={clsx([styles.installLabelHover])}>
<ResetIcon className={styles.icon} />
<span>{t['Restart Install Client Update']()}</span>
</div>
</button>
);
function renderUpdateAvailableAllowAutoUpdate() {
return (
<div className={clsx([styles.updateAvailableWrapper])}>
<div className={clsx([styles.installLabelNormal])}>
<span>
{!updateReady
? t['com.affine.updater.downloading']()
: t['com.affine.updater.update-available']()}
</span>
<span className={styles.versionLabel}>
{updateAvailable?.version}
</span>
</div>
{updateReady ? (
<div className={clsx([styles.installLabelHover])}>
<ResetIcon className={styles.icon} />
<span>{t['com.affine.updater.restart-to-update']()}</span>
</div>
) : (
<div className={styles.progress}>
<div
className={styles.progressInner}
style={{ width: `${downloadProgress}%` }}
></div>
</div>
)}
</div>
);
}
function renderUpdateAvailableNotAllowAutoUpdate() {
return (
<>
<div className={clsx([styles.installLabelNormal])}>
<span>{t['com.affine.updater.update-available']()}</span>
<span className={styles.versionLabel}>
{updateAvailable?.version}
</span>
</div>
<div className={clsx([styles.installLabelHover])}>
<span>{t['com.affine.updater.open-download-page']()}</span>
</div>
</>
);
}
function renderWhatsNew() {
return (
<>
<div className={clsx([styles.whatsNewLabel])}>
<NewIcon className={styles.icon} />
<span>{t[`Discover what's new!`]()}</span>
</div>
<div
className={styles.closeIcon}
onClick={e => {
onReadOrDismissCurrentChangelog(false);
e.stopPropagation();
}}
>
<CloseIcon />
</div>
</>
);
}
}

View File

@@ -1,7 +1,5 @@
import { atom } from 'jotai';
import { atomWithObservable } from 'jotai/utils';
import { atomWithStorage } from 'jotai/utils';
import { Observable } from 'rxjs';
export const APP_SIDEBAR_OPEN = 'app-sidebar-open';
export const appSidebarOpenAtom = atomWithStorage(
@@ -15,20 +13,3 @@ export const appSidebarWidthAtom = atomWithStorage(
'app-sidebar-width',
256 /* px */
);
export const updateAvailableAtom = atomWithObservable<boolean>(() => {
return new Observable<boolean>(subscriber => {
subscriber.next(false);
if (typeof window !== 'undefined') {
const isMacosDesktop = environment.isDesktop && environment.isMacOs;
if (isMacosDesktop) {
const dispose = window.events?.updater.onClientUpdateReady(() => {
subscriber.next(true);
});
return () => {
dispose?.();
};
}
}
});
});

View File

@@ -18,7 +18,6 @@ import {
appSidebarOpenAtom,
appSidebarResizingAtom,
appSidebarWidthAtom,
updateAvailableAtom,
} from './index.jotai';
import { ResizeIndicator } from './resize-indicator';
import type { SidebarHeaderProps } from './sidebar-header';
@@ -122,9 +121,4 @@ export { AppSidebarFallback } from './fallback';
export * from './menu-item';
export * from './quick-search-input';
export * from './sidebar-containers';
export {
appSidebarFloatingAtom,
appSidebarOpenAtom,
appSidebarResizingAtom,
updateAvailableAtom,
};
export { appSidebarFloatingAtom, appSidebarOpenAtom, appSidebarResizingAtom };