mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-02 01:49:51 +08:00
feat: update button enhancements (#2401)
This commit is contained in:
@@ -2,6 +2,7 @@ import { app, Menu } from 'electron';
|
|||||||
|
|
||||||
import { isMacOS } from '../../utils';
|
import { isMacOS } from '../../utils';
|
||||||
import { subjects } from './events';
|
import { subjects } from './events';
|
||||||
|
import { checkForUpdatesAndNotify } from './handlers/updater';
|
||||||
|
|
||||||
// Unique id for menuitems
|
// Unique id for menuitems
|
||||||
const MENUITEM_NEW_PAGE = 'affine:new-page';
|
const MENUITEM_NEW_PAGE = 'affine:new-page';
|
||||||
@@ -114,6 +115,12 @@ export function createApplicationMenu() {
|
|||||||
await shell.openExternal('https://affine.pro/');
|
await shell.openExternal('https://affine.pro/');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Check for Updates',
|
||||||
|
click: async () => {
|
||||||
|
await checkForUpdatesAndNotify(true);
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,19 +1,34 @@
|
|||||||
import { Subject } from 'rxjs';
|
import { BehaviorSubject, Subject } from 'rxjs';
|
||||||
|
|
||||||
import type { MainEventListener } from './type';
|
import type { MainEventListener } from './type';
|
||||||
|
|
||||||
interface UpdateMeta {
|
interface UpdateMeta {
|
||||||
version: string;
|
version: string;
|
||||||
|
allowAutoUpdate: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const updaterSubjects = {
|
export const updaterSubjects = {
|
||||||
// means it is ready for restart and install the new version
|
// means it is ready for restart and install the new version
|
||||||
clientUpdateReady: new Subject<UpdateMeta>(),
|
updateAvailable: new Subject<UpdateMeta>(),
|
||||||
|
updateReady: new Subject<UpdateMeta>(),
|
||||||
|
downloadProgress: new BehaviorSubject<number>(0),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updaterEvents = {
|
export const updaterEvents = {
|
||||||
onClientUpdateReady: (fn: (versionMeta: UpdateMeta) => void) => {
|
onUpdateAvailable: (fn: (versionMeta: UpdateMeta) => void) => {
|
||||||
const sub = updaterSubjects.clientUpdateReady.subscribe(fn);
|
const sub = updaterSubjects.updateAvailable.subscribe(fn);
|
||||||
|
return () => {
|
||||||
|
sub.unsubscribe();
|
||||||
|
};
|
||||||
|
},
|
||||||
|
onUpdateReady: (fn: (versionMeta: UpdateMeta) => void) => {
|
||||||
|
const sub = updaterSubjects.updateReady.subscribe(fn);
|
||||||
|
return () => {
|
||||||
|
sub.unsubscribe();
|
||||||
|
};
|
||||||
|
},
|
||||||
|
onDownloadProgress: (fn: (progress: number) => void) => {
|
||||||
|
const sub = updaterSubjects.downloadProgress.subscribe(fn);
|
||||||
return () => {
|
return () => {
|
||||||
sub.unsubscribe();
|
sub.unsubscribe();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
|
import { app } from 'electron';
|
||||||
|
|
||||||
import type { NamespaceHandlers } from '../type';
|
import type { NamespaceHandlers } from '../type';
|
||||||
|
import { checkForUpdatesAndNotify, quitAndInstall } from './updater';
|
||||||
|
|
||||||
export const updaterHandlers = {
|
export const updaterHandlers = {
|
||||||
updateClient: async () => {
|
currentVersion: async () => {
|
||||||
const { updateClient } = await import('./updater');
|
return app.getVersion();
|
||||||
return updateClient();
|
},
|
||||||
|
quitAndInstall: async () => {
|
||||||
|
return quitAndInstall();
|
||||||
|
},
|
||||||
|
checkForUpdatesAndNotify: async () => {
|
||||||
|
return checkForUpdatesAndNotify(true);
|
||||||
},
|
},
|
||||||
} satisfies NamespaceHandlers;
|
} satisfies NamespaceHandlers;
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { app } from 'electron';
|
||||||
import type { AppUpdater } from 'electron-updater';
|
import type { AppUpdater } from 'electron-updater';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
@@ -21,10 +22,22 @@ const isDev = mode === 'development';
|
|||||||
|
|
||||||
let _autoUpdater: AppUpdater | null = null;
|
let _autoUpdater: AppUpdater | null = null;
|
||||||
|
|
||||||
export const updateClient = async () => {
|
export const quitAndInstall = async () => {
|
||||||
_autoUpdater?.quitAndInstall();
|
_autoUpdater?.quitAndInstall();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let lastCheckTime = 0;
|
||||||
|
export const checkForUpdatesAndNotify = async (force = true) => {
|
||||||
|
if (!_autoUpdater) {
|
||||||
|
return; // ?
|
||||||
|
}
|
||||||
|
// check every 30 minutes (1800 seconds) at most
|
||||||
|
if (force || lastCheckTime + 1000 * 1800 < Date.now()) {
|
||||||
|
lastCheckTime = Date.now();
|
||||||
|
return _autoUpdater.checkForUpdatesAndNotify();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const registerUpdater = async () => {
|
export const registerUpdater = async () => {
|
||||||
// require it will cause some side effects and will break generate-main-exposed-meta,
|
// require it will cause some side effects and will break generate-main-exposed-meta,
|
||||||
// so we wrap it in a function
|
// so we wrap it in a function
|
||||||
@@ -37,6 +50,9 @@ export const registerUpdater = async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: support auto update on windows and linux
|
||||||
|
const allowAutoUpdate = isMacOS();
|
||||||
|
|
||||||
_autoUpdater.autoDownload = false;
|
_autoUpdater.autoDownload = false;
|
||||||
_autoUpdater.allowPrerelease = buildType !== 'stable';
|
_autoUpdater.allowPrerelease = buildType !== 'stable';
|
||||||
_autoUpdater.autoInstallOnAppQuit = false;
|
_autoUpdater.autoInstallOnAppQuit = false;
|
||||||
@@ -49,24 +65,36 @@ export const registerUpdater = async () => {
|
|||||||
releaseType: buildType === 'stable' ? 'release' : 'prerelease',
|
releaseType: buildType === 'stable' ? 'release' : 'prerelease',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isMacOS()) {
|
// register events for checkForUpdatesAndNotify
|
||||||
_autoUpdater.on('update-available', () => {
|
_autoUpdater.on('update-available', info => {
|
||||||
|
if (allowAutoUpdate) {
|
||||||
_autoUpdater!.downloadUpdate();
|
_autoUpdater!.downloadUpdate();
|
||||||
logger.info('Update available, downloading...');
|
logger.info('Update available, downloading...', info);
|
||||||
|
}
|
||||||
|
updaterSubjects.updateAvailable.next({
|
||||||
|
version: info.version,
|
||||||
|
allowAutoUpdate,
|
||||||
});
|
});
|
||||||
_autoUpdater.on('download-progress', e => {
|
});
|
||||||
logger.info(`Download progress: ${e.percent}`);
|
_autoUpdater.on('download-progress', e => {
|
||||||
|
logger.info(`Download progress: ${e.percent}`);
|
||||||
|
updaterSubjects.downloadProgress.next(e.percent);
|
||||||
|
});
|
||||||
|
_autoUpdater.on('update-downloaded', e => {
|
||||||
|
updaterSubjects.updateReady.next({
|
||||||
|
version: e.version,
|
||||||
|
allowAutoUpdate,
|
||||||
});
|
});
|
||||||
_autoUpdater.on('update-downloaded', e => {
|
// I guess we can skip it?
|
||||||
updaterSubjects.clientUpdateReady.next({
|
// updaterSubjects.clientDownloadProgress.next(100);
|
||||||
version: e.version,
|
logger.info('Update downloaded, ready to install');
|
||||||
});
|
});
|
||||||
logger.info('Update downloaded, ready to install');
|
_autoUpdater.on('error', e => {
|
||||||
});
|
logger.error('Error while updating client', e);
|
||||||
_autoUpdater.on('error', e => {
|
});
|
||||||
logger.error('Error while updating client', e);
|
_autoUpdater.forceDevUpdateConfig = isDev;
|
||||||
});
|
|
||||||
_autoUpdater.forceDevUpdateConfig = isDev;
|
app.on('activate', async () => {
|
||||||
await _autoUpdater.checkForUpdatesAndNotify();
|
await checkForUpdatesAndNotify(false);
|
||||||
}
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/consistent-type-imports */
|
/* eslint-disable @typescript-eslint/consistent-type-imports */
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
apis?: typeof import('./src/affine-apis').apis;
|
apis: typeof import('./src/affine-apis').apis;
|
||||||
events?: typeof import('./src/affine-apis').events;
|
events: typeof import('./src/affine-apis').events;
|
||||||
appInfo?: typeof import('./src/affine-apis').appInfo;
|
appInfo: typeof import('./src/affine-apis').appInfo;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
QuickSearchInput,
|
QuickSearchInput,
|
||||||
SidebarContainer,
|
SidebarContainer,
|
||||||
SidebarScrollableContainer,
|
SidebarScrollableContainer,
|
||||||
updateAvailableAtom,
|
|
||||||
} from '@affine/component/app-sidebar';
|
} from '@affine/component/app-sidebar';
|
||||||
import { config } from '@affine/env';
|
import { config } from '@affine/env';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
@@ -20,7 +19,7 @@ import {
|
|||||||
ShareIcon,
|
ShareIcon,
|
||||||
} from '@blocksuite/icons';
|
} from '@blocksuite/icons';
|
||||||
import type { Page } from '@blocksuite/store';
|
import type { Page } from '@blocksuite/store';
|
||||||
import { useAtom, useAtomValue } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
import type { ReactElement } from 'react';
|
import type { ReactElement } from 'react';
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { useCallback, useEffect, useMemo } from 'react';
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
@@ -114,7 +113,6 @@ export const RootAppSidebar = ({
|
|||||||
document.removeEventListener('keydown', keydown, { capture: true });
|
document.removeEventListener('keydown', keydown, { capture: true });
|
||||||
}, [sidebarOpen, setSidebarOpen]);
|
}, [sidebarOpen, setSidebarOpen]);
|
||||||
|
|
||||||
const clientUpdateAvailable = useAtomValue(updateAvailableAtom);
|
|
||||||
const [history, setHistory] = useHistoryAtom();
|
const [history, setHistory] = useHistoryAtom();
|
||||||
const router = useMemo(() => {
|
const router = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
@@ -192,7 +190,8 @@ export const RootAppSidebar = ({
|
|||||||
</RouteMenuLinkItem>
|
</RouteMenuLinkItem>
|
||||||
</SidebarScrollableContainer>
|
</SidebarScrollableContainer>
|
||||||
<SidebarContainer>
|
<SidebarContainer>
|
||||||
{clientUpdateAvailable && <AppUpdaterButton />}
|
{environment.isDesktop && <AppUpdaterButton />}
|
||||||
|
<div />
|
||||||
<AddPageButton onClick={onClickNewPage} />
|
<AddPageButton onClick={onClickNewPage} />
|
||||||
</SidebarContainer>
|
</SidebarContainer>
|
||||||
</AppSidebar>
|
</AppSidebar>
|
||||||
|
|||||||
@@ -13,11 +13,12 @@ export const root = style({
|
|||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
padding: '0 12px',
|
padding: '0 12px',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
selectors: {
|
selectors: {
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
background: 'var(--affine-hover-color)',
|
background: 'var(--affine-white-60)',
|
||||||
},
|
},
|
||||||
'&:before': {
|
'&[data-has-update="true"]:before': {
|
||||||
content: "''",
|
content: "''",
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: '-3px',
|
top: '-3px',
|
||||||
@@ -30,6 +31,9 @@ export const root = style({
|
|||||||
opacity: 1,
|
opacity: 1,
|
||||||
transition: '0.3s ease',
|
transition: '0.3s ease',
|
||||||
},
|
},
|
||||||
|
'&[data-disabled="true"]': {
|
||||||
|
pointerEvents: 'none',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
vars: {
|
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")`,
|
'--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',
|
fontSize: '24px',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const particles = style({
|
export const closeIcon = 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',
|
position: 'absolute',
|
||||||
left: 0,
|
top: '4px',
|
||||||
});
|
right: '4px',
|
||||||
|
height: '14px',
|
||||||
export const particlesBefore = style({
|
width: '14px',
|
||||||
content: '""',
|
display: 'flex',
|
||||||
display: 'block',
|
alignItems: 'center',
|
||||||
position: 'absolute',
|
justifyContent: 'center',
|
||||||
width: '100%',
|
boxShadow: 'var(--affine-shadow-1)',
|
||||||
height: '100%',
|
color: 'var(--affine-text-secondary-color)',
|
||||||
background: `var(--svg-dot-animation), var(--svg-dot-animation), var(--svg-dot-animation)`,
|
backgroundColor: 'var(--affine-background-primary-color)',
|
||||||
backgroundRepeat: 'no-repeat, repeat, repeat',
|
fontSize: '14px',
|
||||||
backgroundPosition: 'center, center top 100%, center center',
|
cursor: 'pointer',
|
||||||
backgroundSize: '100% 120%, 150%, 120%',
|
transition: '0.1s',
|
||||||
filter: 'blur(1px)',
|
borderRadius: '50%',
|
||||||
willChange: 'filter',
|
zIndex: 1,
|
||||||
|
selectors: {
|
||||||
|
'&:hover': {
|
||||||
|
transform: 'scale(1.1)',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const installLabel = style({
|
export const installLabel = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'flex-start',
|
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
flex: 1,
|
||||||
fontSize: 'var(--affine-font-sm)',
|
fontSize: 'var(--affine-font-sm)',
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
});
|
});
|
||||||
@@ -82,6 +82,7 @@ export const installLabel = style({
|
|||||||
export const installLabelNormal = style([
|
export const installLabelNormal = style([
|
||||||
installLabel,
|
installLabel,
|
||||||
{
|
{
|
||||||
|
justifyContent: 'space-between',
|
||||||
selectors: {
|
selectors: {
|
||||||
[`${root}:hover &`]: {
|
[`${root}:hover &`]: {
|
||||||
display: 'none',
|
display: 'none',
|
||||||
@@ -102,31 +103,111 @@ export const installLabelHover = style([
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const halo = style({
|
export const updateAvailableWrapper = style({
|
||||||
overflow: 'hidden',
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
position: 'relative',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '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',
|
position: 'absolute',
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 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': {
|
':before': {
|
||||||
content: '""',
|
content: '""',
|
||||||
display: 'block',
|
display: 'block',
|
||||||
width: '60%',
|
inset: 0,
|
||||||
height: '40%',
|
|
||||||
position: 'absolute',
|
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)',
|
filter: 'blur(10px) saturate(1.2)',
|
||||||
transform: 'translateX(-50%) translateY(calc(0 * 1%)) scale(0)',
|
|
||||||
transition: '0.3s ease',
|
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: {
|
selectors: {
|
||||||
'&:hover:before': {
|
'&:hover:before, &:hover:after': {
|
||||||
transform: 'translateX(-50%) translateY(calc(-70 * 1%)) scale(1)',
|
transform: 'translateY(0) scale(1)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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',
|
||||||
|
{}
|
||||||
|
);
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,35 +1,167 @@
|
|||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { ResetIcon } from '@blocksuite/icons';
|
import { CloseIcon, NewIcon, ResetIcon } from '@blocksuite/icons';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
import { atom, useAtomValue, useSetAtom } from 'jotai';
|
||||||
|
import { startTransition } from 'react';
|
||||||
|
|
||||||
import * as styles from './index.css';
|
import * as styles from './index.css';
|
||||||
|
import {
|
||||||
|
changelogCheckedAtom,
|
||||||
|
downloadProgressAtom,
|
||||||
|
updateAvailableAtom,
|
||||||
|
updateReadyAtom,
|
||||||
|
} from './index.jotai';
|
||||||
|
|
||||||
interface AddPageButtonProps {
|
interface AddPageButtonProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
style?: React.CSSProperties;
|
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.
|
// Although it is called an input, it is actually a button.
|
||||||
export function AppUpdaterButton({ className, style }: AddPageButtonProps) {
|
export function AppUpdaterButton({ className, style }: AddPageButtonProps) {
|
||||||
const t = useAFFiNEI18N();
|
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 (
|
return (
|
||||||
<button
|
<button
|
||||||
style={style}
|
style={style}
|
||||||
className={clsx([styles.root, className])}
|
className={clsx([styles.root, className])}
|
||||||
|
data-has-update={updateAvailable ? 'true' : 'false'}
|
||||||
|
data-disabled={updateAvailable?.allowAutoUpdate && !updateReady}
|
||||||
onClick={() => {
|
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>
|
<div className={styles.particles} aria-hidden="true"></div>
|
||||||
<span className={styles.halo} aria-hidden="true"></span>
|
<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>
|
</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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { atom } from 'jotai';
|
import { atom } from 'jotai';
|
||||||
import { atomWithObservable } from 'jotai/utils';
|
|
||||||
import { atomWithStorage } from 'jotai/utils';
|
import { atomWithStorage } from 'jotai/utils';
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
|
|
||||||
export const APP_SIDEBAR_OPEN = 'app-sidebar-open';
|
export const APP_SIDEBAR_OPEN = 'app-sidebar-open';
|
||||||
export const appSidebarOpenAtom = atomWithStorage(
|
export const appSidebarOpenAtom = atomWithStorage(
|
||||||
@@ -15,20 +13,3 @@ export const appSidebarWidthAtom = atomWithStorage(
|
|||||||
'app-sidebar-width',
|
'app-sidebar-width',
|
||||||
256 /* px */
|
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?.();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import {
|
|||||||
appSidebarOpenAtom,
|
appSidebarOpenAtom,
|
||||||
appSidebarResizingAtom,
|
appSidebarResizingAtom,
|
||||||
appSidebarWidthAtom,
|
appSidebarWidthAtom,
|
||||||
updateAvailableAtom,
|
|
||||||
} from './index.jotai';
|
} from './index.jotai';
|
||||||
import { ResizeIndicator } from './resize-indicator';
|
import { ResizeIndicator } from './resize-indicator';
|
||||||
import type { SidebarHeaderProps } from './sidebar-header';
|
import type { SidebarHeaderProps } from './sidebar-header';
|
||||||
@@ -122,9 +121,4 @@ export { AppSidebarFallback } from './fallback';
|
|||||||
export * from './menu-item';
|
export * from './menu-item';
|
||||||
export * from './quick-search-input';
|
export * from './quick-search-input';
|
||||||
export * from './sidebar-containers';
|
export * from './sidebar-containers';
|
||||||
export {
|
export { appSidebarFloatingAtom, appSidebarOpenAtom, appSidebarResizingAtom };
|
||||||
appSidebarFloatingAtom,
|
|
||||||
appSidebarOpenAtom,
|
|
||||||
appSidebarResizingAtom,
|
|
||||||
updateAvailableAtom,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -231,7 +231,6 @@
|
|||||||
"Synced with AFFiNE Cloud": "Synced with AFFiNE Cloud",
|
"Synced with AFFiNE Cloud": "Synced with AFFiNE Cloud",
|
||||||
"Recent": "Recent",
|
"Recent": "Recent",
|
||||||
"Successfully deleted": "Successfully deleted",
|
"Successfully deleted": "Successfully deleted",
|
||||||
"Restart Install Client Update": "Restart to install update",
|
|
||||||
"Add Workspace": "Add Workspace",
|
"Add Workspace": "Add Workspace",
|
||||||
"Add Workspace Hint": "Select where you already have",
|
"Add Workspace Hint": "Select where you already have",
|
||||||
"Export success": "Export success",
|
"Export success": "Export success",
|
||||||
@@ -271,7 +270,10 @@
|
|||||||
"com.affine.onboarding.videoDescription2": "Create structured documents with ease, using a modular interface to drag and drop blocks of text, images, and other content.",
|
"com.affine.onboarding.videoDescription2": "Create structured documents with ease, using a modular interface to drag and drop blocks of text, images, and other content.",
|
||||||
"FILE_ALREADY_EXISTS": "File already exists",
|
"FILE_ALREADY_EXISTS": "File already exists",
|
||||||
"others": "Others",
|
"others": "Others",
|
||||||
"Update Available": "Update available",
|
"com.affine.updater.update-available": "Update available",
|
||||||
|
"com.affine.updater.downloading": "Downloading",
|
||||||
|
"com.affine.updater.restart-to-update": "Restart to install update",
|
||||||
|
"com.affine.updater.open-download-page": "Open download page",
|
||||||
"dark": "Dark",
|
"dark": "Dark",
|
||||||
"system": "System",
|
"system": "System",
|
||||||
"light": "Light",
|
"light": "Light",
|
||||||
|
|||||||
Reference in New Issue
Block a user