1
0
Fork 0
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:
Michael Hollister 2024-11-17 23:12:24 -06:00
parent a967b7983a
commit 568c972492
6 changed files with 358 additions and 146 deletions

View file

@ -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
});
}
}

View file

@ -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;
}
}

View file

@ -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,
});

View file

@ -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(); };

View file

@ -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>

View file

@ -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 {