fix: stabilize renderer state and lifecycle updates
Prevent stale history, account, diagnostics, and language updates from repainting newer state. Keep queue virtualization, panel sizing, telemetry, cancellation, and runtime timing consistent across rapid UI changes. Strengthen painted-frame and lifecycle regression coverage, and brand packaged Windows metadata with the product publisher.
This commit is contained in:
+163
-30
@@ -1,6 +1,6 @@
|
||||
const HOSTERS = ['doodstream.com', 'voe.sx', 'vidmoly.me', 'byse.sx', 'clouddrop.cc'];
|
||||
const uiLocalizer = window.I18n.createDomLocalizer(document);
|
||||
uiLocalizer.start('en');
|
||||
uiLocalizer.start(new URLSearchParams(window.location.search).get('language'));
|
||||
|
||||
function setUiLanguage(value) {
|
||||
const language = uiLocalizer.setLanguage(window.I18n.normalizeLanguage(value));
|
||||
@@ -24,6 +24,9 @@ function formatRemoteClientStatus(port, count) {
|
||||
function refreshLocalizedRuntimeUi() {
|
||||
renderQueueTable();
|
||||
renderAccounts();
|
||||
sessionFilesData.forEach(row => {
|
||||
if (Number.isFinite(row.dateTs)) row.date = formatDateTime(row.dateTs).text;
|
||||
});
|
||||
renderRecentUploadsPanel();
|
||||
historyRowsData.forEach(row => {
|
||||
if (row.rawTimestamp !== undefined) {
|
||||
@@ -115,6 +118,8 @@ function _maybeLogRendererPerf(activeJobs) {
|
||||
_resetRendererPerf();
|
||||
}
|
||||
let accountStatuses = {}; // { accountId: { status: 'ok'|'warn'|'error'|'checking'|'unchecked', message: '' } }
|
||||
let accountStatusGenerationSequence = 0;
|
||||
const accountStatusGenerations = new Map();
|
||||
let editingAccountId = null; // null = adding, string = editing account by ID
|
||||
let autoHealthCheckEnabled = true;
|
||||
const queuePersistThrottle = (window.ThrottleTimer && window.ThrottleTimer.makeThrottleTimer)
|
||||
@@ -571,6 +576,7 @@ async function init() {
|
||||
// --- Tab switching ---
|
||||
let _historyDirty = false;
|
||||
let _historyEverLoaded = false;
|
||||
let clampRecentPanelHeight = () => {};
|
||||
function _isHistoryTabActive() {
|
||||
const tab = document.querySelector('.tab.active');
|
||||
return !!(tab && tab.dataset.view === 'history');
|
||||
@@ -601,6 +607,7 @@ function _isHistoryTabActive() {
|
||||
tab.tabIndex = 0;
|
||||
const nextView = viewsById[`${tab.dataset.view}-view`];
|
||||
if (nextView) nextView.classList.add('active');
|
||||
if (tab.dataset.view === 'upload') requestAnimationFrame(clampRecentPanelHeight);
|
||||
activeTab = tab;
|
||||
syncTabIndicator(tab);
|
||||
const activeSidebarButton = nextView?.querySelector('.view-sidebar-navigation > .view-sidebar-item.active, .settings-navigation > .settings-nav-button.active');
|
||||
@@ -993,14 +1000,37 @@ function maskCredential(value, keep = 4) {
|
||||
|
||||
function ensureAccountStatusEntries() {
|
||||
const nextStatuses = {};
|
||||
const activeIds = new Set();
|
||||
for (const { account } of getAllAccountsFlat()) {
|
||||
if (account.id) {
|
||||
activeIds.add(account.id);
|
||||
nextStatuses[account.id] = accountStatuses[account.id] || { status: 'unchecked', message: '' };
|
||||
}
|
||||
}
|
||||
for (const accountId of accountStatusGenerations.keys()) {
|
||||
if (!activeIds.has(accountId)) accountStatusGenerations.delete(accountId);
|
||||
}
|
||||
accountStatuses = nextStatuses;
|
||||
}
|
||||
|
||||
function _nextAccountStatusGeneration(accountId) {
|
||||
const generation = ++accountStatusGenerationSequence;
|
||||
accountStatusGenerations.set(accountId, generation);
|
||||
return generation;
|
||||
}
|
||||
|
||||
function _isCurrentAccountStatusGeneration(accountId, generation) {
|
||||
return accountStatusGenerations.get(accountId) === generation;
|
||||
}
|
||||
|
||||
function _finishAccountStatusGeneration(accountId, generation) {
|
||||
if (_isCurrentAccountStatusGeneration(accountId, generation)) accountStatusGenerations.delete(accountId);
|
||||
}
|
||||
|
||||
function _invalidateAccountStatusGeneration(accountId) {
|
||||
accountStatusGenerations.delete(accountId);
|
||||
}
|
||||
|
||||
// Returns flat array of all accounts: [{ name, account, index }]
|
||||
function getAllAccountsFlat() {
|
||||
const result = [];
|
||||
@@ -1969,7 +1999,8 @@ function renderQueueTable() {
|
||||
if (totalRows < 200) {
|
||||
// Try in-place update if row count matches (fast path)
|
||||
const existingRows = tbody.querySelectorAll('.queue-row');
|
||||
if (existingRows.length === totalRows && totalRows > 0) {
|
||||
const hasVirtualSpacers = Boolean(tbody.querySelector('.virtual-spacer'));
|
||||
if (!hasVirtualSpacers && existingRows.length === totalRows && totalRows > 0) {
|
||||
// In-place update – no DOM destruction
|
||||
for (let i = 0; i < totalRows; i++) {
|
||||
const tr = existingRows[i];
|
||||
@@ -2464,6 +2495,8 @@ function applyImportedConfig(importedConfig, message) {
|
||||
}
|
||||
};
|
||||
hosterSettings = config.hosterSettings || {};
|
||||
accountStatusGenerations.clear();
|
||||
accountStatuses = {};
|
||||
ensureAccountStatusEntries();
|
||||
syncSelectedUploadHosters();
|
||||
alwaysOnTopState = !!(config.globalSettings && config.globalSettings.alwaysOnTop);
|
||||
@@ -2709,7 +2742,7 @@ document.addEventListener('keydown', (e) => {
|
||||
cancelHosterModal();
|
||||
if (accountModal && accountModal.style.display !== 'none') closeAccountModal();
|
||||
}
|
||||
if (e.target.closest('input, textarea, select')) return;
|
||||
if (e.target instanceof window.Element && e.target.closest('input, textarea, select')) return;
|
||||
const activeView = document.querySelector('.view.active');
|
||||
// Ctrl+A
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
|
||||
@@ -3412,9 +3445,10 @@ function _handleStatsImpl(data) {
|
||||
if (el) el.textContent = formatDuration(Math.round((Date.now() - statsStartTime) / 1000));
|
||||
}, 1000);
|
||||
}
|
||||
} else if (data.state === 'idle' && statsRunTimer) {
|
||||
clearInterval(statsRunTimer);
|
||||
} else if (data.state === 'idle') {
|
||||
if (statsRunTimer) clearInterval(statsRunTimer);
|
||||
statsRunTimer = null;
|
||||
statsStartTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3934,7 +3968,15 @@ function _setRollingUploadMetric(id, value) {
|
||||
const nextText = numericValue.toLocaleString(getUiLocale());
|
||||
const previousValue = Number(element.dataset.numericValue) || 0;
|
||||
if (previousValue === numericValue) {
|
||||
const previousText = element.getAttribute('aria-label');
|
||||
element.setAttribute('aria-label', nextText);
|
||||
if (previousText !== null && previousText !== nextText) {
|
||||
element.querySelectorAll(':scope > span').forEach(span => span.getAnimations().forEach(animation => animation.cancel()));
|
||||
const settled = document.createElement('span');
|
||||
settled.textContent = nextText;
|
||||
element.replaceChildren(settled);
|
||||
element.dataset.direction = 'none';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4056,7 +4098,7 @@ function updateStatusBar() {
|
||||
_setRollingUploadMetric('uploadTelemetryRemaining', stats.remaining);
|
||||
_setRollingUploadMetric('uploadTelemetryRunning', stats.inProgress);
|
||||
_setRollingUploadMetric('uploadTelemetryCompleted', _sessionDoneCount);
|
||||
_setRollingUploadMetric('uploadTelemetryFailed', _sessionErrorCount);
|
||||
_setRollingUploadMetric('uploadTelemetryFailed', Math.max(_sessionErrorCount, stats.errors));
|
||||
updateUploadSpeedDisplays();
|
||||
_setUploadTelemetryText('uploadTelemetryEta', etaSeconds > 0 ? formatTime(etaSeconds) : '--:--');
|
||||
updateUploadSidebarSummary(stats);
|
||||
@@ -4141,12 +4183,17 @@ function showAppChoice({ message, title, confirmText, alternateText, cancelText
|
||||
return showAppDialog({ message, title, confirmText, alternateText, cancelText, showCancel: true });
|
||||
}
|
||||
|
||||
async function executeHealthCheck(hosters, _mode) {
|
||||
async function executeHealthCheck(hosters, _mode, generations) {
|
||||
renderHealthCheckResults([]);
|
||||
const result = await window.api.runHealthCheck({ hosters });
|
||||
const rows = result && Array.isArray(result.results) ? result.results : [];
|
||||
rows.forEach((row) => {
|
||||
if (!row) return;
|
||||
const currentRows = rows.filter((row) => {
|
||||
if (!row) return false;
|
||||
const key = row.accountId || row.hoster;
|
||||
const generation = generations?.get(key);
|
||||
return generation === undefined || _isCurrentAccountStatusGeneration(key, generation);
|
||||
});
|
||||
currentRows.forEach((row) => {
|
||||
const key = row.accountId || row.hoster;
|
||||
if (key) {
|
||||
accountStatuses[key] = {
|
||||
@@ -4156,10 +4203,10 @@ async function executeHealthCheck(hosters, _mode) {
|
||||
};
|
||||
}
|
||||
});
|
||||
renderHealthCheckResults(rows);
|
||||
renderHealthCheckResults(currentRows);
|
||||
renderAccounts();
|
||||
renderHosterModal();
|
||||
return rows;
|
||||
return currentRows;
|
||||
}
|
||||
|
||||
async function runHealthCheck(mode = 'manual', requestedHosters = null) {
|
||||
@@ -4180,18 +4227,21 @@ async function runHealthCheck(mode = 'manual', requestedHosters = null) {
|
||||
return [];
|
||||
}
|
||||
healthCheckRunning = true;
|
||||
const generations = new Map();
|
||||
// Mark all accounts as checking
|
||||
for (const h of hosters) {
|
||||
const key = typeof h === 'string' ? h : (h.accountId || h.hoster);
|
||||
generations.set(key, _nextAccountStatusGeneration(key));
|
||||
accountStatuses[key] = { status: 'checking', message: '', checkedAt: null };
|
||||
}
|
||||
renderAccounts();
|
||||
try {
|
||||
return await executeHealthCheck(hosters, mode);
|
||||
return await executeHealthCheck(hosters, mode, generations);
|
||||
} catch (err) {
|
||||
renderHealthCheckResults([{ hoster: 'System', status: 'error', message: err.message }]);
|
||||
return [];
|
||||
} finally {
|
||||
for (const [key, generation] of generations) _finishAccountStatusGeneration(key, generation);
|
||||
healthCheckRunning = false;
|
||||
renderAccounts();
|
||||
}
|
||||
@@ -4808,6 +4858,7 @@ function renderSettings() {
|
||||
const issuedEl = document.getElementById('diagCodeIssued');
|
||||
const badgeEl = document.getElementById('diagStatusBadge');
|
||||
if (!enabledEl) return;
|
||||
const diagnosticsEditedFields = new Set();
|
||||
|
||||
const fmtIssued = (ts) => {
|
||||
if (!ts) return '';
|
||||
@@ -4828,7 +4879,7 @@ function renderSettings() {
|
||||
const b = document.createElement('button');
|
||||
b.className = 'btn btn-xs btn-secondary';
|
||||
b.textContent = h;
|
||||
b.addEventListener('click', () => { publicHostEl.value = h; markSettingsDirty(); });
|
||||
b.addEventListener('click', () => { diagnosticsEditedFields.add(publicHostEl.id); publicHostEl.value = h; markSettingsDirty(); });
|
||||
suggestChips.appendChild(b);
|
||||
}
|
||||
} else {
|
||||
@@ -4838,18 +4889,18 @@ function renderSettings() {
|
||||
let lastSuggested = [];
|
||||
const applySettings = (s) => {
|
||||
if (!s) return;
|
||||
enabledEl.checked = !!s.enabled;
|
||||
portEl.value = s.port || 9110;
|
||||
modeEl.value = s.bindMode === 'network' ? 'network' : 'local';
|
||||
publicHostEl.value = s.publicHost || '';
|
||||
allowlistEl.value = Array.isArray(s.allowlist) ? s.allowlist.join('\n') : '';
|
||||
if (!diagnosticsEditedFields.has(enabledEl.id)) enabledEl.checked = !!s.enabled;
|
||||
if (!diagnosticsEditedFields.has(portEl.id)) portEl.value = s.port || 9110;
|
||||
if (!diagnosticsEditedFields.has(modeEl.id)) modeEl.value = s.bindMode === 'network' ? 'network' : 'local';
|
||||
if (!diagnosticsEditedFields.has(publicHostEl.id)) publicHostEl.value = s.publicHost || '';
|
||||
if (!diagnosticsEditedFields.has(allowlistEl.id)) allowlistEl.value = Array.isArray(s.allowlist) ? s.allowlist.join('\n') : '';
|
||||
lastSuggested = Array.isArray(s.suggestedHosts) ? s.suggestedHosts : [];
|
||||
codeEl.value = s.code || '';
|
||||
issuedEl.textContent = fmtIssued(s.codeIssuedAt);
|
||||
renderModeUi(lastSuggested);
|
||||
if (badgeEl) {
|
||||
badgeEl.textContent = s.enabled ? 'Aktiv' : 'Inaktiv';
|
||||
badgeEl.className = 'panel-status' + (s.enabled ? ' active' : '');
|
||||
badgeEl.textContent = enabledEl.checked ? 'Aktiv' : 'Inaktiv';
|
||||
badgeEl.className = 'panel-status' + (enabledEl.checked ? ' active' : '');
|
||||
}
|
||||
};
|
||||
const refreshStatus = () => {
|
||||
@@ -4876,6 +4927,11 @@ function renderSettings() {
|
||||
return true;
|
||||
};
|
||||
|
||||
[enabledEl, portEl, modeEl, publicHostEl, allowlistEl].forEach(element => {
|
||||
const markEdited = () => diagnosticsEditedFields.add(element.id);
|
||||
element.addEventListener('input', markEdited);
|
||||
element.addEventListener('change', markEdited);
|
||||
});
|
||||
window.api.diagnosticsGetSettings().then(applySettings).catch(() => {});
|
||||
refreshStatus();
|
||||
|
||||
@@ -5153,6 +5209,9 @@ async function performSaveSettings(options = {}) {
|
||||
config.hosterSettings = newHosterSettings;
|
||||
config.globalSettings = globalSettings;
|
||||
hosterSettings = newHosterSettings;
|
||||
const startupUrl = new URL(window.location.href);
|
||||
startupUrl.searchParams.set('language', globalSettings.language);
|
||||
window.history.replaceState(null, '', startupUrl.href);
|
||||
clearTimeout(settingsSaveTimer);
|
||||
settingsSaveTimer = null;
|
||||
|
||||
@@ -5545,8 +5604,8 @@ function _buildAccountHosterGroupHtml(name, accounts) {
|
||||
accounts.forEach((account, idx) => { cardsHtml += _buildAccountCardHtml(name, account, idx); });
|
||||
const lifeStat = _hosterLifetimeStat(name);
|
||||
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 account-hoster-lifetime-meta" data-hoster-lifetime="${escapeAttr(name)}" title="${escapeAttr(localizeUiText(`Erfolgsrate aus den letzten ${lifeStat.total} Uploads dieses Hosters`))}">${Math.round(lifeStat.rate * 100)}% ok (${lifeStat.total})</span>`
|
||||
: `<span class="account-hoster-group-meta account-hoster-lifetime-meta" data-hoster-lifetime="${escapeAttr(name)}" hidden></span>`;
|
||||
return `<div class="account-hoster-group" data-hoster-group="${name}">
|
||||
<div class="account-hoster-group-header" data-hoster-toggle="${name}" role="button" tabindex="0" aria-expanded="${isOpen}">
|
||||
<span class="panel-arrow">▶</span>
|
||||
@@ -5573,6 +5632,22 @@ function _hosterLifetimeStat(name) {
|
||||
}
|
||||
function _invalidateHosterLifetimeCache() { _hosterLifetimeCache = null; }
|
||||
|
||||
function _refreshAccountHosterLifetimeStats() {
|
||||
document.querySelectorAll('[data-hoster-lifetime]').forEach(meta => {
|
||||
const name = meta.dataset.hosterLifetime;
|
||||
const lifeStat = _hosterLifetimeStat(name);
|
||||
if (!lifeStat || lifeStat.total <= 0) {
|
||||
meta.hidden = true;
|
||||
meta.textContent = '';
|
||||
meta.removeAttribute('title');
|
||||
return;
|
||||
}
|
||||
meta.hidden = false;
|
||||
meta.textContent = `${Math.round(lifeStat.rate * 100)}% ok (${lifeStat.total})`;
|
||||
meta.title = localizeUiText(`Erfolgsrate aus den letzten ${lifeStat.total} Uploads dieses Hosters`);
|
||||
});
|
||||
}
|
||||
|
||||
function _allAccountGroupsOpen() {
|
||||
const bodies = document.querySelectorAll('#accountsList .account-hoster-group-body');
|
||||
if (!bodies.length) return false;
|
||||
@@ -5777,20 +5852,25 @@ async function checkSingleAccount(accountId) {
|
||||
if (!accountId || healthCheckRunning) return;
|
||||
const found = findAccountById(accountId);
|
||||
if (!found) return;
|
||||
const generation = _nextAccountStatusGeneration(accountId);
|
||||
healthCheckRunning = true;
|
||||
accountStatuses[accountId] = { status: 'checking', message: '' };
|
||||
updateAccountCard(accountId);
|
||||
let nextStatus = null;
|
||||
try {
|
||||
const result = await window.api.runHealthCheck({ hosters: [{ hoster: found.name, accountId }] });
|
||||
const rows = result && Array.isArray(result.results) ? result.results : [];
|
||||
const row = rows.find(r => r.accountId === accountId);
|
||||
if (row) accountStatuses[accountId] = { status: row.status || 'error', message: row.message || '' };
|
||||
if (row) nextStatus = { status: row.status || 'error', message: row.message || '' };
|
||||
} catch (err) {
|
||||
accountStatuses[accountId] = { status: 'error', message: err.message || 'Prüfung fehlgeschlagen' };
|
||||
nextStatus = { status: 'error', message: err.message || 'Prüfung fehlgeschlagen' };
|
||||
} finally {
|
||||
healthCheckRunning = false;
|
||||
}
|
||||
if (!_isCurrentAccountStatusGeneration(accountId, generation) || !findAccountById(accountId)) return;
|
||||
if (nextStatus) accountStatuses[accountId] = nextStatus;
|
||||
updateAccountCard(accountId);
|
||||
_finishAccountStatusGeneration(accountId, generation);
|
||||
}
|
||||
|
||||
async function submitAccountOtp(accountId) {
|
||||
@@ -5809,6 +5889,7 @@ async function submitAccountOtp(accountId) {
|
||||
return;
|
||||
}
|
||||
const submitButton = card?.querySelector('[data-account-otp-submit]');
|
||||
const generation = _nextAccountStatusGeneration(accountId);
|
||||
healthCheckRunning = true;
|
||||
accountStatuses[accountId] = { status: 'checking', message: 'OTP wird geprüft…' };
|
||||
if (otpInput) otpInput.disabled = true;
|
||||
@@ -5816,21 +5897,25 @@ async function submitAccountOtp(accountId) {
|
||||
submitButton.disabled = true;
|
||||
submitButton.textContent = 'Prüfe…';
|
||||
}
|
||||
let nextStatus;
|
||||
try {
|
||||
const result = await window.api.runHealthCheck({ hosters: [{ hoster: found.name, accountId, otp }] });
|
||||
const row = result && Array.isArray(result.results)
|
||||
? result.results.find(item => item.accountId === accountId)
|
||||
: null;
|
||||
accountStatuses[accountId] = row
|
||||
nextStatus = row
|
||||
? { status: row.status || 'error', message: row.message || '' }
|
||||
: { status: 'error', message: 'Keine Antwort vom Hoster erhalten' };
|
||||
} catch (err) {
|
||||
accountStatuses[accountId] = { status: 'error', message: err.message || 'OTP-Prüfung fehlgeschlagen' };
|
||||
nextStatus = { status: 'error', message: err.message || 'OTP-Prüfung fehlgeschlagen' };
|
||||
} finally {
|
||||
healthCheckRunning = false;
|
||||
updateAccountCard(accountId);
|
||||
renderHosterModal();
|
||||
}
|
||||
if (!_isCurrentAccountStatusGeneration(accountId, generation) || !findAccountById(accountId)) return;
|
||||
accountStatuses[accountId] = nextStatus;
|
||||
updateAccountCard(accountId);
|
||||
renderHosterModal();
|
||||
_finishAccountStatusGeneration(accountId, generation);
|
||||
}
|
||||
|
||||
// Per-hoster overrides for the login form. VOE only accepts emails — the
|
||||
@@ -5972,6 +6057,7 @@ async function deleteAccount(accountId) {
|
||||
if (Array.isArray(accounts)) {
|
||||
config.hosters[found.name] = accounts.filter(a => a.id !== accountId);
|
||||
}
|
||||
_invalidateAccountStatusGeneration(accountId);
|
||||
delete accountStatuses[accountId];
|
||||
// saveConfig is async — close the modal immediately so the UI feels
|
||||
// responsive instead of waiting for the atomic write + safeStorage encrypt.
|
||||
@@ -6190,6 +6276,7 @@ async function _persistAccount(ctx, creds) {
|
||||
function _applyCommittedAccount(persisted, validation) {
|
||||
const { accountId, candidateHosters, isEdit } = persisted;
|
||||
config.hosters = candidateHosters;
|
||||
_invalidateAccountStatusGeneration(accountId);
|
||||
accountStatuses[accountId] = { status: validation.status, message: validation.message || '' };
|
||||
ensureAccountStatusEntries();
|
||||
syncSelectedUploadHosters();
|
||||
@@ -6327,13 +6414,16 @@ async function confirmHistoryClear() {
|
||||
}
|
||||
}
|
||||
|
||||
let _historyLoadGeneration = 0;
|
||||
async function loadHistory() {
|
||||
const generation = ++_historyLoadGeneration;
|
||||
const container = document.getElementById('historyContainer');
|
||||
if (container) container.innerHTML = `<p class="empty-state history-loading-state">${localizeUiText('Wird geladen…')}</p>`;
|
||||
let history;
|
||||
try {
|
||||
history = await window.api.getHistory();
|
||||
} catch (error) {
|
||||
if (generation !== _historyLoadGeneration) return;
|
||||
historyRowsData = [];
|
||||
historySidebarCounts = { total: 0, success: 0, error: 0, skipped: 0 };
|
||||
updateHistorySidebarSummary();
|
||||
@@ -6342,10 +6432,12 @@ async function loadHistory() {
|
||||
container?.querySelector('[data-retry-history]')?.addEventListener('click', loadHistory);
|
||||
return;
|
||||
}
|
||||
if (generation !== _historyLoadGeneration) return;
|
||||
window._historyForStats = history || [];
|
||||
_historyEverLoaded = true;
|
||||
_historyDirty = false;
|
||||
_invalidateHosterLifetimeCache();
|
||||
_refreshAccountHosterLifetimeStats();
|
||||
const retSel = document.getElementById('historyRetentionSelect');
|
||||
if (retSel) {
|
||||
retSel.value = (config.globalSettings && config.globalSettings.historyRetention) || 'all';
|
||||
@@ -6498,6 +6590,7 @@ function _renderRecentVirtualRows() {
|
||||
function renderRecentUploadsPanel(_appendOnly = false) {
|
||||
const tbody = document.getElementById('recentFilesBody');
|
||||
if (!tbody) return;
|
||||
const pendingAppends = _recentPendingAppends;
|
||||
_recentPendingAppends = 0;
|
||||
const wrap = tbody.closest('.recent-files-table-wrap');
|
||||
|
||||
@@ -6511,7 +6604,7 @@ function renderRecentUploadsPanel(_appendOnly = false) {
|
||||
_recentLastRange = { start: -1, end: -1 };
|
||||
const sig = `${recentSortState.key}|${recentSortState.direction}`;
|
||||
if (wrap) {
|
||||
const added = _recentWorking.length - prevLen;
|
||||
const added = Math.max(_recentWorking.length - prevLen, pendingAppends);
|
||||
if (sig === 'date|desc' && wrap.scrollTop <= 48) wrap.scrollTop = 0;
|
||||
else if (sig === 'date|desc' && added > 0) wrap.scrollTop += added * VIRTUAL_ROW_HEIGHT;
|
||||
}
|
||||
@@ -7779,6 +7872,44 @@ function showCopyToast(msg, durationMs) {
|
||||
if (resizer && panel) {
|
||||
let startY = 0;
|
||||
let startH = 0;
|
||||
let resizeFrame = 0;
|
||||
|
||||
const maxPanelHeight = () => {
|
||||
if (!document.getElementById('upload-view')?.classList.contains('active')) return null;
|
||||
const shell = panel.closest('.queue-shell');
|
||||
const queue = document.getElementById('queueContainer');
|
||||
if (!shell || !queue) return window.innerHeight * 0.7;
|
||||
const shellRect = shell.getBoundingClientRect();
|
||||
const queueRect = queue.getBoundingClientRect();
|
||||
if (shellRect.width <= 0 || shellRect.height <= 0 || queueRect.width <= 0) return null;
|
||||
let betweenHeight = 0;
|
||||
let afterQueue = false;
|
||||
for (const element of shell.children) {
|
||||
if (element === queue) {
|
||||
afterQueue = true;
|
||||
continue;
|
||||
}
|
||||
if (element === panel) break;
|
||||
if (afterQueue) betweenHeight += element.getBoundingClientRect().height;
|
||||
}
|
||||
const shellBottom = Math.min(shellRect.bottom, window.innerHeight);
|
||||
const availableHeight = Math.max(0, shellBottom - queueRect.top - betweenHeight);
|
||||
return Math.max(60, Math.min(window.innerHeight * 0.7, availableHeight - 120));
|
||||
};
|
||||
|
||||
clampRecentPanelHeight = () => {
|
||||
const requestedHeight = Number.parseFloat(panel.style.flexBasis);
|
||||
if (!Number.isFinite(requestedHeight)) return;
|
||||
const maximumHeight = maxPanelHeight();
|
||||
if (!Number.isFinite(maximumHeight)) return;
|
||||
const nextHeight = Math.max(60, Math.min(requestedHeight, maximumHeight));
|
||||
if (Math.abs(nextHeight - requestedHeight) > 0.5) panel.style.flex = `0 0 ${nextHeight}px`;
|
||||
};
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
window.cancelAnimationFrame(resizeFrame);
|
||||
resizeFrame = window.requestAnimationFrame(clampRecentPanelHeight);
|
||||
});
|
||||
|
||||
resizer.addEventListener('mousedown', (e) => {
|
||||
e.preventDefault();
|
||||
@@ -7790,7 +7921,9 @@ function showCopyToast(msg, durationMs) {
|
||||
|
||||
const onMove = (e2) => {
|
||||
const delta = startY - e2.clientY;
|
||||
const newH = Math.max(60, Math.min(window.innerHeight * 0.7, startH + delta));
|
||||
const maximumHeight = maxPanelHeight();
|
||||
if (!Number.isFinite(maximumHeight)) return;
|
||||
const newH = Math.max(60, Math.min(maximumHeight, startH + delta));
|
||||
panel.style.flex = `0 0 ${newH}px`;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user