feat(accounts): "Alle ausklappen" / "Alle einklappen" toggle for hoster groups

Adds a bottom-right button under the accounts list that expands every hoster
group at once (and collapses them again — the label flips based on current
state). Reuses the existing _hosterGroupOpenMemory so the per-group open/closed
state stays consistent with manual header clicks. The footer is hidden when no
accounts exist. Pure DOM toggle, no re-render.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Administrator 2026-06-15 00:28:13 +02:00
parent 05ad08433c
commit 33364168f1
3 changed files with 40 additions and 0 deletions

View File

@ -3485,12 +3485,15 @@ function renderAccounts() {
const runCheckBtn = document.getElementById('accountsRunHealthCheckBtn'); const runCheckBtn = document.getElementById('accountsRunHealthCheckBtn');
if (runCheckBtn) runCheckBtn.disabled = healthCheckRunning; if (runCheckBtn) runCheckBtn.disabled = healthCheckRunning;
const footer = document.getElementById('accountsListFooter');
if (allAccounts.length === 0) { if (allAccounts.length === 0) {
container.innerHTML = ` container.innerHTML = `
<div class="accounts-empty"> <div class="accounts-empty">
<p>Keine Accounts vorhanden</p> <p>Keine Accounts vorhanden</p>
<span class="hint">Klicke auf "Account hinzufügen", um einen Hoster einzurichten.</span> <span class="hint">Klicke auf "Account hinzufügen", um einen Hoster einzurichten.</span>
</div>`; </div>`;
if (footer) footer.style.display = 'none';
if (!_accountListenersBound) bindAccountListeners(container); if (!_accountListenersBound) bindAccountListeners(container);
return; return;
} }
@ -3509,6 +3512,9 @@ function renderAccounts() {
} }
container.innerHTML = html; container.innerHTML = html;
if (footer) footer.style.display = '';
_updateToggleAllAccountsBtn();
if (!_accountListenersBound) bindAccountListeners(container); if (!_accountListenersBound) bindAccountListeners(container);
} }
@ -3578,6 +3584,35 @@ function _hosterLifetimeStat(name) {
} }
function _invalidateHosterLifetimeCache() { _hosterLifetimeCache = null; } function _invalidateHosterLifetimeCache() { _hosterLifetimeCache = null; }
function _allAccountGroupsOpen() {
const bodies = document.querySelectorAll('#accountsList .account-hoster-group-body');
if (!bodies.length) return false;
for (const b of bodies) if (b.style.display === 'none') return false;
return true;
}
function _setAllAccountGroupsOpen(open) {
const groups = document.querySelectorAll('#accountsList .account-hoster-group');
groups.forEach(group => {
const name = group.dataset.hosterGroup;
const body = group.querySelector('.account-hoster-group-body');
const arrow = group.querySelector('.panel-arrow');
if (body) body.style.display = open ? '' : 'none';
if (arrow) arrow.innerHTML = open ? '&#9660;' : '&#9654;';
if (name) {
const summary = _summarizeHosterGroup(config.hosters[name] || []);
_hosterGroupOpenMemory.set(name, { state: open ? 'open' : 'closed', errorsAtClose: summary.error });
}
});
_updateToggleAllAccountsBtn();
}
function _updateToggleAllAccountsBtn() {
const btn = document.getElementById('toggleAllAccountsBtn');
if (!btn) return;
btn.textContent = _allAccountGroupsOpen() ? 'Alle einklappen' : 'Alle ausklappen';
}
// Single set of delegated listeners on the accounts container. Bound once on // Single set of delegated listeners on the accounts container. Bound once on
// the first render and reused for every subsequent in-place update / card // the first render and reused for every subsequent in-place update / card
// swap. Previously we rebound 4 × N button listeners + 5 × N drag listeners // swap. Previously we rebound 4 × N button listeners + 5 × N drag listeners
@ -4484,6 +4519,7 @@ function setupListeners() {
document.getElementById('moveDownBtn').addEventListener('click', () => moveSelectedJobs('down')); document.getElementById('moveDownBtn').addEventListener('click', () => moveSelectedJobs('down'));
document.getElementById('moveBottomBtn').addEventListener('click', () => moveSelectedJobs('bottom')); document.getElementById('moveBottomBtn').addEventListener('click', () => moveSelectedJobs('bottom'));
document.getElementById('accountsRunHealthCheckBtn').addEventListener('click', () => runHealthCheck('manual')); document.getElementById('accountsRunHealthCheckBtn').addEventListener('click', () => runHealthCheck('manual'));
document.getElementById('toggleAllAccountsBtn').addEventListener('click', () => _setAllAccountGroupsOpen(!_allAccountGroupsOpen()));
document.getElementById('copyAllLinksBtn').addEventListener('click', copyAllLinks); document.getElementById('copyAllLinksBtn').addEventListener('click', copyAllLinks);
document.getElementById('clearRecentFilesBtn').addEventListener('click', clearAllRecentFiles); document.getElementById('clearRecentFilesBtn').addEventListener('click', clearAllRecentFiles);
document.getElementById('exportRecentFilesBtn').addEventListener('click', exportAllRecentFiles); document.getElementById('exportRecentFilesBtn').addEventListener('click', exportAllRecentFiles);

View File

@ -178,6 +178,9 @@
</div> </div>
<div class="health-check-results account-health-results" id="healthCheckResults"></div> <div class="health-check-results account-health-results" id="healthCheckResults"></div>
<div class="accounts-list" id="accountsList"></div> <div class="accounts-list" id="accountsList"></div>
<div class="accounts-list-footer" id="accountsListFooter" style="display:none">
<button class="btn btn-secondary" id="toggleAllAccountsBtn">Alle ausklappen</button>
</div>
</div> </div>
</div> </div>

View File

@ -793,6 +793,7 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; }
padding: 0 0 12px; padding: 0 0 12px;
} }
.accounts-list { display: grid; gap: 8px; } .accounts-list { display: grid; gap: 8px; }
.accounts-list-footer { display: flex; justify-content: flex-end; margin-top: 12px; }
.account-card { .account-card {
display: flex; display: flex;