feat: improve live update and upload feedback
CI / verify (push) Has been cancelled

Refresh release metadata immediately before downloading an update and keep automatic checks current while the app remains open.

Accept desktop file drops from the first renderer frame, preserve early and rapid drops until initialization completes, and show the live remaining upload size in the sidebar.

Add regression coverage for update freshness, startup drag-and-drop ordering, localization, and remaining-byte retry behavior. Bump the application to v2.1.25.
This commit is contained in:
Sucukdeluxe
2026-08-20 10:40:58 +02:00
parent d7c9f287e4
commit c4c7907686
16 changed files with 351 additions and 82 deletions
+48 -12
View File
@@ -17,6 +17,27 @@ const CACHE_TTL = 10 * 60 * 1000; // 10 min
let activeAbort = null;
const launchedInstallerPaths = new Set();
function createUpdateAnnouncementState() {
let announcedVersion = '';
function versionOf(update) {
return String(update?.remoteVersion || '').replace(/^v/i, '').trim();
}
return Object.freeze({
canAnnounce(update, rendererReady) {
const version = versionOf(update);
return Boolean(rendererReady && update?.available && version && version !== announcedVersion);
},
markAnnounced(update) {
announcedVersion = versionOf(update);
},
reset() {
announcedVersion = '';
}
});
}
function getCurrentVersion() {
return app.getVersion();
}
@@ -61,17 +82,32 @@ function findLatestYml(assets) {
return assets.find(a => /^latest\.yml$/i.test(a.name)) || null;
}
async function fetchJson(url, signal) {
function cacheBustedUrl(url) {
const parsed = new URL(url);
parsed.searchParams.set('_mhu_update', crypto.randomUUID());
return parsed.toString();
}
async function fetchJson(url, signal, options = {}) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), CHECK_TIMEOUT);
const onAbort = () => controller.abort();
const forceRefresh = options.forceRefresh === true;
const fetchImpl = options.fetchImpl || fetch;
if (signal) signal.addEventListener('abort', onAbort);
try {
const res = await fetch(url, {
const res = await fetchImpl(forceRefresh ? cacheBustedUrl(url) : url, {
method: 'GET',
signal: controller.signal,
redirect: 'follow'
redirect: 'follow',
...(forceRefresh ? {
cache: 'no-store',
headers: {
'Cache-Control': 'no-cache, no-store',
Pragma: 'no-cache'
}
} : {})
});
const text = await res.text();
try {
@@ -112,13 +148,14 @@ async function fetchGithubReleaseNotes(remoteVersion, fallback = '', fetchImpl =
}
}
async function checkForUpdate() {
// Return cached result if fresh
if (cachedCheck && (Date.now() - cachedCheckTs) < CACHE_TTL) {
async function checkForUpdate(options = {}) {
const forceRefresh = options.forceRefresh === true;
const fetchImpl = options.fetchImpl || fetch;
if (!forceRefresh && cachedCheck && (Date.now() - cachedCheckTs) < CACHE_TTL) {
return cachedCheck;
}
const releases = await fetchJson(API_URL);
const releases = await fetchJson(API_URL, undefined, { forceRefresh, fetchImpl });
if (!Array.isArray(releases) || releases.length === 0) {
return { available: false };
@@ -142,7 +179,7 @@ async function checkForUpdate() {
return { available: false, reason: 'Kein Setup-Asset im Release gefunden' };
}
const releaseNotes = await fetchGithubReleaseNotes(remoteVersion, release.body || '');
const releaseNotes = await fetchGithubReleaseNotes(remoteVersion, release.body || '', fetchImpl);
cachedCheck = {
available: true,
currentVersion,
@@ -210,10 +247,9 @@ async function prepareUpdate(onProgress, options = {}) {
// Stage: starting
if (onProgress) onProgress({ stage: 'starting', percent: 0 });
// Check or use cached
let check = options.checkResult || cachedCheck;
let check = options.checkResult || null;
if (!check || !check.available) {
check = await checkForUpdate();
check = await checkForUpdate({ forceRefresh: true, fetchImpl });
}
if (!check || !check.available) {
throw new Error('Kein Update verfügbar');
@@ -353,4 +389,4 @@ function abortUpdate() {
}
}
module.exports = { checkForUpdate, fetchGithubReleaseNotes, prepareUpdate, launchPreparedUpdate, abortUpdate, isNewer, resolveReleaseVersion, pickSetupAsset, parseLatestYml };
module.exports = { checkForUpdate, fetchGithubReleaseNotes, prepareUpdate, launchPreparedUpdate, abortUpdate, isNewer, resolveReleaseVersion, pickSetupAsset, parseLatestYml, createUpdateAnnouncementState };