Show update progress as a title-bar bar instead of a popout.
This commit is contained in:
@@ -1,95 +1,149 @@
|
|||||||
import { style } from '@vanilla-extract/css';
|
import { keyframes, style } from '@vanilla-extract/css';
|
||||||
import { color, config, toRem } from 'folds';
|
import { color, config, toRem } from 'folds';
|
||||||
|
|
||||||
export const CheckButtonContainer = style({
|
const indeterminate = keyframes({
|
||||||
|
'0%': { transform: 'translateX(-100%)' },
|
||||||
|
'100%': { transform: 'translateX(250%)' },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const IdleSlot = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
height: '32px',
|
height: '100%',
|
||||||
width: '32px',
|
WebkitAppRegion: 'no-drag',
|
||||||
|
flexShrink: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const CheckButton = style({
|
export const GhostCheck = style({
|
||||||
all: 'unset',
|
all: 'unset',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
padding: 0,
|
width: toRem(28),
|
||||||
|
height: toRem(28),
|
||||||
borderRadius: config.radii.R300,
|
borderRadius: config.radii.R300,
|
||||||
cursor: 'pointer',
|
color: color.Surface.OnContainer,
|
||||||
color: color.Secondary.Main,
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
transition: 'opacity 0.2s, background-color 0.15s',
|
|
||||||
height: '32px',
|
|
||||||
width: '32px',
|
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
WebkitAppRegion: 'no-drag',
|
cursor: 'pointer',
|
||||||
flexShrink: 0,
|
transition: 'opacity 0.15s ease, background-color 0.15s ease',
|
||||||
|
|
||||||
selectors: {
|
selectors: {
|
||||||
'&[data-visible="true"]': {
|
'&[data-visible="true"]': {
|
||||||
opacity: 0.7,
|
opacity: 0.65,
|
||||||
|
},
|
||||||
|
'&:hover': {
|
||||||
|
opacity: 1,
|
||||||
|
backgroundColor: color.Surface.ContainerHover,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
':hover': {
|
|
||||||
backgroundColor: color.Surface.ContainerHover,
|
|
||||||
opacity: 1,
|
|
||||||
},
|
|
||||||
|
|
||||||
':active': {
|
|
||||||
backgroundColor: color.Surface.ContainerActive,
|
|
||||||
},
|
|
||||||
|
|
||||||
':focus-visible': {
|
|
||||||
outline: `2px solid ${color.Secondary.Main}`,
|
|
||||||
outlineOffset: '2px',
|
|
||||||
opacity: 1,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const UpdateButton = style({
|
export const Bar = style({
|
||||||
all: 'unset',
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
gap: config.space.S200,
|
||||||
padding: 0,
|
height: toRem(22),
|
||||||
|
minWidth: toRem(120),
|
||||||
|
maxWidth: toRem(200),
|
||||||
|
padding: `0 ${config.space.S200}`,
|
||||||
borderRadius: config.radii.R300,
|
borderRadius: config.radii.R300,
|
||||||
cursor: 'pointer',
|
backgroundColor: color.SurfaceVariant.Container,
|
||||||
color: color.Success.Main,
|
color: color.SurfaceVariant.OnContainer,
|
||||||
backgroundColor: 'transparent',
|
|
||||||
transition: 'background-color 0.15s',
|
|
||||||
height: '32px',
|
|
||||||
width: '32px',
|
|
||||||
WebkitAppRegion: 'no-drag',
|
WebkitAppRegion: 'no-drag',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
});
|
||||||
|
|
||||||
':hover': {
|
export const BarTrack = style({
|
||||||
backgroundColor: color.Surface.ContainerHover,
|
position: 'relative',
|
||||||
},
|
flex: 1,
|
||||||
|
height: toRem(4),
|
||||||
|
borderRadius: toRem(2),
|
||||||
|
overflow: 'hidden',
|
||||||
|
backgroundColor: color.Surface.ContainerLine,
|
||||||
|
minWidth: toRem(64),
|
||||||
|
});
|
||||||
|
|
||||||
':active': {
|
export const BarFill = style({
|
||||||
backgroundColor: color.Surface.ContainerActive,
|
position: 'absolute',
|
||||||
},
|
inset: '0 auto 0 0',
|
||||||
|
height: '100%',
|
||||||
|
borderRadius: 'inherit',
|
||||||
|
backgroundColor: color.Success.Main,
|
||||||
|
transition: 'width 120ms linear',
|
||||||
|
});
|
||||||
|
|
||||||
':focus-visible': {
|
export const BarIndeterminate = style({
|
||||||
outline: `2px solid ${color.Success.Main}`,
|
position: 'absolute',
|
||||||
outlineOffset: '2px',
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '40%',
|
||||||
|
borderRadius: 'inherit',
|
||||||
|
backgroundColor: color.Secondary.Main,
|
||||||
|
animation: `${indeterminate} 1s ease-in-out infinite`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const BarLabel = style({
|
||||||
|
flexShrink: 0,
|
||||||
|
fontVariantNumeric: 'tabular-nums',
|
||||||
|
opacity: 0.9,
|
||||||
|
minWidth: toRem(28),
|
||||||
|
textAlign: 'right',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Chip = style({
|
||||||
|
height: toRem(22),
|
||||||
|
maxWidth: toRem(260),
|
||||||
|
padding: `0 ${config.space.S100} 0 ${config.space.S200}`,
|
||||||
|
borderRadius: config.radii.R300,
|
||||||
|
backgroundColor: color.Success.Container,
|
||||||
|
color: color.Success.OnContainer,
|
||||||
|
WebkitAppRegion: 'no-drag',
|
||||||
|
flexShrink: 0,
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ChipText = style({
|
||||||
|
maxWidth: toRem(140),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ChipAction = style({
|
||||||
|
all: 'unset',
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: toRem(18),
|
||||||
|
padding: `0 ${config.space.S200}`,
|
||||||
|
borderRadius: config.radii.R300,
|
||||||
|
backgroundColor: color.Success.Main,
|
||||||
|
color: color.Success.OnMain,
|
||||||
|
fontSize: toRem(11),
|
||||||
|
fontWeight: 600,
|
||||||
|
cursor: 'pointer',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
selectors: {
|
||||||
|
'&:hover': {
|
||||||
|
filter: 'brightness(1.05)',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const UpdateMenu = style({
|
export const ChipDismiss = style({
|
||||||
minWidth: toRem(280),
|
all: 'unset',
|
||||||
maxWidth: toRem(320),
|
display: 'inline-flex',
|
||||||
backgroundColor: color.Surface.Container,
|
alignItems: 'center',
|
||||||
borderRadius: config.radii.R400,
|
justifyContent: 'center',
|
||||||
boxShadow: config.shadow.E400,
|
width: toRem(18),
|
||||||
border: `1px solid ${color.Surface.ContainerLine}`,
|
height: toRem(18),
|
||||||
});
|
borderRadius: config.radii.R300,
|
||||||
|
cursor: 'pointer',
|
||||||
export const ProgressText = style({
|
opacity: 0.7,
|
||||||
color: color.Success.Main,
|
fontSize: toRem(14),
|
||||||
fontWeight: 500,
|
lineHeight: 1,
|
||||||
minWidth: toRem(35),
|
selectors: {
|
||||||
textAlign: 'center',
|
'&:hover': {
|
||||||
|
opacity: 1,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.08)',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,138 +1,136 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { Box, Spinner, Text, Menu, PopOut, Button, config } from 'folds';
|
import { Box, Text } from 'folds';
|
||||||
import * as css from './UpdateNotification.css';
|
import * as css from './UpdateNotification.css';
|
||||||
|
|
||||||
interface UpdateInfo {
|
interface UpdateInfo {
|
||||||
version: string;
|
version: string;
|
||||||
releaseNotes?: string;
|
releaseNotes?: string;
|
||||||
releaseDate?: string;
|
releaseDate?: string;
|
||||||
|
mock?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DownloadProgress {
|
interface DownloadProgress {
|
||||||
percent: number;
|
percent: number;
|
||||||
transferred: number;
|
transferred: number;
|
||||||
total: number;
|
total: number;
|
||||||
|
mock?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UpdaterPhase = 'idle' | 'checking' | 'available' | 'downloading' | 'ready';
|
||||||
|
|
||||||
export function UpdateNotification() {
|
export function UpdateNotification() {
|
||||||
const [updateAvailable, setUpdateAvailable] = useState(false);
|
const [phase, setPhase] = useState<UpdaterPhase>('idle');
|
||||||
const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null);
|
const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null);
|
||||||
const [downloading, setDownloading] = useState(false);
|
|
||||||
const [downloadProgress, setDownloadProgress] = useState(0);
|
const [downloadProgress, setDownloadProgress] = useState(0);
|
||||||
const [updateReady, setUpdateReady] = useState(false);
|
const [isMock, setIsMock] = useState(false);
|
||||||
const [checking, setChecking] = useState(false);
|
const [hovered, setHovered] = useState(false);
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
|
||||||
const [menuAnchor, setMenuAnchor] = useState<{ x: number; y: number; width: number; height: number } | undefined>(undefined);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Check if we're in Electron environment
|
const electron = (window as any).electron;
|
||||||
if (typeof window === 'undefined' || !(window as any).electron?.updater) {
|
if (!electron?.updater) return undefined;
|
||||||
return;
|
|
||||||
|
const { updater } = electron;
|
||||||
|
|
||||||
|
updater.isMock?.().then((result: { success?: boolean; data?: { mock?: boolean } }) => {
|
||||||
|
if (result?.data?.mock) setIsMock(true);
|
||||||
|
}).catch(() => {});
|
||||||
|
|
||||||
|
updater.onUpdateAvailable((info: UpdateInfo) => {
|
||||||
|
setUpdateInfo(info);
|
||||||
|
if (info.mock) setIsMock(true);
|
||||||
|
setPhase('available');
|
||||||
|
setDownloadProgress(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
updater.onUpdateDownloadProgress((progress: DownloadProgress) => {
|
||||||
|
setPhase('downloading');
|
||||||
|
setDownloadProgress(Math.min(100, Math.round(progress.percent)));
|
||||||
|
});
|
||||||
|
|
||||||
|
updater.onUpdateDownloaded((info: UpdateInfo) => {
|
||||||
|
setUpdateInfo(info);
|
||||||
|
setPhase('ready');
|
||||||
|
setDownloadProgress(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
const onNotAvailable = () => {
|
||||||
|
setPhase('idle');
|
||||||
|
setUpdateInfo(null);
|
||||||
|
setDownloadProgress(0);
|
||||||
|
};
|
||||||
|
// Optional channel — ignore if preload doesn't expose a dedicated listener
|
||||||
|
if (electron.updater.onUpdateNotAvailable) {
|
||||||
|
electron.updater.onUpdateNotAvailable(onNotAvailable);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { updater } = (window as any).electron;
|
return undefined;
|
||||||
|
|
||||||
// Listen for update available
|
|
||||||
updater.onUpdateAvailable((info: UpdateInfo) => {
|
|
||||||
console.log('Update available:', info.version);
|
|
||||||
setUpdateAvailable(true);
|
|
||||||
setUpdateInfo(info);
|
|
||||||
setDownloading(false);
|
|
||||||
setUpdateReady(false);
|
|
||||||
setChecking(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listen for download progress
|
|
||||||
updater.onUpdateDownloadProgress((progress: DownloadProgress) => {
|
|
||||||
setDownloadProgress(Math.round(progress.percent));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listen for update downloaded
|
|
||||||
updater.onUpdateDownloaded((info: UpdateInfo) => {
|
|
||||||
console.log('Update downloaded:', info.version);
|
|
||||||
setDownloading(false);
|
|
||||||
setUpdateReady(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cleanup - IPC listeners don't need manual cleanup in this case
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleCheckForUpdates = async () => {
|
const handleCheck = useCallback(async () => {
|
||||||
setChecking(true);
|
setPhase('checking');
|
||||||
try {
|
try {
|
||||||
const result = await (window as any).electron.updater.checkForUpdates();
|
const result = await (window as any).electron.updater.checkForUpdates();
|
||||||
console.log('Update check result:', result);
|
if (!result?.success) {
|
||||||
|
setPhase('idle');
|
||||||
// Handle error response (including dev mode error)
|
|
||||||
if (!result.success) {
|
|
||||||
console.warn('Update check failed:', result.error);
|
|
||||||
setChecking(false);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
// available event will advance phase; if nothing comes, fall back
|
||||||
// If no update found, show feedback briefly
|
window.setTimeout(() => {
|
||||||
setTimeout(() => {
|
setPhase((current) => (current === 'checking' ? 'idle' : current));
|
||||||
if (!updateAvailable) {
|
}, 4000);
|
||||||
setChecking(false);
|
} catch {
|
||||||
}
|
setPhase('idle');
|
||||||
}, 2000);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to check for updates:', error);
|
|
||||||
setChecking(false);
|
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const handleDownload = async () => {
|
const handleDownload = useCallback(async () => {
|
||||||
setDownloading(true);
|
setPhase('downloading');
|
||||||
setDownloadProgress(0);
|
setDownloadProgress(0);
|
||||||
try {
|
try {
|
||||||
await (window as any).electron.updater.downloadUpdate();
|
await (window as any).electron.updater.downloadUpdate();
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.error('Failed to download update:', error);
|
setPhase('available');
|
||||||
setDownloading(false);
|
|
||||||
}
|
}
|
||||||
setMenuAnchor(undefined);
|
}, []);
|
||||||
};
|
|
||||||
|
|
||||||
const handleInstall = async () => {
|
const handleInstall = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
await (window as any).electron.updater.installUpdate();
|
await (window as any).electron.updater.installUpdate();
|
||||||
} catch (error) {
|
if (isMock) {
|
||||||
console.error('Failed to install update:', error);
|
setPhase('idle');
|
||||||
|
setUpdateInfo(null);
|
||||||
|
setDownloadProgress(0);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// keep ready state
|
||||||
}
|
}
|
||||||
};
|
}, [isMock]);
|
||||||
|
|
||||||
const handleMenuToggle = (event: React.MouseEvent<HTMLButtonElement>) => {
|
const handleDismiss = useCallback(() => {
|
||||||
if (menuAnchor) {
|
setPhase('idle');
|
||||||
setMenuAnchor(undefined);
|
setUpdateInfo(null);
|
||||||
} else {
|
setDownloadProgress(0);
|
||||||
const rect = event.currentTarget.getBoundingClientRect();
|
}, []);
|
||||||
setMenuAnchor({ x: rect.x, y: rect.y, width: rect.width, height: rect.height });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Don't render anything if not in Electron
|
|
||||||
if (typeof window === 'undefined' || !(window as any).electron?.updater) {
|
if (typeof window === 'undefined' || !(window as any).electron?.updater) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show check button if no update status
|
if (phase === 'idle') {
|
||||||
if (!updateAvailable && !updateReady && !checking) {
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={css.CheckButtonContainer}
|
className={css.IdleSlot}
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
onMouseEnter={() => setHovered(true)}
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
onMouseLeave={() => setHovered(false)}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className={css.CheckButton}
|
|
||||||
data-visible={isHovered}
|
|
||||||
onClick={handleCheckForUpdates}
|
|
||||||
aria-label="Check for updates"
|
|
||||||
title="Check for updates"
|
|
||||||
type="button"
|
type="button"
|
||||||
|
className={css.GhostCheck}
|
||||||
|
data-visible={hovered || isMock ? 'true' : undefined}
|
||||||
|
onClick={handleCheck}
|
||||||
|
title={isMock ? 'Check for updates (mock)' : 'Check for updates'}
|
||||||
|
aria-label="Check for updates"
|
||||||
>
|
>
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden>
|
||||||
<path
|
<path
|
||||||
d="M8 2V10M8 10L5 7M8 10L11 7"
|
d="M8 2V10M8 10L5 7M8 10L11 7"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
@@ -140,109 +138,75 @@ export function UpdateNotification() {
|
|||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
/>
|
/>
|
||||||
<path
|
<path d="M3 14H13" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
||||||
d="M3 14H13"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
strokeLinecap="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show checking state
|
if (phase === 'checking') {
|
||||||
if (checking) {
|
|
||||||
return (
|
return (
|
||||||
<button className={css.UpdateButton} disabled type="button">
|
<div className={css.Bar} title="Checking for updates…">
|
||||||
<Spinner variant="Secondary" size="50" />
|
<div className={css.BarTrack}>
|
||||||
</button>
|
<div className={css.BarIndeterminate} />
|
||||||
|
</div>
|
||||||
|
<Text className={css.BarLabel} size="L400">
|
||||||
|
Checking…
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show update available/ready state
|
if (phase === 'downloading') {
|
||||||
if (!updateAvailable && !updateReady) {
|
return (
|
||||||
return null;
|
<div
|
||||||
|
className={css.Bar}
|
||||||
|
title={`Downloading update${updateInfo ? ` ${updateInfo.version}` : ''}… ${downloadProgress}%`}
|
||||||
|
>
|
||||||
|
<div className={css.BarTrack}>
|
||||||
|
<div className={css.BarFill} style={{ width: `${downloadProgress}%` }} />
|
||||||
|
</div>
|
||||||
|
<Text className={css.BarLabel} size="L400">
|
||||||
|
{downloadProgress}%
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
if (phase === 'ready') {
|
||||||
<>
|
return (
|
||||||
<button
|
<Box className={css.Chip} alignItems="Center" gap="100">
|
||||||
className={css.UpdateButton}
|
<Text className={css.ChipText} size="L400" truncate>
|
||||||
onClick={handleMenuToggle}
|
{isMock ? 'Mock ready' : 'Update ready'}
|
||||||
aria-label={updateReady ? 'Update ready' : 'Update available'}
|
{updateInfo ? ` · ${updateInfo.version}` : ''}
|
||||||
title={updateReady ? 'Update downloaded - click to install' : 'New version available'}
|
</Text>
|
||||||
type="button"
|
<button type="button" className={css.ChipAction} onClick={handleInstall}>
|
||||||
>
|
Restart
|
||||||
{downloading ? (
|
</button>
|
||||||
<Spinner variant="Secondary" size="50" />
|
</Box>
|
||||||
) : (
|
);
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
}
|
||||||
<path
|
|
||||||
d="M8 2V10M8 10L5 7M8 10L11 7"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M3 14H13"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
strokeLinecap="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<PopOut
|
// available
|
||||||
anchor={menuAnchor}
|
return (
|
||||||
position="Bottom"
|
<Box className={css.Chip} alignItems="Center" gap="100">
|
||||||
align="End"
|
<Text className={css.ChipText} size="L400" truncate>
|
||||||
offset={8}
|
{isMock ? 'Mock update' : 'Update'}
|
||||||
content={
|
{updateInfo ? ` ${updateInfo.version}` : ''}
|
||||||
<Menu className={css.UpdateMenu}>
|
</Text>
|
||||||
<Box direction="Column" gap="200" style={{ padding: config.space.S300 }}>
|
<button type="button" className={css.ChipAction} onClick={handleDownload}>
|
||||||
<Box direction="Column" gap="100">
|
Download
|
||||||
<Text size="H5" priority="400">
|
</button>
|
||||||
{updateReady ? 'Update Ready' : 'Update Available'}
|
<button
|
||||||
</Text>
|
type="button"
|
||||||
{updateInfo && (
|
className={css.ChipDismiss}
|
||||||
<Text size="T200" priority="300">
|
onClick={handleDismiss}
|
||||||
Version {updateInfo.version}
|
aria-label="Dismiss update"
|
||||||
</Text>
|
title="Dismiss"
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{updateReady ? (
|
|
||||||
<Button
|
|
||||||
variant="Primary"
|
|
||||||
size="400"
|
|
||||||
onClick={handleInstall}
|
|
||||||
fill="Solid"
|
|
||||||
>
|
|
||||||
<Text size="B400">Install and Restart</Text>
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
variant="Primary"
|
|
||||||
size="400"
|
|
||||||
onClick={handleDownload}
|
|
||||||
disabled={downloading}
|
|
||||||
fill="Solid"
|
|
||||||
>
|
|
||||||
<Text size="B400">
|
|
||||||
{downloading ? `Downloading ${downloadProgress}%` : 'Download Update'}
|
|
||||||
</Text>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Menu>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{null}
|
×
|
||||||
</PopOut>
|
</button>
|
||||||
</>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user