mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-27 02:42:25 +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);
|
||||||
}
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
6
apps/electron/layers/preload/preload.d.ts
vendored
6
apps/electron/layers/preload/preload.d.ts
vendored
@@ -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,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -232,8 +232,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",
|
||||||
"Update Available": "Update available",
|
|
||||||
"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",
|
||||||
@@ -274,5 +272,17 @@
|
|||||||
"com.affine.onboarding.title1": "Hyper merged whiteboard and docs",
|
"com.affine.onboarding.title1": "Hyper merged whiteboard and docs",
|
||||||
"com.affine.onboarding.title2": "Intuitive & robust block-based editing",
|
"com.affine.onboarding.title2": "Intuitive & robust block-based editing",
|
||||||
"com.affine.onboarding.videoDescription1": "Easily switch between Page mode for structured document creation and Whiteboard mode for the freeform visual expression of creative ideas.",
|
"com.affine.onboarding.videoDescription1": "Easily switch between Page mode for structured document creation and Whiteboard mode for the freeform visual expression of creative ideas.",
|
||||||
"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",
|
||||||
|
"others": "Others",
|
||||||
|
"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",
|
||||||
|
"system": "System",
|
||||||
|
"light": "Light",
|
||||||
|
"com.affine.banner.content": "Enjoying the demo? <1>Download the AFFiNE Client</1> for the full experience.",
|
||||||
|
"com.affine.cloudTempDisable.title": "AFFiNE Cloud is upgrading now.",
|
||||||
|
"com.affine.cloudTempDisable.description": "We are upgrading the AFFiNE Cloud service and it is temporarily unavailable on the client side. If you wish to stay updated on the progress and be notified on availability, you can join the <1>AFFiNE Community</1>."
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user