diff --git a/README.md b/README.md index ffacd87..9a864eb 100644 --- a/README.md +++ b/README.md @@ -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). -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 diff --git a/lib/startup-renderer.js b/lib/startup-renderer.js index 8b6722f..ec6e844 100644 --- a/lib/startup-renderer.js +++ b/lib/startup-renderer.js @@ -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; } diff --git a/package-lock.json b/package-lock.json index fbdb06d..d8843d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "multi-hoster-uploader", - "version": "2.1.2", + "version": "2.1.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "multi-hoster-uploader", - "version": "2.1.2", + "version": "2.1.3", "dependencies": { "chokidar": "^3.6.0", "undici": "^7.29.0", diff --git a/package.json b/package.json index 482417e..e58050c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "multi-hoster-uploader", - "version": "2.1.2", + "version": "2.1.3", "description": "Upload files to doodstream, voe, vidmoly, byse simultaneously", "main": "main.js", "scripts": { diff --git a/renderer/app.js b/renderer/app.js index 89ea617..6e496a9 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -6545,6 +6545,28 @@ function setupListeners() { } // --- 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) { if (!info) return; _knownUpdateInfo = { ...info, available: true }; @@ -6561,9 +6583,12 @@ function showUpdateBanner(info) { const notesBody = document.getElementById('updateReleaseNotesBody'); const installButton = document.getElementById('installUpdateBtn'); 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) { - const releaseNotes = String(info.releaseNotes || '').trim(); + const releaseNotes = _formatUpdateReleaseNotes(info.releaseNotes); notesBody.textContent = releaseNotes.length > 2400 ? `${releaseNotes.slice(0, 2399)}…` : releaseNotes; notes.hidden = !releaseNotes; } @@ -6585,38 +6610,41 @@ function handleUpdateProgress(data) { _updateInstallBusy = true; _setUpdateDialogBusy(true); _setUpdateProgress(0, 'Download 0%'); - if (message) message.textContent = 'Download wird vorbereitet…'; + if (message) message.hidden = true; if (button) button.textContent = 'Download 0%'; } else if (progress.stage === 'downloading') { const percent = Math.max(0, Math.min(100, Math.round(Number(progress.percent) || 0))); _updateInstallBusy = true; _setUpdateDialogBusy(true); _setUpdateProgress(percent, `Download ${percent}%`); - if (message) message.textContent = `Update wird heruntergeladen… ${percent}%`; + if (message) message.hidden = true; if (button) button.textContent = `Download ${percent}%`; } else if (progress.stage === 'verifying') { _updateInstallBusy = true; _setUpdateDialogBusy(true); _setUpdateProgress(100, 'Prüfen…'); - if (message) message.textContent = 'Download wird geprüft…'; + if (message) message.hidden = true; if (button) button.textContent = 'Prüfen…'; } else if (progress.stage === 'prepared') { _updateInstallBusy = true; _setUpdateDialogBusy(true); _setUpdateProgress(100, 'Neustart…'); - if (message) message.textContent = 'Update ist bereit. Einstellungen werden gespeichert…'; + if (message) message.hidden = true; if (button) button.textContent = 'Neustart…'; } else if (progress.stage === 'launching' || progress.stage === 'done') { _updateInstallBusy = true; _setUpdateDialogBusy(true); _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…'; } else if (progress.stage === 'error') { _updateInstallBusy = false; _setUpdateDialogBusy(false); _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) { button.disabled = false; button.textContent = 'Wiederholen'; @@ -6761,7 +6789,7 @@ async function installKnownUpdate() { _setUpdateProgress(0, 'Download 0%'); const message = document.getElementById('updateMessage'); const button = document.getElementById('installUpdateBtn'); - if (message) message.textContent = 'Download wird vorbereitet…'; + if (message) message.hidden = true; if (button) button.textContent = 'Download 0%'; try { await persistQueueStateNow(); diff --git a/renderer/styles.css b/renderer/styles.css index 2f5654a..f0e3822 100644 --- a/renderer/styles.css +++ b/renderer/styles.css @@ -2286,7 +2286,6 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; } color: var(--text-muted); font-size: 11px; line-height: 1.55; - white-space: pre-wrap; } .update-release-notes-title { diff --git a/scripts/afterPack.cjs b/scripts/afterPack.cjs index 98e9acf..67832aa 100644 --- a/scripts/afterPack.cjs +++ b/scripts/afterPack.cjs @@ -10,8 +10,9 @@ module.exports = async function afterPack(context) { } const productFilename = context.packager?.appInfo?.productFilename; - if (!productFilename) { - console.warn(" rcedit: skipped - productFilename not available"); + const version = context.packager?.appInfo?.version; + if (!productFilename || !version) { + console.warn(" rcedit: skipped - application metadata not available"); return; } @@ -24,8 +25,18 @@ module.exports = async function afterPack(context) { console.warn(" rcedit: skipped - app_icon.ico not found"); return; } - console.log(` rcedit: patching icon -> ${exePath}`); - await rcedit(exePath, { icon: iconPath }); + console.log(` rcedit: branding executable -> ${exePath}`); + 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) { console.warn(` rcedit: failed - ${String(error)}`); } diff --git a/tests/package-build-files.test.js b/tests/package-build-files.test.js index b801e3e..823c234 100644 --- a/tests/package-build-files.test.js +++ b/tests/package-build-files.test.js @@ -1,6 +1,7 @@ const test = require('node:test'); const assert = require('node:assert/strict'); const Module = require('node:module'); +const path = require('node:path'); const packageJson = require('../package.json'); 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')); }); +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', () => { const listeners = new Map(); const sent = []; diff --git a/tests/startup-renderer.test.js b/tests/startup-renderer.test.js index bf96be5..873d275 100644 --- a/tests/startup-renderer.test.js +++ b/tests/startup-renderer.test.js @@ -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; - configureStartupRenderer({ disableHardwareAcceleration() { calls++; } }); + configureStartupRenderer({ disableHardwareAcceleration() { calls++; } }, { SESSIONNAME: 'Console' }, 'win32'); 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', () => { const startup = createStartupWindow(TestBrowserWindow, { width: 1100, show: true }); diff --git a/tests/ui-smoke.js b/tests/ui-smoke.js index 55537a9..f488497 100644 --- a/tests/ui-smoke.js +++ b/tests/ui-smoke.js @@ -1423,7 +1423,7 @@ setTimeout(async () => { check('Sidebar indicator stays aligned at the minimum window size', queueProgressVisibility.minimum.sidebarIndicatorAligned); 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'); 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("|")'); 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'); - check('Update dialog shows the GitHub changelog with real line breaks', updateDialogChangelog === 'false|Changelog|Added live language switching.\\nImproved settings layout.'); + 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 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.\\n• Improved 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("|"); })()'); 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, dismissDisabled: document.getElementById('dismissUpdateBtn').disabled, headerHidden: header.hidden, + messageHidden: document.getElementById('updateMessage').hidden, + messageText: document.getElementById('updateMessage').textContent, progressLabel: progress.getAttribute('aria-label'), 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('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'); check('Update errors restore all close actions', updateErrorRecovery === 'none|false|false|false');