feat: resizable queue table columns (JDownloader-style)

Drag the right edge of any queue column header to resize it. Cursor
changes to col-resize on hover. Widths are saved to localStorage and
restored on next launch.

- Resizer handles in all 8 queue table columns
- Resize state visible via dragging class + body cursor override
- Min width 40px, no max (table can scroll horizontally)
- Click on resizer doesn't trigger column sort
- Persisted across sessions via localStorage

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Administrator 2026-04-06 23:13:40 +02:00
parent e02926e849
commit e7dd91ae59
3 changed files with 94 additions and 9 deletions

View File

@ -24,6 +24,7 @@ let queuePersistTimer = null;
let settingsSaveTimer = null;
let lastUploadStats = { state: 'idle', globalSpeedKbs: 0, totalBytes: 0, elapsed: 0, activeJobs: 0 };
const AUTO_CHECK_PREF_KEY = 'autoHealthCheckBeforeUpload';
const QUEUE_COL_WIDTHS_KEY = 'queueColumnWidthsPx';
const STARTABLE_QUEUE_STATUSES = new Set(['preview', 'queued', 'error', 'aborted', 'skipped']);
function isStartableQueueStatus(status) {
@ -71,6 +72,7 @@ async function init() {
renderAccounts();
setupListeners();
setupDragDrop();
restoreQueueColumnWidths();
loadHistory();
renderRecentUploadsPanel();
updateUploadView();
@ -3263,7 +3265,9 @@ function setupListeners() {
// Queue table sorting
document.querySelectorAll('#queueTable th.sortable').forEach(th => {
th.addEventListener('click', () => {
th.addEventListener('click', (e) => {
// Don't sort if click was on the resizer handle
if (e.target.classList.contains('col-resizer')) return;
const key = th.dataset.sort;
if (queueSortState.key === key) queueSortState.direction = queueSortState.direction === 'asc' ? 'desc' : 'asc';
else { queueSortState.key = key; queueSortState.direction = 'asc'; }
@ -3272,6 +3276,9 @@ function setupListeners() {
});
});
// Queue table column resizing (JDownloader-style)
setupColumnResizing();
// Shutdown cancel
document.getElementById('cancelShutdownBtn').addEventListener('click', async () => {
await window.api.cancelShutdown();
@ -3516,6 +3523,67 @@ function loadAutoCheckPreference() {
catch { return true; }
}
// --- Queue table column resizing (JDownloader-style) ---
function restoreQueueColumnWidths() {
try {
const raw = localStorage.getItem(QUEUE_COL_WIDTHS_KEY);
if (!raw) return;
const widths = JSON.parse(raw);
if (!widths || typeof widths !== 'object') return;
for (const [col, px] of Object.entries(widths)) {
const th = document.querySelector(`#queueTable th[data-col="${col}"]`);
if (th && typeof px === 'number' && px > 20) {
th.style.width = px + 'px';
}
}
} catch {}
}
function saveQueueColumnWidths() {
try {
const widths = {};
document.querySelectorAll('#queueTable th[data-col]').forEach(th => {
widths[th.dataset.col] = th.getBoundingClientRect().width;
});
localStorage.setItem(QUEUE_COL_WIDTHS_KEY, JSON.stringify(widths));
} catch {}
}
function setupColumnResizing() {
const headers = document.querySelectorAll('#queueTable th[data-col]');
headers.forEach(th => {
const resizer = th.querySelector('.col-resizer');
if (!resizer) return;
resizer.addEventListener('mousedown', (e) => {
e.preventDefault();
e.stopPropagation();
const startX = e.clientX;
const startWidth = th.getBoundingClientRect().width;
resizer.classList.add('dragging');
document.body.classList.add('col-resizing');
const onMove = (ev) => {
const delta = ev.clientX - startX;
const newWidth = Math.max(40, startWidth + delta);
th.style.width = newWidth + 'px';
};
const onUp = () => {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
resizer.classList.remove('dragging');
document.body.classList.remove('col-resizing');
saveQueueColumnWidths();
};
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
});
}
function escapeHtml(str) {
if (!str) return '';
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');

View File

@ -77,14 +77,14 @@
<table class="queue-table" id="queueTable">
<thead>
<tr>
<th class="col-filename sortable" data-sort="filename">Filename</th>
<th class="col-size sortable" data-sort="size">Uploaded / Size</th>
<th class="col-host sortable" data-sort="host">Host</th>
<th class="col-status sortable" data-sort="status">Status</th>
<th class="col-elapsed">Zeit</th>
<th class="col-remaining">Rest</th>
<th class="col-speed sortable" data-sort="speed">Speed</th>
<th class="col-progress sortable" data-sort="progress">Progress</th>
<th class="col-filename sortable" data-col="filename" data-sort="filename">Filename<span class="col-resizer"></span></th>
<th class="col-size sortable" data-col="size" data-sort="size">Uploaded / Size<span class="col-resizer"></span></th>
<th class="col-host sortable" data-col="host" data-sort="host">Host<span class="col-resizer"></span></th>
<th class="col-status sortable" data-col="status" data-sort="status">Status<span class="col-resizer"></span></th>
<th class="col-elapsed" data-col="elapsed">Zeit<span class="col-resizer"></span></th>
<th class="col-remaining" data-col="remaining">Rest<span class="col-resizer"></span></th>
<th class="col-speed sortable" data-col="speed" data-sort="speed">Speed<span class="col-resizer"></span></th>
<th class="col-progress sortable" data-col="progress" data-sort="progress">Progress</th>
</tr>
</thead>
<tbody id="queueBody"></tbody>

View File

@ -267,6 +267,23 @@ body {
}
.queue-table th.sortable { cursor: pointer; }
.queue-table th.sortable:hover { color: var(--text); }
.queue-table th { position: relative; }
.col-resizer {
position: absolute;
top: 0;
right: 0;
width: 6px;
height: 100%;
cursor: col-resize;
user-select: none;
z-index: 6;
background: transparent;
transition: background 0.15s;
}
.col-resizer:hover { background: rgba(102, 126, 234, 0.4); }
.col-resizer.dragging { background: rgba(102, 126, 234, 0.6); }
body.col-resizing, body.col-resizing * { cursor: col-resize !important; user-select: none !important; }
.col-filename { width: 30%; }
.col-size { width: 12%; }