mirror of
https://gitlab.com/futo-org/fcast.git
synced 2025-06-24 21:25:23 +00:00
New updater flow and UI
This commit is contained in:
parent
a967b7983a
commit
568c972492
6 changed files with 358 additions and 146 deletions
|
@ -53,22 +53,18 @@ export default class Main {
|
|||
{
|
||||
label: 'Check for updates',
|
||||
click: async () => {
|
||||
if (!Updater.isDownloading) {
|
||||
try {
|
||||
if (await Updater.update()) {
|
||||
const restartPrompt = await dialog.showMessageBox({
|
||||
type: 'info',
|
||||
title: 'Update ready',
|
||||
message: 'Update downloaded, restart now to apply the changes.',
|
||||
buttons: ['Restart'],
|
||||
defaultId: 0
|
||||
});
|
||||
|
||||
// Restart the app if the user clicks the 'Restart' button
|
||||
if (restartPrompt.response === 0) {
|
||||
Updater.restart();
|
||||
if (Updater.updateDownloaded) {
|
||||
Main.mainWindow.webContents.send("download-complete");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
|
||||
try {
|
||||
const updateAvailable = await Updater.checkForUpdates();
|
||||
|
||||
if (updateAvailable) {
|
||||
Main.mainWindow.webContents.send("update-available");
|
||||
}
|
||||
else {
|
||||
await dialog.showMessageBox({
|
||||
type: 'info',
|
||||
title: 'Already up-to-date',
|
||||
|
@ -80,14 +76,13 @@ export default class Main {
|
|||
} catch (err) {
|
||||
await dialog.showMessageBox({
|
||||
type: 'error',
|
||||
title: 'Failed to update',
|
||||
title: 'Failed to check for updates',
|
||||
message: err,
|
||||
buttons: ['OK'],
|
||||
defaultId: 0
|
||||
});
|
||||
|
||||
Main.logger.error('Failed to update:', err);
|
||||
}
|
||||
Main.logger.error('Failed to check for updates:', err);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@ -182,8 +177,34 @@ export default class Main {
|
|||
ipcMain.on('send-volume-update', (event: IpcMainEvent, value: VolumeUpdateMessage) => {
|
||||
l.send(Opcode.VolumeUpdate, value);
|
||||
});
|
||||
|
||||
ipcMain.on('send-download-request', async () => {
|
||||
if (!Updater.isDownloading) {
|
||||
try {
|
||||
await Updater.downloadUpdate();
|
||||
Main.mainWindow.webContents.send("download-complete");
|
||||
} catch (err) {
|
||||
await dialog.showMessageBox({
|
||||
type: 'error',
|
||||
title: 'Failed to download update',
|
||||
message: err,
|
||||
buttons: ['OK'],
|
||||
defaultId: 0
|
||||
});
|
||||
|
||||
Main.logger.error('Failed to download update:', err);
|
||||
Main.mainWindow.webContents.send("download-failed");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('send-restart-request', async () => {
|
||||
Updater.restart();
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.handle('updater-progress', async () => { return Updater.updateProgress; });
|
||||
|
||||
ipcMain.handle('is-full-screen', async () => {
|
||||
const window = Main.playerWindow;
|
||||
if (!window) {
|
||||
|
@ -214,6 +235,16 @@ export default class Main {
|
|||
if (Main.shouldOpenMainWindow) {
|
||||
Main.openMainWindow();
|
||||
}
|
||||
|
||||
if (Updater.updateError) {
|
||||
dialog.showMessageBox({
|
||||
type: 'error',
|
||||
title: 'Error applying update',
|
||||
message: 'Please try again later or visit https://fcast.org to update.',
|
||||
buttons: ['OK'],
|
||||
defaultId: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -50,9 +50,17 @@ export class Updater {
|
|||
private static updateDataPath: string = path.join(app.getPath('userData'), 'updater');
|
||||
private static updateMetadataPath = path.join(Updater.updateDataPath, './update.json');
|
||||
private static baseUrl: string = 'https://dl.fcast.org/electron';
|
||||
private static channelVersion: string = null;
|
||||
private static isRestarting: boolean = false;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private static localPackageJson: any = null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private static releasesJson: any = null;
|
||||
|
||||
public static isDownloading: boolean = false;
|
||||
public static updateError: boolean = false;
|
||||
public static updateDownloaded: boolean = false;
|
||||
public static updateProgress: number = 0;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private static async fetchJSON(url: string): Promise<any> {
|
||||
|
@ -80,7 +88,15 @@ export class Updater {
|
|||
return new Promise((resolve, reject) => {
|
||||
const file = fs.createWriteStream(destination);
|
||||
https.get(url, (response) => {
|
||||
const downloadSize = Number(response.headers['content-length']);
|
||||
logger.info(`Update size: ${downloadSize} bytes`);
|
||||
response.pipe(file);
|
||||
let downloadedBytes = 0;
|
||||
|
||||
response.on('data', (chunk) => {
|
||||
downloadedBytes += chunk.length;
|
||||
Updater.updateProgress = downloadedBytes / downloadSize;
|
||||
});
|
||||
file.on('finish', () => {
|
||||
file.close();
|
||||
resolve();
|
||||
|
@ -169,19 +185,23 @@ export class Updater {
|
|||
}
|
||||
|
||||
public static restart() {
|
||||
if (!Updater.isRestarting) {
|
||||
Updater.isRestarting = true;
|
||||
const updateInfo: UpdateInfo = JSON.parse(fs.readFileSync(Updater.updateMetadataPath, 'utf8'));
|
||||
const extractionDir = process.platform === 'darwin' ? 'FCast Receiver.app' : `fcast-receiver-${process.platform}-${process.arch}`;
|
||||
const binaryName = process.platform === 'win32' ? 'fcast-receiver.exe' : 'fcast-receiver';
|
||||
const updateBinPath = process.platform === 'darwin' ? path.join(updateInfo.tempPath, extractionDir) : path.join(updateInfo.tempPath, extractionDir, binaryName);
|
||||
|
||||
Updater.relaunch(updateBinPath);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
public static isUpdating(): boolean {
|
||||
try {
|
||||
const updateInfo: UpdateInfo = JSON.parse(fs.readFileSync(Updater.updateMetadataPath, 'utf8'));
|
||||
// TODO: In case of error inform user
|
||||
Updater.updateError = true;
|
||||
return updateInfo.updateState !== 'error';
|
||||
}
|
||||
catch {
|
||||
|
@ -190,12 +210,12 @@ export class Updater {
|
|||
}
|
||||
|
||||
public static getChannelVersion(): string {
|
||||
if (Updater.channelVersion === null) {
|
||||
const localPackage = JSON.parse(fs.readFileSync(path.join(Updater.appPath, './package.json'), 'utf8'));
|
||||
Updater.channelVersion = localPackage.channelVersion ? localPackage.channelVersion : 0
|
||||
if (Updater.localPackageJson === null) {
|
||||
Updater.localPackageJson = JSON.parse(fs.readFileSync(path.join(Updater.appPath, './package.json'), 'utf8'));
|
||||
Updater.localPackageJson.channelVersion = Updater.localPackageJson.channelVersion ? Updater.localPackageJson.channelVersion : 0
|
||||
}
|
||||
|
||||
return Updater.channelVersion;
|
||||
return Updater.localPackageJson.channelVersion;
|
||||
}
|
||||
|
||||
public static async processUpdate(): Promise<void> {
|
||||
|
@ -215,9 +235,6 @@ export class Updater {
|
|||
await Updater.applyUpdate(src, updateInfo.installPath);
|
||||
updateInfo.updateState = UpdateState.Cleanup;
|
||||
fs.writeFileSync(Updater.updateMetadataPath, JSON.stringify(updateInfo));
|
||||
|
||||
Updater.relaunch(installBinPath);
|
||||
return;
|
||||
}
|
||||
catch (err) {
|
||||
logger.error('Error while applying update...');
|
||||
|
@ -226,10 +243,9 @@ export class Updater {
|
|||
updateInfo.updateState = UpdateState.Error;
|
||||
updateInfo.error = JSON.stringify(err);
|
||||
fs.writeFileSync(Updater.updateMetadataPath, JSON.stringify(updateInfo));
|
||||
log4js.shutdown();
|
||||
app.exit();
|
||||
}
|
||||
|
||||
Updater.relaunch(installBinPath);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -272,8 +288,44 @@ export class Updater {
|
|||
}
|
||||
}
|
||||
|
||||
public static async update(): Promise<boolean> {
|
||||
logger.info('Updater invoked');
|
||||
public static async checkForUpdates(): Promise<boolean> {
|
||||
logger.info('Checking for updates...');
|
||||
Updater.localPackageJson = JSON.parse(fs.readFileSync(path.join(Updater.appPath, './package.json'), 'utf8'));
|
||||
|
||||
try {
|
||||
Updater.releasesJson = await Updater.fetchJSON(`${Updater.baseUrl}/releases_v${Updater.supportedReleasesJsonVersion}.json`.toString()) as ReleaseInfo;
|
||||
|
||||
let updaterSettings = Store.get('updater');
|
||||
if (updaterSettings === null) {
|
||||
updaterSettings = {
|
||||
'channel': Updater.localPackageJson.channel,
|
||||
}
|
||||
|
||||
Store.set('updater', updaterSettings);
|
||||
}
|
||||
else {
|
||||
Updater.localPackageJson.channel = updaterSettings.channel;
|
||||
}
|
||||
|
||||
const localChannelVersion: number = Updater.localPackageJson.channelVersion ? Updater.localPackageJson.channelVersion : 0;
|
||||
const currentChannelVersion: number = Updater.releasesJson.channelCurrentVersions[Updater.localPackageJson.channel] ? Updater.releasesJson.channelCurrentVersions[Updater.localPackageJson.channel] : 0;
|
||||
logger.info('Update check', { channel: Updater.localPackageJson.channel, channel_version: localChannelVersion, localVersion: Updater.localPackageJson.version,
|
||||
currentVersion: Updater.releasesJson.currentVersion, currentChannelVersion: currentChannelVersion });
|
||||
|
||||
if (Updater.localPackageJson.version !== Updater.releasesJson.currentVersion || (Updater.localPackageJson.channel !== 'stable' && localChannelVersion < currentChannelVersion)) {
|
||||
logger.info('Update available...');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
logger.error(`Failed to check for updates: ${err}`);
|
||||
throw 'Please try again later or visit https://fcast.org for updates.';
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static async downloadUpdate(): Promise<boolean> {
|
||||
try {
|
||||
fs.accessSync(Updater.updateDataPath, fs.constants.F_OK);
|
||||
}
|
||||
|
@ -282,27 +334,9 @@ export class Updater {
|
|||
fs.mkdirSync(Updater.updateDataPath);
|
||||
}
|
||||
|
||||
const localPackage = JSON.parse(fs.readFileSync(path.join(Updater.appPath, './package.json'), 'utf8'));
|
||||
try {
|
||||
const releases = await Updater.fetchJSON(`${Updater.baseUrl}/releases_v${Updater.supportedReleasesJsonVersion}.json`.toString()) as ReleaseInfo;
|
||||
|
||||
let updaterSettings = Store.get('updater');
|
||||
if (updaterSettings === null) {
|
||||
updaterSettings = {
|
||||
'channel': localPackage.channel,
|
||||
}
|
||||
|
||||
Store.set('updater', updaterSettings);
|
||||
}
|
||||
|
||||
const localChannelVersion: number = localPackage.channelVersion ? localPackage.channelVersion : 0
|
||||
const currentChannelVersion: number = releases.channelCurrentVersions[localPackage.channel] ? releases.channelCurrentVersions[localPackage.channel] : 0
|
||||
logger.info('Update check', { channel: localPackage.channel, channel_version: localChannelVersion, localVersion: localPackage.version,
|
||||
currentVersion: releases.currentVersion, currentChannelVersion: currentChannelVersion });
|
||||
|
||||
if (localPackage.version !== releases.currentVersion || (localPackage.channel !== 'stable' && localChannelVersion < currentChannelVersion)) {
|
||||
const channel = localPackage.version !== releases.currentVersion ? 'stable' : localPackage.channel;
|
||||
const fileInfo = releases.currentReleases[channel][process.platform][process.arch]
|
||||
const channel = Updater.localPackageJson.version !== Updater.releasesJson.currentVersion ? 'stable' : Updater.localPackageJson.channel;
|
||||
const fileInfo = Updater.releasesJson.currentReleases[channel][process.platform][process.arch]
|
||||
const file = fileInfo.url.toString().split('/').pop();
|
||||
|
||||
const destination = path.join(Updater.updateDataPath, file);
|
||||
|
@ -313,7 +347,7 @@ export class Updater {
|
|||
const downloadedFile = await fs.promises.readFile(destination);
|
||||
const hash = crypto.createHash('sha256').end(downloadedFile).digest('hex');
|
||||
if (fileInfo.sha256Digest !== hash) {
|
||||
const message = 'Update failed integrity check. Please try checking for updates again or downloading the update manually.';
|
||||
const message = 'Update failed integrity check. Please try again later or visit https://fcast.org to for updates.';
|
||||
logger.error(`Update failed integrity check. Expected hash: ${fileInfo.sha256Digest}, actual hash: ${hash}`);
|
||||
throw message;
|
||||
}
|
||||
|
@ -329,23 +363,21 @@ export class Updater {
|
|||
updateState: UpdateState.Copy,
|
||||
installPath: Updater.installPath,
|
||||
tempPath: path.dirname(destination),
|
||||
currentVersion: releases.currentVersion,
|
||||
currentVersion: Updater.releasesJson.currentVersion,
|
||||
downloadFile: file,
|
||||
};
|
||||
|
||||
fs.writeFileSync(Updater.updateMetadataPath, JSON.stringify(updateInfo));
|
||||
logger.info('Written update metadata.');
|
||||
Updater.isDownloading = false;
|
||||
Updater.updateDownloaded = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
Updater.isDownloading = false;
|
||||
process.noAsar = false;
|
||||
logger.error(`Failed to check for updates: ${err}`);
|
||||
throw 'Failed to check for updates. Please try again later or visit https://fcast.org for updates.';
|
||||
logger.error(`Failed to download update: ${err}`);
|
||||
throw 'Failed to download update. Please try again later or visit https://fcast.org to download.';
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,12 @@ ipcRenderer.on("device-info", (_event, value) => {
|
|||
})
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
updaterProgress: () => ipcRenderer.invoke('updater-progress'),
|
||||
onDeviceInfo: (callback) => ipcRenderer.on("device-info", callback),
|
||||
onUpdateAvailable: (callback) => ipcRenderer.on("update-available", callback),
|
||||
sendDownloadRequest: () => ipcRenderer.send('send-download-request'),
|
||||
onDownloadComplete: (callback) => ipcRenderer.on("download-complete", callback),
|
||||
onDownloadFailed: (callback) => ipcRenderer.on("download-failed", callback),
|
||||
sendRestartRequest: () => ipcRenderer.send('send-restart-request'),
|
||||
getDeviceInfo: () => deviceInfo,
|
||||
});
|
||||
|
|
|
@ -1,5 +1,15 @@
|
|||
import QRCode from 'qrcode';
|
||||
|
||||
const updateView = document.getElementById("update-view");
|
||||
const updateViewTitle = document.getElementById("update-view-title");
|
||||
const updateText = document.getElementById("update-text");
|
||||
const updateButton = document.getElementById("update-button");
|
||||
const restartButton = document.getElementById("restart-button");
|
||||
const updateLaterButton = document.getElementById("update-later-button");
|
||||
const progressBar = document.getElementById("progress-bar");
|
||||
const progressBarProgress = document.getElementById("progress-bar-progress");
|
||||
|
||||
let updaterProgressUIUpdateTimer = null;
|
||||
window.electronAPI.onDeviceInfo(renderIPsAndQRCode);
|
||||
|
||||
if(window.electronAPI.getDeviceInfo()) {
|
||||
|
@ -45,3 +55,57 @@ function renderIPsAndQRCode() {
|
|||
console.log(`Error rendering QR Code: ${e}`)
|
||||
});
|
||||
}
|
||||
|
||||
window.electronAPI.onUpdateAvailable(() => {
|
||||
console.log(`Received UpdateAvailable event`);
|
||||
updateViewTitle.textContent = 'FCast update available';
|
||||
|
||||
updateText.textContent = 'Do you wish to update now?';
|
||||
updateButton.setAttribute("style", "display: block");
|
||||
updateLaterButton.setAttribute("style", "display: block");
|
||||
restartButton.setAttribute("style", "display: none");
|
||||
progressBar.setAttribute("style", "display: none");
|
||||
updateView.setAttribute("style", "display: flex");
|
||||
});
|
||||
|
||||
window.electronAPI.onDownloadComplete(() => {
|
||||
console.log(`Received DownloadComplete event`);
|
||||
window.clearTimeout(updaterProgressUIUpdateTimer);
|
||||
updateViewTitle.textContent = 'FCast update ready';
|
||||
|
||||
updateText.textContent = 'Restart now to apply the changes?';
|
||||
updateButton.setAttribute("style", "display: none");
|
||||
progressBar.setAttribute("style", "display: none");
|
||||
restartButton.setAttribute("style", "display: block");
|
||||
updateLaterButton.setAttribute("style", "display: block");
|
||||
updateView.setAttribute("style", "display: flex");
|
||||
});
|
||||
|
||||
window.electronAPI.onDownloadFailed(() => {
|
||||
console.log(`Received DownloadFailed event`);
|
||||
window.clearTimeout(updaterProgressUIUpdateTimer);
|
||||
updateView.setAttribute("style", "display: none");
|
||||
});
|
||||
|
||||
updateLaterButton.onclick = () => { updateView.setAttribute("style", "display: none"); };
|
||||
updateButton.onclick = () => {
|
||||
updaterProgressUIUpdateTimer = window.setInterval( async () => {
|
||||
const updateProgress = await window.electronAPI.updaterProgress();
|
||||
|
||||
if (updateProgress >= 1.0) {
|
||||
updateText.textContent = "Preparing update...";
|
||||
progressBarProgress.setAttribute("style", `width: 100%`);
|
||||
}
|
||||
else {
|
||||
progressBarProgress.setAttribute("style", `width: ${Math.max(12, updateProgress * 100)}%`);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
updateText.textContent = 'Downloading...';
|
||||
updateButton.setAttribute("style", "display: none");
|
||||
updateLaterButton.setAttribute("style", "display: none");
|
||||
progressBarProgress.setAttribute("style", "width: 12%");
|
||||
progressBar.setAttribute("style", "display: block");
|
||||
window.electronAPI.sendDownloadRequest();
|
||||
};
|
||||
restartButton.onclick = () => { window.electronAPI.sendRestartRequest(); };
|
||||
|
|
|
@ -25,17 +25,25 @@
|
|||
<div id="spinner" class="lds-ring"><div></div><div></div><div></div><div></div></div>
|
||||
</div>
|
||||
|
||||
<!-- <div id="update-dialog">There is an update available. Do you wish to update?</div>
|
||||
<div id="update-button">Update</div>
|
||||
<div id="update-button">Later</div>
|
||||
<div id="progress-container">
|
||||
<div id="update-spinner" class="lds-ring"><div></div><div></div><div></div><div></div></div>
|
||||
<div id="progress-text"></div>
|
||||
</div> -->
|
||||
<div id="update-view" class="card">
|
||||
<div id="update-view-title" class="non-selectable card-title">FCast update available</div>
|
||||
<div class="card-title-separator"></div>
|
||||
|
||||
<div id="update-text">Do you wish to update now?</div>
|
||||
<div id="update-button-container">
|
||||
<div id="update-button" class="button button-primary">Update</div>
|
||||
<div id="restart-button" class="button button-primary">Restart</div>
|
||||
<div id="update-later-button" class="button button-secondary">Later</div>
|
||||
</div>
|
||||
<div id="detail-view">
|
||||
<div id="manual-connection-info" class="non-selectable">Manual connection information</div>
|
||||
<div id="manual-connection-info-separator"></div>
|
||||
|
||||
<div id="progress-bar">
|
||||
<div id="progress-bar-progress"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="detail-view" class="card">
|
||||
<div class="non-selectable card-title">Manual connection information</div>
|
||||
<div class="card-title-separator"></div>
|
||||
<div>
|
||||
<div id="ips">IPs</div><br />
|
||||
<div>Port<br>46899 (TCP), 46898 (WS)</div>
|
||||
|
|
|
@ -19,6 +19,70 @@ body, html {
|
|||
user-select: none;
|
||||
}
|
||||
|
||||
.card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
|
||||
background-color: rgba(20, 20, 20, 0.5);
|
||||
padding: 25px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #2E2E2E;
|
||||
scrollbar-width: thin;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-weight: 700;
|
||||
line-height: 24px;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.card-title-separator {
|
||||
height: 1px;
|
||||
background: #2E2E2E;
|
||||
margin-top: 3px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: inline-block;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
gap: 6px;
|
||||
flex: 1 0 0;
|
||||
border-radius: 6px;
|
||||
|
||||
margin: 20px 10px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.button-primary {
|
||||
background: #008BD7;
|
||||
}
|
||||
|
||||
.button-primary:hover {
|
||||
background: #0D9DDF;
|
||||
}
|
||||
|
||||
.button-primary:active {
|
||||
background: #0069AA;
|
||||
}
|
||||
|
||||
.button-secondary {
|
||||
background: #3E3E3E;
|
||||
}
|
||||
|
||||
.button-secondary:hover {
|
||||
background: #555555;
|
||||
}
|
||||
|
||||
.button-secondary:active {
|
||||
background: #3E3E3E;
|
||||
}
|
||||
|
||||
#ui-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -80,17 +144,6 @@ body, html {
|
|||
padding: 25px;
|
||||
}
|
||||
|
||||
#detail-view {
|
||||
text-align: center;
|
||||
|
||||
background-color: rgba(20, 20, 20, 0.5);
|
||||
padding: 25px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #2E2E2E;
|
||||
scrollbar-width: thin;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#manual-connection-info {
|
||||
font-weight: 700;
|
||||
line-height: 24px;
|
||||
|
@ -118,41 +171,59 @@ body, html {
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
#update-dialog, #waiting-for-connection, #ips, #automatic-discovery {
|
||||
#waiting-for-connection, #ips, #automatic-discovery {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
#update-text {
|
||||
margin-top: 20px;
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
#update-view {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#restart-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#spinner {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#update-button {
|
||||
background: blue;
|
||||
padding: 10px 28px;
|
||||
margin-top: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* .button {
|
||||
display: inline-block;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 100px;
|
||||
padding: 18px 16px;
|
||||
gap: 6px;
|
||||
flex: 1 0 0;
|
||||
border-radius: 6px;
|
||||
} */
|
||||
|
||||
|
||||
#progress-container {
|
||||
#update-button-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 8px;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#progress-text {
|
||||
margin-left: 8px;
|
||||
#progress-bar {
|
||||
display: none;
|
||||
|
||||
width: 320px;
|
||||
height: 40px;
|
||||
margin-top: 20px;
|
||||
border-radius: 50px;
|
||||
border: 1px solid #4E4E4E;
|
||||
background: linear-gradient(rgba(20, 20, 20, 0.5), rgba(80, 80, 80, 0.5));
|
||||
/* background-size: cover; */
|
||||
}
|
||||
|
||||
#progress-bar-progress {
|
||||
width: 12%;
|
||||
height: 40px;
|
||||
border-radius: 50px;
|
||||
background-image: linear-gradient(to bottom, #008BD7 35%, #0069AA);
|
||||
transition: width .6s ease;
|
||||
}
|
||||
|
||||
@keyframes progress-bar-stripes {
|
||||
from {
|
||||
background-position: 1rem 0;
|
||||
}
|
||||
to {
|
||||
background-position: 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
#window-can-be-closed {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue