feat: animate account sections and align header actions
This commit is contained in:
+25
-20
@@ -4936,12 +4936,13 @@ function _buildHosterSettingsHtml(name) {
|
|||||||
const hs = (config.hosterSettings && config.hosterSettings[name]) || {};
|
const hs = (config.hosterSettings && config.hosterSettings[name]) || {};
|
||||||
const maxSpeedMbs = hs.maxSpeedKbs > 0 ? String(+(hs.maxSpeedKbs / 1024).toFixed(2)) : '0';
|
const maxSpeedMbs = hs.maxSpeedKbs > 0 ? String(+(hs.maxSpeedKbs / 1024).toFixed(2)) : '0';
|
||||||
return `<div class="account-hoster-settings">
|
return `<div class="account-hoster-settings">
|
||||||
<div class="account-hoster-settings-header" data-hoster-settings-toggle="${name}">
|
<div class="account-hoster-settings-header" data-hoster-settings-toggle="${name}" aria-expanded="false">
|
||||||
<span class="panel-arrow">▶</span>
|
<span class="panel-arrow">▶</span>
|
||||||
<span>Upload-Einstellungen</span>
|
<span>Upload-Einstellungen</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="account-hoster-settings-body" style="display:none">
|
<div class="account-hoster-settings-body account-collapse" aria-hidden="true" inert>
|
||||||
<div class="settings-grid-mini">
|
<div class="account-collapse-content">
|
||||||
|
<div class="account-hoster-settings-body-inner settings-grid-mini">
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<label>Retries</label>
|
<label>Retries</label>
|
||||||
<input type="number" class="hs-input" data-hoster="${name}" data-hs="retries" value="${hs.retries ?? 3}" min="0" max="500">
|
<input type="number" class="hs-input" data-hoster="${name}" data-hs="retries" value="${hs.retries ?? 3}" min="0" max="500">
|
||||||
@@ -4986,6 +4987,7 @@ function _buildHosterSettingsHtml(name) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4994,17 +4996,15 @@ function _buildAccountHosterGroupHtml(name, accounts) {
|
|||||||
const isOpen = _hosterGroupOpenState(name, summary);
|
const isOpen = _hosterGroupOpenState(name, summary);
|
||||||
const dot = window.AccountStatus.getAccountGroupStatus(summary);
|
const dot = window.AccountStatus.getAccountGroupStatus(summary);
|
||||||
const countLabel = `${summary.ok}/${summary.total}`;
|
const countLabel = `${summary.ok}/${summary.total}`;
|
||||||
const arrow = isOpen ? '▼' : '▶';
|
|
||||||
let cardsHtml = '';
|
let cardsHtml = '';
|
||||||
accounts.forEach((account, idx) => { cardsHtml += _buildAccountCardHtml(name, account, idx); });
|
accounts.forEach((account, idx) => { cardsHtml += _buildAccountCardHtml(name, account, idx); });
|
||||||
const bodyStyle = isOpen ? '' : 'style="display:none"';
|
|
||||||
const lifeStat = _hosterLifetimeStat(name);
|
const lifeStat = _hosterLifetimeStat(name);
|
||||||
const lifeMeta = lifeStat && lifeStat.total > 0
|
const lifeMeta = lifeStat && lifeStat.total > 0
|
||||||
? `<span class="account-hoster-group-meta" title="Erfolgsrate aus den letzten ${lifeStat.total} Uploads dieses Hosters">${Math.round(lifeStat.rate * 100)}% ok (${lifeStat.total})</span>`
|
? `<span class="account-hoster-group-meta" title="Erfolgsrate aus den letzten ${lifeStat.total} Uploads dieses Hosters">${Math.round(lifeStat.rate * 100)}% ok (${lifeStat.total})</span>`
|
||||||
: '';
|
: '';
|
||||||
return `<div class="account-hoster-group" data-hoster-group="${name}">
|
return `<div class="account-hoster-group" data-hoster-group="${name}">
|
||||||
<div class="account-hoster-group-header" data-hoster-toggle="${name}">
|
<div class="account-hoster-group-header" data-hoster-toggle="${name}" aria-expanded="${isOpen}">
|
||||||
<span class="panel-arrow">${arrow}</span>
|
<span class="panel-arrow">▶</span>
|
||||||
<span class="account-status-dot status-${dot}"></span>
|
<span class="account-status-dot status-${dot}"></span>
|
||||||
<span class="account-hoster-group-title">${escapeHtml(getHosterLabel(name))}</span>
|
<span class="account-hoster-group-title">${escapeHtml(getHosterLabel(name))}</span>
|
||||||
<span class="account-hoster-group-count">${countLabel}</span>
|
<span class="account-hoster-group-count">${countLabel}</span>
|
||||||
@@ -5012,7 +5012,9 @@ function _buildAccountHosterGroupHtml(name, accounts) {
|
|||||||
${summary.error ? `<span class="account-hoster-group-meta error">${summary.error} Fehler</span>` : ''}
|
${summary.error ? `<span class="account-hoster-group-meta error">${summary.error} Fehler</span>` : ''}
|
||||||
${lifeMeta}
|
${lifeMeta}
|
||||||
</div>
|
</div>
|
||||||
<div class="account-hoster-group-body" ${bodyStyle}>${cardsHtml}</div>
|
<div class="account-hoster-group-body account-collapse${isOpen ? ' is-open' : ''}" aria-hidden="${!isOpen}" ${isOpen ? '' : 'inert'}>
|
||||||
|
<div class="account-collapse-content"><div class="account-hoster-group-body-inner">${cardsHtml}</div></div>
|
||||||
|
</div>
|
||||||
${_buildHosterSettingsHtml(name)}
|
${_buildHosterSettingsHtml(name)}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
@@ -5029,18 +5031,25 @@ function _invalidateHosterLifetimeCache() { _hosterLifetimeCache = null; }
|
|||||||
function _allAccountGroupsOpen() {
|
function _allAccountGroupsOpen() {
|
||||||
const bodies = document.querySelectorAll('#accountsList .account-hoster-group-body');
|
const bodies = document.querySelectorAll('#accountsList .account-hoster-group-body');
|
||||||
if (!bodies.length) return false;
|
if (!bodies.length) return false;
|
||||||
for (const b of bodies) if (b.style.display === 'none') return false;
|
for (const body of bodies) if (!body.classList.contains('is-open')) return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _setAccountCollapseOpen(trigger, body, open) {
|
||||||
|
if (!body) return;
|
||||||
|
body.classList.toggle('is-open', open);
|
||||||
|
body.setAttribute('aria-hidden', String(!open));
|
||||||
|
body.toggleAttribute('inert', !open);
|
||||||
|
if (trigger) trigger.setAttribute('aria-expanded', String(open));
|
||||||
|
}
|
||||||
|
|
||||||
function _setAllAccountGroupsOpen(open) {
|
function _setAllAccountGroupsOpen(open) {
|
||||||
const groups = document.querySelectorAll('#accountsList .account-hoster-group');
|
const groups = document.querySelectorAll('#accountsList .account-hoster-group');
|
||||||
groups.forEach(group => {
|
groups.forEach(group => {
|
||||||
const name = group.dataset.hosterGroup;
|
const name = group.dataset.hosterGroup;
|
||||||
const body = group.querySelector('.account-hoster-group-body');
|
const body = group.querySelector('.account-hoster-group-body');
|
||||||
const arrow = group.querySelector('.panel-arrow');
|
const header = group.querySelector('[data-hoster-toggle]');
|
||||||
if (body) body.style.display = open ? '' : 'none';
|
_setAccountCollapseOpen(header, body, open);
|
||||||
if (arrow) arrow.innerHTML = open ? '▼' : '▶';
|
|
||||||
if (name) {
|
if (name) {
|
||||||
const summary = _summarizeHosterGroup(config.hosters[name] || []);
|
const summary = _summarizeHosterGroup(config.hosters[name] || []);
|
||||||
_hosterGroupOpenMemory.set(name, { state: open ? 'open' : 'closed', errorsAtClose: summary.error });
|
_hosterGroupOpenMemory.set(name, { state: open ? 'open' : 'closed', errorsAtClose: summary.error });
|
||||||
@@ -5068,11 +5077,9 @@ function bindAccountListeners(container) {
|
|||||||
const name = header.dataset.hosterToggle;
|
const name = header.dataset.hosterToggle;
|
||||||
const group = header.closest('.account-hoster-group');
|
const group = header.closest('.account-hoster-group');
|
||||||
const body = group && group.querySelector('.account-hoster-group-body');
|
const body = group && group.querySelector('.account-hoster-group-body');
|
||||||
const arrow = header.querySelector('.panel-arrow');
|
|
||||||
if (body) {
|
if (body) {
|
||||||
const willOpen = body.style.display === 'none';
|
const willOpen = !body.classList.contains('is-open');
|
||||||
body.style.display = willOpen ? '' : 'none';
|
_setAccountCollapseOpen(header, body, willOpen);
|
||||||
if (arrow) arrow.innerHTML = willOpen ? '▼' : '▶';
|
|
||||||
const summary = _summarizeHosterGroup(config.hosters[name] || []);
|
const summary = _summarizeHosterGroup(config.hosters[name] || []);
|
||||||
_hosterGroupOpenMemory.set(name, { state: willOpen ? 'open' : 'closed', errorsAtClose: summary.error });
|
_hosterGroupOpenMemory.set(name, { state: willOpen ? 'open' : 'closed', errorsAtClose: summary.error });
|
||||||
}
|
}
|
||||||
@@ -5081,11 +5088,9 @@ function bindAccountListeners(container) {
|
|||||||
const settingsHeader = e.target.closest('[data-hoster-settings-toggle]');
|
const settingsHeader = e.target.closest('[data-hoster-settings-toggle]');
|
||||||
if (settingsHeader) {
|
if (settingsHeader) {
|
||||||
const body = settingsHeader.nextElementSibling;
|
const body = settingsHeader.nextElementSibling;
|
||||||
const arrow = settingsHeader.querySelector('.panel-arrow');
|
|
||||||
if (body) {
|
if (body) {
|
||||||
const isOpen = body.style.display !== 'none';
|
const willOpen = !body.classList.contains('is-open');
|
||||||
body.style.display = isOpen ? 'none' : '';
|
_setAccountCollapseOpen(settingsHeader, body, willOpen);
|
||||||
if (arrow) arrow.innerHTML = isOpen ? '▶' : '▼';
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
+51
-3
@@ -1275,7 +1275,7 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; }
|
|||||||
}
|
}
|
||||||
.account-hoster-settings-header:hover { background: var(--bg-card-hover); color: var(--text); }
|
.account-hoster-settings-header:hover { background: var(--bg-card-hover); color: var(--text); }
|
||||||
.account-hoster-settings-header .panel-arrow { font-size: 10px; color: var(--text-dim); }
|
.account-hoster-settings-header .panel-arrow { font-size: 10px; color: var(--text-dim); }
|
||||||
.account-hoster-settings-body { padding: 4px 12px 10px; }
|
.account-hoster-settings-body-inner { padding: 4px 12px 10px; }
|
||||||
|
|
||||||
.settings-hoster-pointer {
|
.settings-hoster-pointer {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
@@ -1374,7 +1374,7 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; }
|
|||||||
.batch-cat-tag.retryable { background: rgba(76, 175, 80, 0.18); color: #a5d6a7; }
|
.batch-cat-tag.retryable { background: rgba(76, 175, 80, 0.18); color: #a5d6a7; }
|
||||||
.batch-cat-list { margin: 0; padding-left: 18px; font-size: 11px; color: var(--text-muted); }
|
.batch-cat-list { margin: 0; padding-left: 18px; font-size: 11px; color: var(--text-muted); }
|
||||||
.batch-cat-list em { color: var(--text-muted); font-style: italic; }
|
.batch-cat-list em { color: var(--text-muted); font-style: italic; }
|
||||||
.account-hoster-group-body {
|
.account-hoster-group-body-inner {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
@@ -1392,6 +1392,38 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; }
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.account-collapse {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 0fr;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition: grid-template-rows 220ms cubic-bezier(.2, .8, .2, 1), opacity 160ms ease, visibility 0s linear 220ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-collapse.is-open {
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
transition-delay: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-collapse-content {
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-hoster-group-header .panel-arrow,
|
||||||
|
.account-hoster-settings-header .panel-arrow {
|
||||||
|
display: inline-block;
|
||||||
|
transform-origin: center;
|
||||||
|
transition: transform 180ms cubic-bezier(.2, .8, .2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-hoster-group-header[aria-expanded="true"] .panel-arrow,
|
||||||
|
.account-hoster-settings-header[aria-expanded="true"] .panel-arrow {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
.accounts-empty {
|
.accounts-empty {
|
||||||
width: min(520px, 100%);
|
width: min(520px, 100%);
|
||||||
margin: clamp(56px, 12vh, 120px) auto 0;
|
margin: clamp(56px, 12vh, 120px) auto 0;
|
||||||
@@ -1716,6 +1748,15 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; }
|
|||||||
.menu-closing {
|
.menu-closing {
|
||||||
animation-duration: 160ms !important;
|
animation-duration: 160ms !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.account-collapse {
|
||||||
|
transition-duration: 220ms, 160ms, 0s !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-hoster-group-header .panel-arrow,
|
||||||
|
.account-hoster-settings-header .panel-arrow {
|
||||||
|
transition-duration: 180ms !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.online-backup-panel {
|
.online-backup-panel {
|
||||||
@@ -3186,10 +3227,17 @@ input[type="checkbox"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.accounts-auto-check {
|
.accounts-auto-check {
|
||||||
min-height: 38px;
|
min-height: 34px;
|
||||||
|
height: 34px;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: var(--bg-raised);
|
background: var(--bg-raised);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accounts-header-actions > .btn {
|
||||||
|
min-height: 34px;
|
||||||
|
height: 34px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-health-results {
|
.account-health-results {
|
||||||
|
|||||||
+39
-3
@@ -457,6 +457,9 @@ setTimeout(async () => {
|
|||||||
const accountsFrameFit = await wc.executeJavaScript('(() => { const view = document.getElementById("accounts-view")?.getBoundingClientRect(); const status = document.getElementById("statusbar")?.getBoundingClientRect(); return Boolean(view && status && status.height > 0 && view.bottom <= status.top + 1 && status.bottom <= window.innerHeight + 1); })()');
|
const accountsFrameFit = await wc.executeJavaScript('(() => { const view = document.getElementById("accounts-view")?.getBoundingClientRect(); const status = document.getElementById("statusbar")?.getBoundingClientRect(); return Boolean(view && status && status.height > 0 && view.bottom <= status.top + 1 && status.bottom <= window.innerHeight + 1); })()');
|
||||||
check('Accounts view and statusbar fit inside the viewport', accountsFrameFit === true);
|
check('Accounts view and statusbar fit inside the viewport', accountsFrameFit === true);
|
||||||
|
|
||||||
|
const accountHeaderControlHeights = await wc.executeJavaScript('(() => [document.getElementById("accountsRunHealthCheckBtn"), document.querySelector(".accounts-auto-check"), document.getElementById("addAccountBtn")].map(element => element?.getBoundingClientRect().height || 0))()');
|
||||||
|
check('Accounts header actions share one rendered height', accountHeaderControlHeights.every(height => height > 0 && Math.abs(height - accountHeaderControlHeights[0]) <= 0.5));
|
||||||
|
|
||||||
await captureVisual('02-accounts.png');
|
await captureVisual('02-accounts.png');
|
||||||
|
|
||||||
const accountListValid = await wc.executeJavaScript('Boolean(document.querySelector("#accountsList .accounts-empty") || document.querySelectorAll("#accountsList .account-hoster-group").length)');
|
const accountListValid = await wc.executeJavaScript('Boolean(document.querySelector("#accountsList .accounts-empty") || document.querySelectorAll("#accountsList .account-hoster-group").length)');
|
||||||
@@ -536,6 +539,39 @@ setTimeout(async () => {
|
|||||||
check('Empty account state remains contained at 1280x720', emptyAccountsGeometry.emptyVisible && emptyAccountsGeometry.contained);
|
check('Empty account state remains contained at 1280x720', emptyAccountsGeometry.emptyVisible && emptyAccountsGeometry.contained);
|
||||||
await captureVisual('02-accounts-empty-1280x720.png');
|
await captureVisual('02-accounts-empty-1280x720.png');
|
||||||
|
|
||||||
|
const accountCollapseMotion = await wc.executeJavaScript(\`(async () => {
|
||||||
|
const hoster = HOSTERS[0];
|
||||||
|
HOSTERS.forEach(name => { config.hosters[name] = []; });
|
||||||
|
config.hosters[hoster] = [{ id: 'ui-collapse-account', label: 'Animated account', enabled: true, authType: 'login', username: 'animated@example.invalid', password: 'fictional-password' }];
|
||||||
|
accountStatuses = { 'ui-collapse-account': { status: 'ok', message: 'Bereit' } };
|
||||||
|
_hosterGroupOpenMemory.set(hoster, { state: 'closed', errorsAtClose: 0 });
|
||||||
|
renderAccounts();
|
||||||
|
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
const sample = async (trigger, body) => {
|
||||||
|
const closedStart = body.getBoundingClientRect().height;
|
||||||
|
trigger.click();
|
||||||
|
await wait(70);
|
||||||
|
const opening = body.getBoundingClientRect().height;
|
||||||
|
const openingState = body.classList.contains('is-open');
|
||||||
|
const duration = parseFloat(getComputedStyle(body).transitionDuration) * 1000;
|
||||||
|
await wait(240);
|
||||||
|
const opened = body.getBoundingClientRect().height;
|
||||||
|
trigger.click();
|
||||||
|
await wait(70);
|
||||||
|
const closing = body.getBoundingClientRect().height;
|
||||||
|
const closingState = !body.classList.contains('is-open');
|
||||||
|
await wait(240);
|
||||||
|
const closedEnd = body.getBoundingClientRect().height;
|
||||||
|
return { closedStart, opening, opened, closing, closedEnd, openingState, closingState, duration };
|
||||||
|
};
|
||||||
|
const hosterMotion = await sample(document.querySelector('[data-hoster-toggle]'), document.querySelector('.account-hoster-group-body'));
|
||||||
|
const settingsMotion = await sample(document.querySelector('[data-hoster-settings-toggle]'), document.querySelector('.account-hoster-settings-body'));
|
||||||
|
return { hosterMotion, settingsMotion };
|
||||||
|
})()\`);
|
||||||
|
const hasSmoothAccountCollapse = motion => motion.closedStart <= 1 && motion.opening > 1 && motion.opening < motion.opened - 1 && motion.closing > 1 && motion.closing < motion.opened - 1 && motion.closedEnd <= 1 && motion.openingState && motion.closingState && motion.duration >= 180;
|
||||||
|
check('Hoster groups visibly animate while opening and closing', hasSmoothAccountCollapse(accountCollapseMotion.hosterMotion));
|
||||||
|
check('Hoster upload settings visibly animate while opening and closing', hasSmoothAccountCollapse(accountCollapseMotion.settingsMotion));
|
||||||
|
|
||||||
const tallAccountGroupGeometry = await wc.executeJavaScript(\`(() => {
|
const tallAccountGroupGeometry = await wc.executeJavaScript(\`(() => {
|
||||||
const hoster = HOSTERS[0];
|
const hoster = HOSTERS[0];
|
||||||
HOSTERS.forEach(name => { config.hosters[name] = []; });
|
HOSTERS.forEach(name => { config.hosters[name] = []; });
|
||||||
@@ -582,7 +618,7 @@ setTimeout(async () => {
|
|||||||
});
|
});
|
||||||
renderAccounts();
|
renderAccounts();
|
||||||
document.querySelectorAll('[data-hoster-toggle]').forEach(header => {
|
document.querySelectorAll('[data-hoster-toggle]').forEach(header => {
|
||||||
if (header.nextElementSibling?.style.display === 'none') header.click();
|
if (!header.nextElementSibling?.classList.contains('is-open')) header.click();
|
||||||
});
|
});
|
||||||
[...document.querySelectorAll('[data-hoster-settings-toggle]')].slice(0, 3).forEach(header => header.click());
|
[...document.querySelectorAll('[data-hoster-settings-toggle]')].slice(0, 3).forEach(header => header.click());
|
||||||
const list = document.getElementById('accountsList');
|
const list = document.getElementById('accountsList');
|
||||||
@@ -590,8 +626,8 @@ setTimeout(async () => {
|
|||||||
if (list) list.scrollTop = list.scrollHeight;
|
if (list) list.scrollTop = list.scrollHeight;
|
||||||
return {
|
return {
|
||||||
groupCount: groups.length,
|
groupCount: groups.length,
|
||||||
openGroupCount: groups.filter(group => group.querySelector('.account-hoster-group-body')?.style.display !== 'none').length,
|
openGroupCount: groups.filter(group => group.querySelector('.account-hoster-group-body')?.classList.contains('is-open')).length,
|
||||||
openSettingsCount: groups.filter(group => group.querySelector('.account-hoster-settings-body')?.style.display !== 'none').length,
|
openSettingsCount: groups.filter(group => group.querySelector('.account-hoster-settings-body')?.classList.contains('is-open')).length,
|
||||||
listClientHeight: list?.clientHeight || 0,
|
listClientHeight: list?.clientHeight || 0,
|
||||||
listScrollHeight: list?.scrollHeight || 0,
|
listScrollHeight: list?.scrollHeight || 0,
|
||||||
listScrollTop: list?.scrollTop || 0,
|
listScrollTop: list?.scrollTop || 0,
|
||||||
|
|||||||
Reference in New Issue
Block a user