✨ 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:
parent
e02926e849
commit
e7dd91ae59
@ -24,6 +24,7 @@ let queuePersistTimer = null;
|
|||||||
let settingsSaveTimer = null;
|
let settingsSaveTimer = null;
|
||||||
let lastUploadStats = { state: 'idle', globalSpeedKbs: 0, totalBytes: 0, elapsed: 0, activeJobs: 0 };
|
let lastUploadStats = { state: 'idle', globalSpeedKbs: 0, totalBytes: 0, elapsed: 0, activeJobs: 0 };
|
||||||
const AUTO_CHECK_PREF_KEY = 'autoHealthCheckBeforeUpload';
|
const AUTO_CHECK_PREF_KEY = 'autoHealthCheckBeforeUpload';
|
||||||
|
const QUEUE_COL_WIDTHS_KEY = 'queueColumnWidthsPx';
|
||||||
const STARTABLE_QUEUE_STATUSES = new Set(['preview', 'queued', 'error', 'aborted', 'skipped']);
|
const STARTABLE_QUEUE_STATUSES = new Set(['preview', 'queued', 'error', 'aborted', 'skipped']);
|
||||||
|
|
||||||
function isStartableQueueStatus(status) {
|
function isStartableQueueStatus(status) {
|
||||||
@ -71,6 +72,7 @@ async function init() {
|
|||||||
renderAccounts();
|
renderAccounts();
|
||||||
setupListeners();
|
setupListeners();
|
||||||
setupDragDrop();
|
setupDragDrop();
|
||||||
|
restoreQueueColumnWidths();
|
||||||
loadHistory();
|
loadHistory();
|
||||||
renderRecentUploadsPanel();
|
renderRecentUploadsPanel();
|
||||||
updateUploadView();
|
updateUploadView();
|
||||||
@ -3263,7 +3265,9 @@ function setupListeners() {
|
|||||||
|
|
||||||
// Queue table sorting
|
// Queue table sorting
|
||||||
document.querySelectorAll('#queueTable th.sortable').forEach(th => {
|
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;
|
const key = th.dataset.sort;
|
||||||
if (queueSortState.key === key) queueSortState.direction = queueSortState.direction === 'asc' ? 'desc' : 'asc';
|
if (queueSortState.key === key) queueSortState.direction = queueSortState.direction === 'asc' ? 'desc' : 'asc';
|
||||||
else { queueSortState.key = key; queueSortState.direction = 'asc'; }
|
else { queueSortState.key = key; queueSortState.direction = 'asc'; }
|
||||||
@ -3272,6 +3276,9 @@ function setupListeners() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Queue table column resizing (JDownloader-style)
|
||||||
|
setupColumnResizing();
|
||||||
|
|
||||||
// Shutdown cancel
|
// Shutdown cancel
|
||||||
document.getElementById('cancelShutdownBtn').addEventListener('click', async () => {
|
document.getElementById('cancelShutdownBtn').addEventListener('click', async () => {
|
||||||
await window.api.cancelShutdown();
|
await window.api.cancelShutdown();
|
||||||
@ -3516,6 +3523,67 @@ function loadAutoCheckPreference() {
|
|||||||
catch { return true; }
|
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) {
|
function escapeHtml(str) {
|
||||||
if (!str) return '';
|
if (!str) return '';
|
||||||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||||
|
|||||||
@ -77,14 +77,14 @@
|
|||||||
<table class="queue-table" id="queueTable">
|
<table class="queue-table" id="queueTable">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="col-filename sortable" data-sort="filename">Filename</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-sort="size">Uploaded / Size</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-sort="host">Host</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-sort="status">Status</th>
|
<th class="col-status sortable" data-col="status" data-sort="status">Status<span class="col-resizer"></span></th>
|
||||||
<th class="col-elapsed">Zeit</th>
|
<th class="col-elapsed" data-col="elapsed">Zeit<span class="col-resizer"></span></th>
|
||||||
<th class="col-remaining">Rest</th>
|
<th class="col-remaining" data-col="remaining">Rest<span class="col-resizer"></span></th>
|
||||||
<th class="col-speed sortable" data-sort="speed">Speed</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-sort="progress">Progress</th>
|
<th class="col-progress sortable" data-col="progress" data-sort="progress">Progress</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="queueBody"></tbody>
|
<tbody id="queueBody"></tbody>
|
||||||
|
|||||||
@ -267,6 +267,23 @@ body {
|
|||||||
}
|
}
|
||||||
.queue-table th.sortable { cursor: pointer; }
|
.queue-table th.sortable { cursor: pointer; }
|
||||||
.queue-table th.sortable:hover { color: var(--text); }
|
.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-filename { width: 30%; }
|
||||||
.col-size { width: 12%; }
|
.col-size { width: 12%; }
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user