mirror of
https://github.com/Pandipipas/scoreko-electron-dev.git
synced 2026-06-06 05:32:06 +00:00
feat: Implement update management refactor with new dialog and settings handling
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import { BrowserWindow, dialog } from "electron";
|
||||
import type { MessageBoxOptions } from "electron";
|
||||
|
||||
import { ReleaseUpdate } from "./update-utils";
|
||||
|
||||
export type DownloadUpdateChoice = "download" | "open-release" | "dismiss";
|
||||
|
||||
export async function askToDownloadUpdate(
|
||||
update: ReleaseUpdate,
|
||||
releasePageUrl: string | undefined,
|
||||
parentWindow: BrowserWindow | null,
|
||||
): Promise<DownloadUpdateChoice> {
|
||||
const result = await showMessageBox(parentWindow, {
|
||||
type: "info",
|
||||
title: "Actualización disponible",
|
||||
message: `Scoreko ${update.version} está disponible.`,
|
||||
detail: "Puedes descargarla ahora o seguir usando esta versión.",
|
||||
buttons: releasePageUrl ? ["Descargar", "Ver release", "Ahora no"] : ["Descargar", "Ahora no"],
|
||||
defaultId: 0,
|
||||
cancelId: releasePageUrl ? 2 : 1,
|
||||
});
|
||||
|
||||
if (releasePageUrl && result.response === 1) {
|
||||
return "open-release";
|
||||
}
|
||||
|
||||
return result.response === 0 ? "download" : "dismiss";
|
||||
}
|
||||
|
||||
export async function askToInstallUpdate(update: ReleaseUpdate, parentWindow: BrowserWindow | null): Promise<boolean> {
|
||||
const result = await showMessageBox(parentWindow, {
|
||||
type: "question",
|
||||
title: "Actualización descargada",
|
||||
message: `Scoreko ${update.version} se ha descargado.`,
|
||||
detail: "Para instalarla se cerrará Scoreko y se abrirá el instalador.",
|
||||
buttons: ["Instalar y cerrar", "Luego"],
|
||||
defaultId: 0,
|
||||
cancelId: 1,
|
||||
});
|
||||
|
||||
return result.response === 0;
|
||||
}
|
||||
|
||||
function showMessageBox(parentWindow: BrowserWindow | null, options: MessageBoxOptions) {
|
||||
return parentWindow ? dialog.showMessageBox(parentWindow, options) : dialog.showMessageBox(options);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { Readable } from "node:stream";
|
||||
|
||||
import { ReleaseUpdate, sanitizeFileName } from "./update-utils";
|
||||
|
||||
type UpdateDownloadConfig = {
|
||||
tempDirectory: string;
|
||||
};
|
||||
|
||||
export async function downloadInstaller(update: ReleaseUpdate, config: UpdateDownloadConfig): Promise<string> {
|
||||
const safeFileName = sanitizeFileName(update.installer.name);
|
||||
const downloadDirectory = path.join(config.tempDirectory, "scoreko-updates");
|
||||
const targetPath = path.join(downloadDirectory, safeFileName);
|
||||
|
||||
fs.mkdirSync(downloadDirectory, { recursive: true });
|
||||
|
||||
const response = await fetch(update.installer.downloadUrl);
|
||||
if (!response.ok || !response.body) {
|
||||
throw new Error(`Could not download update installer. HTTP ${response.status}.`);
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const fileStream = fs.createWriteStream(targetPath);
|
||||
const responseStream = Readable.fromWeb(response.body as Parameters<typeof Readable.fromWeb>[0]);
|
||||
|
||||
responseStream.on("error", reject);
|
||||
fileStream.on("error", reject);
|
||||
fileStream.on("finish", resolve);
|
||||
responseStream.pipe(fileStream);
|
||||
});
|
||||
|
||||
return targetPath;
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
import { app, BrowserWindow, dialog, shell } from "electron";
|
||||
import type { MessageBoxOptions } from "electron";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { Readable } from "node:stream";
|
||||
import { app, BrowserWindow, shell } from "electron";
|
||||
|
||||
import { AppRuntimeConfig } from "../config/runtime-config";
|
||||
import { buildReleaseUpdate, GiteaRelease, ReleaseUpdate, sanitizeFileName, UpdateFileConfig } from "./update-utils";
|
||||
import { askToDownloadUpdate, askToInstallUpdate } from "./update-dialogs";
|
||||
import { downloadInstaller } from "./update-download";
|
||||
import { loadUpdateSettings, UpdateSettings } from "./update-settings";
|
||||
import { buildReleaseUpdate, GiteaRelease } from "./update-utils";
|
||||
|
||||
type UpdateManagerConfig = {
|
||||
appConfig: AppRuntimeConfig;
|
||||
@@ -15,13 +14,6 @@ type UpdateManagerConfig = {
|
||||
log: (...args: unknown[]) => void;
|
||||
};
|
||||
|
||||
type UpdateSettings = {
|
||||
enabled: boolean;
|
||||
apiUrl?: string;
|
||||
releasePageUrl?: string;
|
||||
assetPattern: string;
|
||||
};
|
||||
|
||||
export function scheduleUpdateCheck({
|
||||
appConfig,
|
||||
rootPath,
|
||||
@@ -65,16 +57,19 @@ async function checkForUpdates({
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldDownload = await askToDownloadUpdate(
|
||||
update,
|
||||
settings.releasePageUrl ?? update.pageUrl,
|
||||
getParentWindow(),
|
||||
);
|
||||
if (!shouldDownload) {
|
||||
const releasePageUrl = settings.releasePageUrl ?? update.pageUrl;
|
||||
const downloadChoice = await askToDownloadUpdate(update, releasePageUrl, getParentWindow());
|
||||
|
||||
if (downloadChoice === "open-release") {
|
||||
await openReleasePage(releasePageUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
const installerPath = await downloadInstaller(update);
|
||||
if (downloadChoice !== "download") {
|
||||
return;
|
||||
}
|
||||
|
||||
const installerPath = await downloadInstaller(update, { tempDirectory: app.getPath("temp") });
|
||||
const shouldInstall = await askToInstallUpdate(update, getParentWindow());
|
||||
if (!shouldInstall) {
|
||||
await shell.showItemInFolder(installerPath);
|
||||
@@ -93,41 +88,6 @@ async function checkForUpdates({
|
||||
}
|
||||
}
|
||||
|
||||
function loadUpdateSettings(
|
||||
appConfig: AppRuntimeConfig,
|
||||
rootPath: string,
|
||||
log: (...args: unknown[]) => void,
|
||||
): UpdateSettings {
|
||||
const fileConfig = readUpdateFileConfig(appConfig, rootPath, log);
|
||||
|
||||
return {
|
||||
enabled: appConfig.updatesEnabled && (Boolean(fileConfig.enabled) || Boolean(appConfig.updateApiUrl)),
|
||||
apiUrl: appConfig.updateApiUrl ?? readOptionalString(fileConfig.apiUrl),
|
||||
releasePageUrl: appConfig.updateReleasePageUrl ?? readOptionalString(fileConfig.releasePageUrl),
|
||||
assetPattern:
|
||||
appConfig.updateAssetPattern || readOptionalString(fileConfig.assetPattern) || "Scoreko-setup-.*\\.exe$",
|
||||
};
|
||||
}
|
||||
|
||||
function readUpdateFileConfig(
|
||||
appConfig: AppRuntimeConfig,
|
||||
rootPath: string,
|
||||
log: (...args: unknown[]) => void,
|
||||
): UpdateFileConfig {
|
||||
const configPath = appConfig.updateConfigPathOverride ?? path.join(rootPath, "static", "updates.json");
|
||||
|
||||
if (!fs.existsSync(configPath)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(configPath, "utf8")) as UpdateFileConfig;
|
||||
} catch (error) {
|
||||
log(`Could not read update config at ${configPath}.`, error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchLatestRelease(apiUrl: string): Promise<GiteaRelease> {
|
||||
const response = await fetch(apiUrl, {
|
||||
headers: {
|
||||
@@ -142,72 +102,8 @@ async function fetchLatestRelease(apiUrl: string): Promise<GiteaRelease> {
|
||||
return (await response.json()) as GiteaRelease;
|
||||
}
|
||||
|
||||
async function askToDownloadUpdate(
|
||||
update: ReleaseUpdate,
|
||||
releasePageUrl: string | undefined,
|
||||
parentWindow: BrowserWindow | null,
|
||||
): Promise<boolean> {
|
||||
const result = await showMessageBox(parentWindow, {
|
||||
type: "info",
|
||||
title: "Actualización disponible",
|
||||
message: `Scoreko ${update.version} está disponible.`,
|
||||
detail: "Puedes descargarla ahora o seguir usando esta versión.",
|
||||
buttons: releasePageUrl ? ["Descargar", "Ver release", "Ahora no"] : ["Descargar", "Ahora no"],
|
||||
defaultId: 0,
|
||||
cancelId: releasePageUrl ? 2 : 1,
|
||||
});
|
||||
|
||||
if (releasePageUrl && result.response === 1) {
|
||||
async function openReleasePage(releasePageUrl: string | undefined): Promise<void> {
|
||||
if (releasePageUrl) {
|
||||
await shell.openExternal(releasePageUrl);
|
||||
return false;
|
||||
}
|
||||
|
||||
return result.response === 0;
|
||||
}
|
||||
|
||||
async function askToInstallUpdate(update: ReleaseUpdate, parentWindow: BrowserWindow | null): Promise<boolean> {
|
||||
const result = await showMessageBox(parentWindow, {
|
||||
type: "question",
|
||||
title: "Actualización descargada",
|
||||
message: `Scoreko ${update.version} se ha descargado.`,
|
||||
detail: "Para instalarla se cerrará Scoreko y se abrirá el instalador.",
|
||||
buttons: ["Instalar y cerrar", "Luego"],
|
||||
defaultId: 0,
|
||||
cancelId: 1,
|
||||
});
|
||||
|
||||
return result.response === 0;
|
||||
}
|
||||
|
||||
async function downloadInstaller(update: ReleaseUpdate): Promise<string> {
|
||||
const safeFileName = sanitizeFileName(update.installer.name);
|
||||
const downloadDirectory = path.join(app.getPath("temp"), "scoreko-updates");
|
||||
const targetPath = path.join(downloadDirectory, safeFileName);
|
||||
|
||||
fs.mkdirSync(downloadDirectory, { recursive: true });
|
||||
|
||||
const response = await fetch(update.installer.downloadUrl);
|
||||
if (!response.ok || !response.body) {
|
||||
throw new Error(`Could not download update installer. HTTP ${response.status}.`);
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const fileStream = fs.createWriteStream(targetPath);
|
||||
const responseStream = Readable.fromWeb(response.body as Parameters<typeof Readable.fromWeb>[0]);
|
||||
|
||||
responseStream.on("error", reject);
|
||||
fileStream.on("error", reject);
|
||||
fileStream.on("finish", resolve);
|
||||
responseStream.pipe(fileStream);
|
||||
});
|
||||
|
||||
return targetPath;
|
||||
}
|
||||
|
||||
function readOptionalString(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
function showMessageBox(parentWindow: BrowserWindow | null, options: MessageBoxOptions) {
|
||||
return parentWindow ? dialog.showMessageBox(parentWindow, options) : dialog.showMessageBox(options);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import { AppRuntimeConfig } from "../config/runtime-config";
|
||||
import { UpdateFileConfig } from "./update-utils";
|
||||
|
||||
const DEFAULT_UPDATE_ASSET_PATTERN = "Scoreko-setup-.*\\.exe$";
|
||||
|
||||
export type UpdateSettings = {
|
||||
enabled: boolean;
|
||||
apiUrl?: string;
|
||||
releasePageUrl?: string;
|
||||
assetPattern: string;
|
||||
};
|
||||
|
||||
export function loadUpdateSettings(
|
||||
appConfig: AppRuntimeConfig,
|
||||
rootPath: string,
|
||||
log: (...args: unknown[]) => void,
|
||||
): UpdateSettings {
|
||||
const fileConfig = readUpdateFileConfig(appConfig, rootPath, log);
|
||||
|
||||
return {
|
||||
enabled: appConfig.updatesEnabled && (Boolean(fileConfig.enabled) || Boolean(appConfig.updateApiUrl)),
|
||||
apiUrl: appConfig.updateApiUrl ?? readOptionalString(fileConfig.apiUrl),
|
||||
releasePageUrl: appConfig.updateReleasePageUrl ?? readOptionalString(fileConfig.releasePageUrl),
|
||||
assetPattern:
|
||||
appConfig.updateAssetPattern || readOptionalString(fileConfig.assetPattern) || DEFAULT_UPDATE_ASSET_PATTERN,
|
||||
};
|
||||
}
|
||||
|
||||
export function readUpdateFileConfig(
|
||||
appConfig: AppRuntimeConfig,
|
||||
rootPath: string,
|
||||
log: (...args: unknown[]) => void,
|
||||
): UpdateFileConfig {
|
||||
const configPath = appConfig.updateConfigPathOverride ?? path.join(rootPath, "static", "updates.json");
|
||||
|
||||
if (!fs.existsSync(configPath)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedConfig: unknown = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
||||
return normalizeUpdateFileConfig(parsedConfig);
|
||||
} catch (error) {
|
||||
log(`Could not read update config at ${configPath}.`, error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeUpdateFileConfig(value: unknown): UpdateFileConfig {
|
||||
if (!isRecord(value)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
enabled: value.enabled,
|
||||
apiUrl: value.apiUrl,
|
||||
releasePageUrl: value.releasePageUrl,
|
||||
assetPattern: value.assetPattern,
|
||||
};
|
||||
}
|
||||
|
||||
function readOptionalString(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
Reference in New Issue
Block a user