release: Multi-Hoster-Upload 2.1.3

This commit is contained in:
Sucukdeluxe
2026-08-11 00:03:28 +02:00
parent 4b89df17ef
commit 0d9931c4b5
10 changed files with 107 additions and 24 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ Multi-Hoster-Upload is a Windows desktop application for sending file batches to
Download the current Setup or Portable build from the [latest GitHub release](https://github.com/Sucukdeluxe/Multi-Hoster-Upload/releases/latest). Download the current Setup or Portable build from the [latest GitHub release](https://github.com/Sucukdeluxe/Multi-Hoster-Upload/releases/latest).
The latest public release is version 2.1.2. Use the release page for the executables and the full English changelog. The latest public release is version 2.1.3. Use the release page for the executables and the full English changelog.
## Features ## Features
+3 -1
View File
@@ -1,4 +1,6 @@
function configureStartupRenderer(app) { function configureStartupRenderer(app, env = process.env, platform = process.platform) {
const sessionName = String(env && env.SESSIONNAME || '');
if (platform === 'win32' && /^RDP-/i.test(sessionName)) app.disableHardwareAcceleration();
return app; return app;
} }
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "multi-hoster-uploader", "name": "multi-hoster-uploader",
"version": "2.1.2", "version": "2.1.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "multi-hoster-uploader", "name": "multi-hoster-uploader",
"version": "2.1.2", "version": "2.1.3",
"dependencies": { "dependencies": {
"chokidar": "^3.6.0", "chokidar": "^3.6.0",
"undici": "^7.29.0", "undici": "^7.29.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "multi-hoster-uploader", "name": "multi-hoster-uploader",
"version": "2.1.2", "version": "2.1.3",
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously", "description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
+37 -9
View File
@@ -6545,6 +6545,28 @@ function setupListeners() {
} }
// --- Update UI --- // --- Update UI ---
function _formatUpdateReleaseNotes(value) {
const output = [];
let pendingBlank = false;
for (const rawLine of String(value || '').replace(/\r\n?/g, '\n').split('\n')) {
let line = rawLine.trim();
if (!line) {
if (output.length > 0) pendingBlank = true;
continue;
}
line = line
.replace(/^#{1,6}\s+/, '')
.replace(/^[-*+]\s+/, '• ')
.replace(/\*\*([^*]+)\*\*/g, '$1')
.replace(/`([^`]+)`/g, '$1')
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
if (pendingBlank && output.at(-1) !== '') output.push('');
output.push(line);
pendingBlank = false;
}
return output.join('\n');
}
function showUpdateBanner(info) { function showUpdateBanner(info) {
if (!info) return; if (!info) return;
_knownUpdateInfo = { ...info, available: true }; _knownUpdateInfo = { ...info, available: true };
@@ -6561,9 +6583,12 @@ function showUpdateBanner(info) {
const notesBody = document.getElementById('updateReleaseNotesBody'); const notesBody = document.getElementById('updateReleaseNotesBody');
const installButton = document.getElementById('installUpdateBtn'); const installButton = document.getElementById('installUpdateBtn');
if (title) title.textContent = 'Eine neue Version ist verfügbar'; if (title) title.textContent = 'Eine neue Version ist verfügbar';
if (message) message.textContent = `Update v${version} verfügbar`; if (message) {
message.textContent = `Update v${version} verfügbar`;
message.hidden = false;
}
if (notes && notesBody) { if (notes && notesBody) {
const releaseNotes = String(info.releaseNotes || '').trim(); const releaseNotes = _formatUpdateReleaseNotes(info.releaseNotes);
notesBody.textContent = releaseNotes.length > 2400 ? `${releaseNotes.slice(0, 2399)}` : releaseNotes; notesBody.textContent = releaseNotes.length > 2400 ? `${releaseNotes.slice(0, 2399)}` : releaseNotes;
notes.hidden = !releaseNotes; notes.hidden = !releaseNotes;
} }
@@ -6585,38 +6610,41 @@ function handleUpdateProgress(data) {
_updateInstallBusy = true; _updateInstallBusy = true;
_setUpdateDialogBusy(true); _setUpdateDialogBusy(true);
_setUpdateProgress(0, 'Download 0%'); _setUpdateProgress(0, 'Download 0%');
if (message) message.textContent = 'Download wird vorbereitet…'; if (message) message.hidden = true;
if (button) button.textContent = 'Download 0%'; if (button) button.textContent = 'Download 0%';
} else if (progress.stage === 'downloading') { } else if (progress.stage === 'downloading') {
const percent = Math.max(0, Math.min(100, Math.round(Number(progress.percent) || 0))); const percent = Math.max(0, Math.min(100, Math.round(Number(progress.percent) || 0)));
_updateInstallBusy = true; _updateInstallBusy = true;
_setUpdateDialogBusy(true); _setUpdateDialogBusy(true);
_setUpdateProgress(percent, `Download ${percent}%`); _setUpdateProgress(percent, `Download ${percent}%`);
if (message) message.textContent = `Update wird heruntergeladen… ${percent}%`; if (message) message.hidden = true;
if (button) button.textContent = `Download ${percent}%`; if (button) button.textContent = `Download ${percent}%`;
} else if (progress.stage === 'verifying') { } else if (progress.stage === 'verifying') {
_updateInstallBusy = true; _updateInstallBusy = true;
_setUpdateDialogBusy(true); _setUpdateDialogBusy(true);
_setUpdateProgress(100, 'Prüfen…'); _setUpdateProgress(100, 'Prüfen…');
if (message) message.textContent = 'Download wird geprüft…'; if (message) message.hidden = true;
if (button) button.textContent = 'Prüfen…'; if (button) button.textContent = 'Prüfen…';
} else if (progress.stage === 'prepared') { } else if (progress.stage === 'prepared') {
_updateInstallBusy = true; _updateInstallBusy = true;
_setUpdateDialogBusy(true); _setUpdateDialogBusy(true);
_setUpdateProgress(100, 'Neustart…'); _setUpdateProgress(100, 'Neustart…');
if (message) message.textContent = 'Update ist bereit. Einstellungen werden gespeichert…'; if (message) message.hidden = true;
if (button) button.textContent = 'Neustart…'; if (button) button.textContent = 'Neustart…';
} else if (progress.stage === 'launching' || progress.stage === 'done') { } else if (progress.stage === 'launching' || progress.stage === 'done') {
_updateInstallBusy = true; _updateInstallBusy = true;
_setUpdateDialogBusy(true); _setUpdateDialogBusy(true);
_setUpdateProgress(100, 'Neustart…'); _setUpdateProgress(100, 'Neustart…');
if (message) message.textContent = progress.stage === 'done' ? 'Update installiert. Die Anwendung startet neu…' : 'Update wird installiert. Die Anwendung startet neu…'; if (message) message.hidden = true;
if (button) button.textContent = 'Neustart…'; if (button) button.textContent = 'Neustart…';
} else if (progress.stage === 'error') { } else if (progress.stage === 'error') {
_updateInstallBusy = false; _updateInstallBusy = false;
_setUpdateDialogBusy(false); _setUpdateDialogBusy(false);
_setUpdateProgress(0, 'Update fehlgeschlagen'); _setUpdateProgress(0, 'Update fehlgeschlagen');
if (message) message.textContent = `Update fehlgeschlagen: ${String(progress.error || 'Unbekannter Fehler').slice(0, 400)}`; if (message) {
message.hidden = false;
message.textContent = `Update fehlgeschlagen: ${String(progress.error || 'Unbekannter Fehler').slice(0, 400)}`;
}
if (button) { if (button) {
button.disabled = false; button.disabled = false;
button.textContent = 'Wiederholen'; button.textContent = 'Wiederholen';
@@ -6761,7 +6789,7 @@ async function installKnownUpdate() {
_setUpdateProgress(0, 'Download 0%'); _setUpdateProgress(0, 'Download 0%');
const message = document.getElementById('updateMessage'); const message = document.getElementById('updateMessage');
const button = document.getElementById('installUpdateBtn'); const button = document.getElementById('installUpdateBtn');
if (message) message.textContent = 'Download wird vorbereitet…'; if (message) message.hidden = true;
if (button) button.textContent = 'Download 0%'; if (button) button.textContent = 'Download 0%';
try { try {
await persistQueueStateNow(); await persistQueueStateNow();
-1
View File
@@ -2286,7 +2286,6 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; }
color: var(--text-muted); color: var(--text-muted);
font-size: 11px; font-size: 11px;
line-height: 1.55; line-height: 1.55;
white-space: pre-wrap;
} }
.update-release-notes-title { .update-release-notes-title {
+15 -4
View File
@@ -10,8 +10,9 @@ module.exports = async function afterPack(context) {
} }
const productFilename = context.packager?.appInfo?.productFilename; const productFilename = context.packager?.appInfo?.productFilename;
if (!productFilename) { const version = context.packager?.appInfo?.version;
console.warn(" rcedit: skipped - productFilename not available"); if (!productFilename || !version) {
console.warn(" rcedit: skipped - application metadata not available");
return; return;
} }
@@ -24,8 +25,18 @@ module.exports = async function afterPack(context) {
console.warn(" rcedit: skipped - app_icon.ico not found"); console.warn(" rcedit: skipped - app_icon.ico not found");
return; return;
} }
console.log(` rcedit: patching icon -> ${exePath}`); console.log(` rcedit: branding executable -> ${exePath}`);
await rcedit(exePath, { icon: iconPath }); await rcedit(exePath, {
icon: iconPath,
"file-version": version,
"product-version": version,
"version-string": {
FileDescription: "Multi Hoster Uploader",
InternalName: productFilename,
OriginalFilename: `${productFilename}.exe`,
ProductName: "Multi Hoster Uploader"
}
});
} catch (error) { } catch (error) {
console.warn(` rcedit: failed - ${String(error)}`); console.warn(` rcedit: failed - ${String(error)}`);
} }
+34
View File
@@ -1,6 +1,7 @@
const test = require('node:test'); const test = require('node:test');
const assert = require('node:assert/strict'); const assert = require('node:assert/strict');
const Module = require('node:module'); const Module = require('node:module');
const path = require('node:path');
const packageJson = require('../package.json'); const packageJson = require('../package.json');
test('packages every Electron preload referenced by the main process', () => { test('packages every Electron preload referenced by the main process', () => {
@@ -8,6 +9,39 @@ test('packages every Electron preload referenced by the main process', () => {
assert.ok(packageJson.build.files.includes('preload-drop-target.js')); assert.ok(packageJson.build.files.includes('preload-drop-target.js'));
}); });
test('afterPack brands the executable metadata shown by Windows', async () => {
let editCall = null;
const originalLoad = Module._load;
const afterPackPath = require.resolve('../scripts/afterPack.cjs');
delete require.cache[afterPackPath];
Module._load = function (request, parent, isMain) {
if (request === 'rcedit') {
return async (exePath, options) => { editCall = { exePath, options }; };
}
return originalLoad.call(this, request, parent, isMain);
};
try {
const afterPack = require(afterPackPath);
await afterPack({
appOutDir: 'C:\\release',
packager: { appInfo: { productFilename: 'Multi-Hoster-Upload', version: '9.8.7' } }
});
} finally {
Module._load = originalLoad;
delete require.cache[afterPackPath];
}
assert.equal(editCall.exePath, path.join('C:\\release', 'Multi-Hoster-Upload.exe'));
assert.equal(editCall.options['file-version'], '9.8.7');
assert.equal(editCall.options['product-version'], '9.8.7');
assert.deepEqual(editCall.options['version-string'], {
FileDescription: 'Multi Hoster Uploader',
InternalName: 'Multi-Hoster-Upload',
OriginalFilename: 'Multi-Hoster-Upload.exe',
ProductName: 'Multi Hoster Uploader'
});
});
test('close readiness is signaled only after the renderer explicitly finishes initialization', () => { test('close readiness is signaled only after the renderer explicitly finishes initialization', () => {
const listeners = new Map(); const listeners = new Map();
const sent = []; const sent = [];
+8 -2
View File
@@ -29,12 +29,18 @@ class TestBrowserWindow extends EventEmitter {
} }
} }
test('configureStartupRenderer leaves hardware acceleration enabled', () => { test('configureStartupRenderer leaves hardware acceleration enabled for a local Windows session', () => {
let calls = 0; let calls = 0;
configureStartupRenderer({ disableHardwareAcceleration() { calls++; } }); configureStartupRenderer({ disableHardwareAcceleration() { calls++; } }, { SESSIONNAME: 'Console' }, 'win32');
assert.equal(calls, 0); assert.equal(calls, 0);
}); });
test('configureStartupRenderer disables hardware acceleration for a Windows Remote Desktop session', () => {
let calls = 0;
configureStartupRenderer({ disableHardwareAcceleration() { calls++; } }, { SESSIONNAME: 'RDP-Tcp#12' }, 'win32');
assert.equal(calls, 1);
});
test('createStartupWindow forces the main window to start hidden', () => { test('createStartupWindow forces the main window to start hidden', () => {
const startup = createStartupWindow(TestBrowserWindow, { width: 1100, show: true }); const startup = createStartupWindow(TestBrowserWindow, { width: 1100, show: true });
+6 -3
View File
@@ -1423,7 +1423,7 @@ setTimeout(async () => {
check('Sidebar indicator stays aligned at the minimum window size', queueProgressVisibility.minimum.sidebarIndicatorAligned); check('Sidebar indicator stays aligned at the minimum window size', queueProgressVisibility.minimum.sidebarIndicatorAligned);
check('Minimum window keeps the settings header compact', compactSettingsHeader <= 58); check('Minimum window keeps the settings header compact', compactSettingsHeader <= 58);
const updateOverlayState = await wc.executeJavaScript('_knownUpdateInfo = { available: true, remoteVersion: "9.9.9" }; _syncHeaderUpdateState(); document.getElementById("headerUpdateBtn").focus(); showUpdateBanner({ remoteVersion: "9.9.9", releaseNotes: "Added live language switching.\\\\nImproved settings layout." }); (() => { const overlay = document.getElementById("updateBanner"); const dialog = overlay?.querySelector(".update-dialog"); const button = document.getElementById("headerUpdateBtn"); return [overlay?.classList.contains("update-overlay"), overlay?.style.display, dialog?.getAttribute("role"), dialog?.getAttribute("aria-modal"), button?.hidden, getComputedStyle(button).display].join("|"); })()'); const updateOverlayState = await wc.executeJavaScript('_knownUpdateInfo = { available: true, remoteVersion: "9.9.9" }; _syncHeaderUpdateState(); document.getElementById("headerUpdateBtn").focus(); showUpdateBanner({ remoteVersion: "9.9.9", releaseNotes: "\\\\n\\\\n\\\\n## New in this version\\\\n\\\\n\\\\n### Menus and navigation\\\\n\\\\n- Added live language switching.\\\\n- Improved settings layout.\\\\n\\\\n\\\\n" }); (() => { const overlay = document.getElementById("updateBanner"); const dialog = overlay?.querySelector(".update-dialog"); const button = document.getElementById("headerUpdateBtn"); return [overlay?.classList.contains("update-overlay"), overlay?.style.display, dialog?.getAttribute("role"), dialog?.getAttribute("aria-modal"), button?.hidden, getComputedStyle(button).display].join("|"); })()');
check('Available update opens an accessible update dialog', updateOverlayState === 'true|flex|dialog|true|false|flex'); check('Available update opens an accessible update dialog', updateOverlayState === 'true|flex|dialog|true|false|flex');
await new Promise(resolve => setTimeout(resolve, 100)); await new Promise(resolve => setTimeout(resolve, 100));
@@ -1450,8 +1450,8 @@ setTimeout(async () => {
const updateDialogActions = await wc.executeJavaScript('[document.getElementById("dismissUpdateBtn")?.textContent?.trim(), document.getElementById("installUpdateBtn")?.textContent?.trim()].join("|")'); const updateDialogActions = await wc.executeJavaScript('[document.getElementById("dismissUpdateBtn")?.textContent?.trim(), document.getElementById("installUpdateBtn")?.textContent?.trim()].join("|")');
check('Update dialog offers cancel and install actions', updateDialogActions === 'Abbrechen|Jetzt installieren'); check('Update dialog offers cancel and install actions', updateDialogActions === 'Abbrechen|Jetzt installieren');
const updateDialogChangelog = await wc.executeJavaScript('document.getElementById("updateReleaseNotes")?.hidden + "|" + document.querySelector(".update-release-notes-title")?.textContent?.trim() + "|" + document.getElementById("updateReleaseNotesBody")?.textContent'); const updateDialogChangelog = await wc.executeJavaScript('(() => { const title = document.querySelector(".update-release-notes-title"); const body = document.getElementById("updateReleaseNotesBody"); const titleRect = title?.getBoundingClientRect(); const bodyRect = body?.getBoundingClientRect(); return { hidden: document.getElementById("updateReleaseNotes")?.hidden, title: title?.textContent?.trim(), body: body?.textContent, gap: bodyRect && titleRect ? bodyRect.top - titleRect.bottom : null }; })()');
check('Update dialog shows the GitHub changelog with real line breaks', updateDialogChangelog === 'false|Changelog|Added live language switching.\\nImproved settings layout.'); check('Update dialog renders a compact normalized changelog', updateDialogChangelog.hidden === false && updateDialogChangelog.title === 'Changelog' && updateDialogChangelog.body === 'New in this version\\n\\nMenus and navigation\\n\\n• Added live language switching.\\nImproved settings layout.' && updateDialogChangelog.gap <= 10);
const updateHeaderHint = await wc.executeJavaScript('(() => { const button = document.getElementById("headerUpdateBtn"); return [button?.textContent?.trim(), button?.getAttribute("aria-label"), button?.dataset.tooltip].join("|"); })()'); const updateHeaderHint = await wc.executeJavaScript('(() => { const button = document.getElementById("headerUpdateBtn"); return [button?.textContent?.trim(), button?.getAttribute("aria-label"), button?.dataset.tooltip].join("|"); })()');
check('Available update gives the header action a matching hint', updateHeaderHint === 'Update verfügbar|Update v9.9.9 verfügbar. Klicken zum Installieren.|Update v9.9.9 verfügbar. Klicken zum Installieren.'); check('Available update gives the header action a matching hint', updateHeaderHint === 'Update verfügbar|Update v9.9.9 verfügbar. Klicken zum Installieren.|Update v9.9.9 verfügbar. Klicken zum Installieren.');
@@ -1475,12 +1475,15 @@ setTimeout(async () => {
closeDisabled: document.getElementById('updateCloseBtn').disabled, closeDisabled: document.getElementById('updateCloseBtn').disabled,
dismissDisabled: document.getElementById('dismissUpdateBtn').disabled, dismissDisabled: document.getElementById('dismissUpdateBtn').disabled,
headerHidden: header.hidden, headerHidden: header.hidden,
messageHidden: document.getElementById('updateMessage').hidden,
messageText: document.getElementById('updateMessage').textContent,
progressLabel: progress.getAttribute('aria-label'), progressLabel: progress.getAttribute('aria-label'),
progressText: progress.getAttribute('aria-valuetext') progressText: progress.getAttribute('aria-valuetext')
}; };
})()\`); })()\`);
check('Busy update keeps its progress dialog open', busyUpdateState.display === 'flex' && busyUpdateState.hidden === 'false' && busyUpdateState.closeDisabled === true && busyUpdateState.dismissDisabled === true && busyUpdateState.headerHidden === false); check('Busy update keeps its progress dialog open', busyUpdateState.display === 'flex' && busyUpdateState.hidden === 'false' && busyUpdateState.closeDisabled === true && busyUpdateState.dismissDisabled === true && busyUpdateState.headerHidden === false);
check('Update progress exposes an accessible live value', busyUpdateState.progressLabel === 'Update-Fortschritt' && busyUpdateState.progressText === 'Download 50%'); check('Update progress exposes an accessible live value', busyUpdateState.progressLabel === 'Update-Fortschritt' && busyUpdateState.progressText === 'Download 50%');
check('Busy update shows progress only below the bar', busyUpdateState.messageHidden === true && busyUpdateState.messageText === 'Update v9.9.9 verfügbar');
const updateErrorRecovery = await wc.executeJavaScript('handleUpdateProgress({ stage: "error", error: "Netzwerkfehler" }); document.getElementById("dismissUpdateBtn").click(); document.getElementById("updateBanner").style.display + "|" + document.getElementById("updateCloseBtn").disabled + "|" + document.getElementById("dismissUpdateBtn").disabled + "|" + document.getElementById("headerUpdateBtn").hidden'); const updateErrorRecovery = await wc.executeJavaScript('handleUpdateProgress({ stage: "error", error: "Netzwerkfehler" }); document.getElementById("dismissUpdateBtn").click(); document.getElementById("updateBanner").style.display + "|" + document.getElementById("updateCloseBtn").disabled + "|" + document.getElementById("dismissUpdateBtn").disabled + "|" + document.getElementById("headerUpdateBtn").hidden');
check('Update errors restore all close actions', updateErrorRecovery === 'none|false|false|false'); check('Update errors restore all close actions', updateErrorRecovery === 'none|false|false|false');