diff --git a/renderer/app.js b/renderer/app.js index b40c078..0af1ea0 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -7445,26 +7445,40 @@ function setupListeners() { } // --- Update UI --- -function _formatUpdateReleaseNotes(value) { - const output = []; - let pendingBlank = false; +function _cleanUpdateReleaseText(value) { + return String(value || '') + .replace(/\*\*([^*]+)\*\*/g, '$1') + .replace(/`([^`]+)`/g, '$1') + .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1'); +} + +function _renderUpdateReleaseNotes(container, value) { + const fragment = document.createDocumentFragment(); + let consumed = 0; 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; + const line = rawLine.trim(); + if (!line) continue; + const heading = line.match(/^(#{1,6})\s+(.+)$/); + const bullet = line.match(/^[-*+]\s+(.+)$/); + const className = heading + ? (heading[1].length <= 2 ? 'update-release-heading' : 'update-release-category') + : bullet + ? 'update-release-item' + : 'update-release-line'; + const source = heading ? heading[2] : bullet ? `• ${bullet[1]}` : line; + const cleaned = _cleanUpdateReleaseText(source); + const available = 2400 - consumed; + if (available <= 0) break; + const text = cleaned.length > available ? `${cleaned.slice(0, Math.max(0, available - 1))}…` : cleaned; + const element = document.createElement('div'); + element.className = className; + element.textContent = text; + fragment.appendChild(element); + consumed += text.length; + if (text.endsWith('…')) break; } - return output.join('\n'); + container.replaceChildren(fragment); + return container.childElementCount > 0; } function showUpdateBanner(info) { @@ -7488,9 +7502,7 @@ function showUpdateBanner(info) { message.hidden = false; } if (notes && notesBody) { - const releaseNotes = _formatUpdateReleaseNotes(info.releaseNotes); - notesBody.textContent = releaseNotes.length > 2400 ? `${releaseNotes.slice(0, 2399)}…` : releaseNotes; - notes.hidden = !releaseNotes; + notes.hidden = !_renderUpdateReleaseNotes(notesBody, info.releaseNotes); } if (installButton) { installButton.disabled = false; diff --git a/renderer/styles.css b/renderer/styles.css index 1c82022..6ef63f6 100644 --- a/renderer/styles.css +++ b/renderer/styles.css @@ -2491,7 +2491,37 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; } } .update-release-notes-body { - white-space: pre-wrap; + white-space: normal; +} + +.update-release-heading { + margin: 2px 0 11px; + color: var(--text); + font-size: 14px; + font-weight: 750; + line-height: 1.3; +} + +.update-release-category { + margin: 15px 0 7px; + color: var(--text); + font-size: 13px; + font-weight: 700; + line-height: 1.35; +} + +.update-release-heading + .update-release-category { + margin-top: 0; +} + +.update-release-item, +.update-release-line { + margin: 4px 0; +} + +.update-release-item { + padding-left: 11px; + text-indent: -9px; } .update-progress-track { diff --git a/tests/startup-renderer.test.js b/tests/startup-renderer.test.js index 1ca593b..d76006f 100644 --- a/tests/startup-renderer.test.js +++ b/tests/startup-renderer.test.js @@ -179,5 +179,7 @@ test('header occupies its final geometry before asynchronous initialization', () assert.match(html, /class="update-progress-footer"[\s\S]*id="updateProgressDetails"[\s\S]*id="updateProgressSize"[\s\S]*id="updateProgressSpeed"[\s\S]*id="updateProgressEta"[\s\S]*id="updateProgressText"/u); assert.match(css, /#updateProgressDetails\s*\{[^}]*display:\s*grid;[^}]*grid-template-columns:\s*19ch 2ch 11ch 2ch 10ch;[^}]*column-gap:\s*0;[^}]*font-variant-numeric:\s*tabular-nums;/su); assert.match(css, /\.update-progress-separator\s*\{[^}]*display:\s*grid;[^}]*place-items:\s*center;/su); + assert.match(css, /\.update-release-heading\s*\{[^}]*font-size:\s*14px;[^}]*font-weight:\s*750;/su); + assert.match(css, /\.update-release-category\s*\{[^}]*font-size:\s*13px;[^}]*font-weight:\s*700;/su); assert.match(css, /#updateProgressDetails\[hidden\]\s*\{[^}]*display:\s*none;/su); }); diff --git a/tests/ui-smoke.js b/tests/ui-smoke.js index 8867b9d..0aaf4e7 100644 --- a/tests/ui-smoke.js +++ b/tests/ui-smoke.js @@ -2447,8 +2447,8 @@ setTimeout(async () => { check('Update dialog offers cancel and install actions', updateDialogActions === 'Abbrechen|Jetzt installieren'); const updateCloseColor = await wc.executeJavaScript('getComputedStyle(document.getElementById("updateCloseBtn")).color'); check('Update dialog exposes a red close action', updateCloseColor === 'rgb(255, 133, 140)'); - 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 updateDialogChangelog = await wc.executeJavaScript('(() => { const title = document.querySelector(".update-release-notes-title"); const body = document.getElementById("updateReleaseNotesBody"); const heading = body?.querySelector(".update-release-heading"); const category = body?.querySelector(".update-release-category"); const items = [...(body?.querySelectorAll(".update-release-item") || [])]; const titleRect = title?.getBoundingClientRect(); const bodyRect = body?.getBoundingClientRect(); return { hidden: document.getElementById("updateReleaseNotes")?.hidden, title: title?.textContent?.trim(), heading: heading?.textContent, headingSize: getComputedStyle(heading).fontSize, category: category?.textContent, categorySize: getComputedStyle(category).fontSize, items: items.map(item => item.textContent).join("|"), gap: bodyRect && titleRect ? bodyRect.top - titleRect.bottom : null }; })()'); + check('Update dialog renders a compact changelog with larger headings and categories', updateDialogChangelog.hidden === false && updateDialogChangelog.title === 'Changelog' && updateDialogChangelog.heading === 'New in this version' && updateDialogChangelog.headingSize === '14px' && updateDialogChangelog.category === 'Menus and navigation' && updateDialogChangelog.categorySize === '13px' && updateDialogChangelog.items === '• Added live language switching.|• Improved settings layout.' && updateDialogChangelog.gap <= 10); const updateDialogSize = await wc.executeJavaScript('(() => ({ width: document.querySelector(".update-dialog")?.getBoundingClientRect().width, notesHeight: document.getElementById("updateReleaseNotes")?.getBoundingClientRect().height }))()'); check('Update dialog and changelog provide more horizontal and vertical room', updateDialogSize.width === 576 && updateDialogSize.notesHeight === 264);