feat: reveal available updates beside speed
Collapse the header update action completely when no update exists and animate the green action outward to the left of the fixed bandwidth widget only after a real update is detected.
This commit is contained in:
+8
-1
@@ -801,6 +801,7 @@ function _setHeaderUpdateLabel(text) {
|
|||||||
|
|
||||||
function _syncHeaderUpdateState() {
|
function _syncHeaderUpdateState() {
|
||||||
const button = document.getElementById('headerUpdateBtn');
|
const button = document.getElementById('headerUpdateBtn');
|
||||||
|
const slot = document.getElementById('headerUpdateSlot');
|
||||||
const available = !!(_knownUpdateInfo && _knownUpdateInfo.available);
|
const available = !!(_knownUpdateInfo && _knownUpdateInfo.available);
|
||||||
const version = available ? String(_knownUpdateInfo.remoteVersion || '').replace(/^v/i, '') : '';
|
const version = available ? String(_knownUpdateInfo.remoteVersion || '').replace(/^v/i, '') : '';
|
||||||
const hint = _updateCheckBusy
|
const hint = _updateCheckBusy
|
||||||
@@ -808,10 +809,15 @@ function _syncHeaderUpdateState() {
|
|||||||
: available
|
: available
|
||||||
? `Update v${version || 'unbekannt'} verfügbar. Klicken zum Installieren.`
|
? `Update v${version || 'unbekannt'} verfügbar. Klicken zum Installieren.`
|
||||||
: 'Nach Aktualisierungen suchen';
|
: 'Nach Aktualisierungen suchen';
|
||||||
|
if (slot) {
|
||||||
|
slot.classList.toggle('is-visible', available);
|
||||||
|
slot.setAttribute('aria-hidden', available ? 'false' : 'true');
|
||||||
|
}
|
||||||
if (button) {
|
if (button) {
|
||||||
button.classList.toggle('update-available', available);
|
button.classList.toggle('update-available', available);
|
||||||
button.classList.toggle('is-checking', _updateCheckBusy);
|
button.classList.toggle('is-checking', _updateCheckBusy);
|
||||||
button.disabled = _updateCheckBusy;
|
button.disabled = !available || _updateCheckBusy;
|
||||||
|
button.tabIndex = available ? 0 : -1;
|
||||||
button.setAttribute('aria-busy', _updateCheckBusy ? 'true' : 'false');
|
button.setAttribute('aria-busy', _updateCheckBusy ? 'true' : 'false');
|
||||||
button.setAttribute('aria-label', hint);
|
button.setAttribute('aria-label', hint);
|
||||||
button.title = hint;
|
button.title = hint;
|
||||||
@@ -843,6 +849,7 @@ async function requestUpdateCheck({ forceRefresh = true } = {}) {
|
|||||||
} else if (result && result.error) {
|
} else if (result && result.error) {
|
||||||
showCopyToast('Updateprüfung fehlgeschlagen');
|
showCopyToast('Updateprüfung fehlgeschlagen');
|
||||||
} else {
|
} else {
|
||||||
|
_knownUpdateInfo = null;
|
||||||
showCopyToast('Kein Update verfügbar');
|
showCopyToast('Kein Update verfügbar');
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
+6
-4
@@ -54,6 +54,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="header-spacer" aria-hidden="true"></div>
|
<div class="header-spacer" aria-hidden="true"></div>
|
||||||
<div class="header-cluster header-utilities">
|
<div class="header-cluster header-utilities">
|
||||||
|
<div class="header-update-slot" id="headerUpdateSlot" aria-hidden="true">
|
||||||
|
<button class="header-update-button" id="headerUpdateBtn" title="Nach Aktualisierungen suchen" aria-label="Nach Aktualisierungen suchen" data-tooltip="Nach Aktualisierungen suchen" tabindex="-1" disabled>
|
||||||
|
<svg class="header-action-icon" aria-hidden="true"><use href="#icon-download"></use></svg>
|
||||||
|
<span class="header-update-label">Update</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div class="upload-speed-sparkline" id="uploadSpeedSparkline" title="Aktuelle Upload-Geschwindigkeit" aria-label="Aktuelle Upload-Geschwindigkeit">
|
<div class="upload-speed-sparkline" id="uploadSpeedSparkline" title="Aktuelle Upload-Geschwindigkeit" aria-label="Aktuelle Upload-Geschwindigkeit">
|
||||||
<span class="upload-speed-chart" aria-hidden="true">
|
<span class="upload-speed-chart" aria-hidden="true">
|
||||||
<span class="upload-speed-baseline"></span>
|
<span class="upload-speed-baseline"></span>
|
||||||
@@ -61,10 +67,6 @@
|
|||||||
</span>
|
</span>
|
||||||
<strong id="uploadSpeedValue">0 B/s</strong>
|
<strong id="uploadSpeedValue">0 B/s</strong>
|
||||||
</div>
|
</div>
|
||||||
<button class="header-update-button" id="headerUpdateBtn" title="Nach Aktualisierungen suchen" aria-label="Nach Aktualisierungen suchen" data-tooltip="Nach Aktualisierungen suchen">
|
|
||||||
<svg class="header-action-icon" aria-hidden="true"><use href="#icon-download"></use></svg>
|
|
||||||
<span class="header-update-label">Update</span>
|
|
||||||
</button>
|
|
||||||
<span class="header-divider" aria-hidden="true"></span>
|
<span class="header-divider" aria-hidden="true"></span>
|
||||||
<nav class="menu-bar" id="menuBar" aria-label="Anwendungsmenüs">
|
<nav class="menu-bar" id="menuBar" aria-label="Anwendungsmenüs">
|
||||||
<div class="menu-bar-item" data-menu="datei">
|
<div class="menu-bar-item" data-menu="datei">
|
||||||
|
|||||||
+38
-1
@@ -2109,6 +2109,29 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; }
|
|||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-update-slot {
|
||||||
|
width: 0;
|
||||||
|
flex: 0 0 0;
|
||||||
|
height: 32px;
|
||||||
|
margin-right: -4px;
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: width .28s cubic-bezier(.22, 1, .36, 1), flex-basis .28s cubic-bezier(.22, 1, .36, 1), margin-right .28s cubic-bezier(.22, 1, .36, 1), opacity .18s ease, visibility 0s linear .28s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-update-slot.is-visible {
|
||||||
|
width: 146px;
|
||||||
|
flex-basis: 146px;
|
||||||
|
margin-right: 0;
|
||||||
|
overflow: visible;
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
pointer-events: auto;
|
||||||
|
transition-delay: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
.header-update-button {
|
.header-update-button {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 146px;
|
width: 146px;
|
||||||
@@ -2127,7 +2150,16 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; }
|
|||||||
font: inherit;
|
font: inherit;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
transition: background-color .14s, transform .14s;
|
opacity: 0;
|
||||||
|
transform: translateX(14px);
|
||||||
|
pointer-events: none;
|
||||||
|
transition: background-color .14s, transform .28s cubic-bezier(.22, 1, .36, 1), opacity .18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-update-slot.is-visible .header-update-button {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-update-button.update-available {
|
.header-update-button.update-available {
|
||||||
@@ -4040,6 +4072,11 @@ input[type="checkbox"] {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-update-slot.is-visible {
|
||||||
|
width: 34px;
|
||||||
|
flex-basis: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
.header-update-label,
|
.header-update-label,
|
||||||
.version-label {
|
.version-label {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@@ -154,11 +154,16 @@ test('header occupies its final geometry before asynchronous initialization', ()
|
|||||||
const mainSource = fs.readFileSync(path.join(projectRoot, 'main.js'), 'utf8');
|
const mainSource = fs.readFileSync(path.join(projectRoot, 'main.js'), 'utf8');
|
||||||
const css = fs.readFileSync(path.join(projectRoot, 'renderer', 'styles.css'), 'utf8');
|
const css = fs.readFileSync(path.join(projectRoot, 'renderer', 'styles.css'), 'utf8');
|
||||||
const updateButton = html.match(/<button class="header-update-button"[^>]*id="headerUpdateBtn"[^>]*>/u)?.[0] || '';
|
const updateButton = html.match(/<button class="header-update-button"[^>]*id="headerUpdateBtn"[^>]*>/u)?.[0] || '';
|
||||||
|
const updateSlotIndex = html.indexOf('class="header-update-slot"');
|
||||||
|
const speedWidgetIndex = html.indexOf('id="uploadSpeedSparkline"');
|
||||||
const firstFrameInitialization = appSource.lastIndexOf('\ninitializeStaticHeader();');
|
const firstFrameInitialization = appSource.lastIndexOf('\ninitializeStaticHeader();');
|
||||||
const asynchronousInitialization = appSource.lastIndexOf('\ninit().then(');
|
const asynchronousInitialization = appSource.lastIndexOf('\ninit().then(');
|
||||||
|
|
||||||
assert.doesNotMatch(updateButton, /\shidden(?:\s|>)/u);
|
assert.doesNotMatch(updateButton, /\shidden(?:\s|>)/u);
|
||||||
assert.match(css, /\.header-update-button\s*\{[^}]*width:\s*146px;[^}]*flex:\s*0 0 146px;/su);
|
assert.ok(updateSlotIndex >= 0 && updateSlotIndex < speedWidgetIndex);
|
||||||
|
assert.match(css, /\.header-update-slot\s*\{[^}]*width:\s*0;[^}]*flex:\s*0 0 0;[^}]*overflow:\s*hidden;/su);
|
||||||
|
assert.match(css, /\.header-update-slot\.is-visible\s*\{[^}]*width:\s*146px;[^}]*flex-basis:\s*146px;/su);
|
||||||
|
assert.match(css, /\.header-update-slot\.is-visible\s+\.header-update-button\s*\{[^}]*opacity:\s*1;[^}]*transform:\s*translateX\(0\);/su);
|
||||||
assert.match(css, /\.version-badge\s*\{[^}]*min-width:\s*48px;/su);
|
assert.match(css, /\.version-badge\s*\{[^}]*min-width:\s*48px;/su);
|
||||||
assert.notEqual(firstFrameInitialization, -1);
|
assert.notEqual(firstFrameInitialization, -1);
|
||||||
assert.ok(firstFrameInitialization < asynchronousInitialization);
|
assert.ok(firstFrameInitialization < asynchronousInitialization);
|
||||||
|
|||||||
+10
-10
@@ -185,8 +185,8 @@ setTimeout(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const startupUpdateState = await wc.executeJavaScript('(() => { const button = document.getElementById("headerUpdateBtn"); return [_knownUpdateInfo?.remoteVersion, button?.hidden, getComputedStyle(button).display, document.getElementById("updateBanner")?.style.display].join("|"); })()');
|
const startupUpdateState = await wc.executeJavaScript('(() => { const slot = document.getElementById("headerUpdateSlot"); return [_knownUpdateInfo?.remoteVersion, slot?.classList.contains("is-visible"), slot?.getAttribute("aria-hidden"), document.getElementById("updateBanner")?.style.display].join("|"); })()');
|
||||||
check('Startup update survives pending renderer initialization', startupUpdateState === '9.9.8|false|flex|flex');
|
check('Startup update survives pending renderer initialization', startupUpdateState === '9.9.8|true|false|flex');
|
||||||
await wc.executeJavaScript('_knownUpdateInfo = null; closeUpdateDialog(); _syncHeaderUpdateState();');
|
await wc.executeJavaScript('_knownUpdateInfo = null; closeUpdateDialog(); _syncHeaderUpdateState();');
|
||||||
|
|
||||||
const germanStartupReady = await waitUntil(() => wc.executeJavaScript('document.documentElement.lang + "|" + document.getElementById("languageInput")?.value + "|" + [...document.querySelectorAll(".tab")].map(tab => tab.textContent.trim()).join(",")'));
|
const germanStartupReady = await waitUntil(() => wc.executeJavaScript('document.documentElement.lang + "|" + document.getElementById("languageInput")?.value + "|" + [...document.querySelectorAll(".tab")].map(tab => tab.textContent.trim()).join(",")'));
|
||||||
@@ -290,8 +290,8 @@ setTimeout(async () => {
|
|||||||
const headerUpdateButtonExists = await wc.executeJavaScript('Boolean(document.getElementById("headerUpdateBtn"))');
|
const headerUpdateButtonExists = await wc.executeJavaScript('Boolean(document.getElementById("headerUpdateBtn"))');
|
||||||
check('App header exposes the update action', headerUpdateButtonExists);
|
check('App header exposes the update action', headerUpdateButtonExists);
|
||||||
|
|
||||||
const initialHeaderUpdateVisibility = await wc.executeJavaScript('(() => { const button = document.getElementById("headerUpdateBtn"); return [button?.hidden, getComputedStyle(button).display].join("|"); })()');
|
const initialHeaderUpdateVisibility = await wc.executeJavaScript('(() => { const slot = document.getElementById("headerUpdateSlot"); const button = document.getElementById("headerUpdateBtn"); return [slot?.classList.contains("is-visible"), slot?.getAttribute("aria-hidden"), Math.round(slot?.getBoundingClientRect().width || 0), button?.disabled, button?.tabIndex].join("|"); })()');
|
||||||
check('Header update action keeps its layout slot before an update is available', initialHeaderUpdateVisibility === 'false|flex');
|
check('Header update action is fully collapsed before an update is available', initialHeaderUpdateVisibility === 'false|true|0|true|-1');
|
||||||
|
|
||||||
const initialUpdateLabel = await wc.executeJavaScript('document.querySelector("#headerUpdateBtn .header-update-label")?.textContent?.trim()');
|
const initialUpdateLabel = await wc.executeJavaScript('document.querySelector("#headerUpdateBtn .header-update-label")?.textContent?.trim()');
|
||||||
check('App header uses the compact update label', initialUpdateLabel === 'Update');
|
check('App header uses the compact update label', initialUpdateLabel === 'Update');
|
||||||
@@ -1198,20 +1198,20 @@ setTimeout(async () => {
|
|||||||
document.getElementById('headerUpdateBtn').click();
|
document.getElementById('headerUpdateBtn').click();
|
||||||
})()\`);
|
})()\`);
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
const coordinatedUpdateBusy = await wc.executeJavaScript('(() => { const manual = document.getElementById("manualUpdateCheckBtn"); const header = document.getElementById("headerUpdateBtn"); return [manual?.disabled, manual?.getAttribute("aria-busy"), manual?.textContent?.trim(), header?.disabled, header?.getAttribute("aria-busy"), header?.hidden].join("|"); })()');
|
const coordinatedUpdateBusy = await wc.executeJavaScript('(() => { const manual = document.getElementById("manualUpdateCheckBtn"); const header = document.getElementById("headerUpdateBtn"); const slot = document.getElementById("headerUpdateSlot"); return [manual?.disabled, manual?.getAttribute("aria-busy"), manual?.textContent?.trim(), header?.disabled, header?.getAttribute("aria-busy"), slot?.classList.contains("is-visible")].join("|"); })()');
|
||||||
check('All update entry points share one in-flight check', updateCheckCallCount === 1 && coordinatedUpdateBusy === 'true|true|Prüfe…|true|true|false');
|
check('All update entry points share one in-flight check without revealing the header action', updateCheckCallCount === 1 && coordinatedUpdateBusy === 'true|true|Prüfe…|true|true|false');
|
||||||
updateCheckResolvers.splice(0).forEach(resolve => resolve({ available: false, error: 'Simulierter Netzwerkfehler' }));
|
updateCheckResolvers.splice(0).forEach(resolve => resolve({ available: false, error: 'Simulierter Netzwerkfehler' }));
|
||||||
await new Promise(resolve => setTimeout(resolve, 150));
|
await new Promise(resolve => setTimeout(resolve, 150));
|
||||||
const coordinatedUpdateError = await wc.executeJavaScript('(() => { const manual = document.getElementById("manualUpdateCheckBtn"); const header = document.getElementById("headerUpdateBtn"); return [manual?.disabled, header?.disabled, header?.hidden, manual?.textContent?.trim(), document.getElementById("copyToast")?.textContent?.trim()].join("|"); })()');
|
const coordinatedUpdateError = await wc.executeJavaScript('(() => { const manual = document.getElementById("manualUpdateCheckBtn"); const header = document.getElementById("headerUpdateBtn"); const slot = document.getElementById("headerUpdateSlot"); return [manual?.disabled, header?.disabled, slot?.classList.contains("is-visible"), manual?.textContent?.trim(), document.getElementById("copyToast")?.textContent?.trim()].join("|"); })()');
|
||||||
check('Settings update check uses the shared error contract', coordinatedUpdateError === 'false|false|false|Nach Updates suchen|Updateprüfung fehlgeschlagen');
|
check('Settings update check uses the shared error contract', coordinatedUpdateError === 'false|true|false|Nach Updates suchen|Updateprüfung fehlgeschlagen');
|
||||||
await wc.executeJavaScript('document.getElementById("copyToast")?.classList.remove("show")');
|
await wc.executeJavaScript('document.getElementById("copyToast")?.classList.remove("show")');
|
||||||
|
|
||||||
await wc.executeJavaScript('requestUpdateCheck(); true');
|
await wc.executeJavaScript('requestUpdateCheck(); true');
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
updateCheckResolvers.splice(0).forEach(resolve => resolve({ available: false }));
|
updateCheckResolvers.splice(0).forEach(resolve => resolve({ available: false }));
|
||||||
await new Promise(resolve => setTimeout(resolve, 150));
|
await new Promise(resolve => setTimeout(resolve, 150));
|
||||||
const noUpdateHeaderVisibility = await wc.executeJavaScript('(() => { const button = document.getElementById("headerUpdateBtn"); return [button?.hidden, getComputedStyle(button).display].join("|"); })()');
|
const noUpdateHeaderVisibility = await wc.executeJavaScript('(() => { const slot = document.getElementById("headerUpdateSlot"); const button = document.getElementById("headerUpdateBtn"); return [slot?.classList.contains("is-visible"), Math.round(slot?.getBoundingClientRect().width || 0), button?.disabled].join("|"); })()');
|
||||||
check('Successful no-update result keeps the header action in its stable slot', noUpdateHeaderVisibility === 'false|flex');
|
check('Successful no-update result keeps the header action collapsed', noUpdateHeaderVisibility === 'false|0|true');
|
||||||
|
|
||||||
const settingsNavigation = await wc.executeJavaScript('(() => { const buttons = [...document.querySelectorAll(".settings-nav-button")]; return [buttons.length, buttons.map(button => button.textContent.trim()).join("|"), document.querySelector(".settings-nav-button.active")?.dataset.settingsPage, document.getElementById("settingsSearchInput")?.placeholder].join("::"); })()');
|
const settingsNavigation = await wc.executeJavaScript('(() => { const buttons = [...document.querySelectorAll(".settings-nav-button")]; return [buttons.length, buttons.map(button => button.textContent.trim()).join("|"), document.querySelector(".settings-nav-button.active")?.dataset.settingsPage, document.getElementById("settingsSearchInput")?.placeholder].join("::"); })()');
|
||||||
check('Settings use the task-based sidebar navigation', settingsNavigation === '8::Allgemein|Uploads|Automatik|Benachrichtigungen|Logs & Support|Fernsteuerung|Diagnose-Zugriff|Backup & Übertragen::allgemein::Einstellungen durchsuchen');
|
check('Settings use the task-based sidebar navigation', settingsNavigation === '8::Allgemein|Uploads|Automatik|Benachrichtigungen|Logs & Support|Fernsteuerung|Diagnose-Zugriff|Backup & Übertragen::allgemein::Einstellungen durchsuchen');
|
||||||
|
|||||||
Reference in New Issue
Block a user