Files
decypharr/pkg/qbit/server/templates/index.html

222 lines
9.3 KiB
HTML

{{ define "index" }}
<div class="container mt-4">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h4 class="mb-0"><i class="bi bi-table me-2"></i>Active Torrents</h4>
<div>
<button class="btn btn-outline-danger btn-sm me-2" id="batchDeleteBtn" style="display: none;">
<i class="bi bi-trash me-1"></i>Delete Selected
</button>
<button class="btn btn-outline-secondary btn-sm me-2" id="refreshBtn">
<i class="bi bi-arrow-clockwise me-1"></i>Refresh
</button>
<select class="form-select form-select-sm d-inline-block w-auto" id="categoryFilter">
<option value="">All Categories</option>
</select>
</div>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>
<input type="checkbox" class="form-check-input" id="selectAll">
</th>
<th>Name</th>
<th>Size</th>
<th>Progress</th>
<th>Speed</th>
<th>Category</th>
<th>Debrid</th>
<th>State</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="torrentsList">
</tbody>
</table>
</div>
</div>
</div>
</div>
<script>
const torrentRowTemplate = (torrent) => `
<tr data-hash="${torrent.hash}">
<td>
<input type="checkbox" class="form-check-input torrent-select" data-hash="${torrent.hash}">
</td>
<td class="text-nowrap text-truncate overflow-hidden" style="max-width: 350px;" title="${torrent.name}">${torrent.name}</td>
<td class="text-nowrap">${formatBytes(torrent.size)}</td>
<td style="min-width: 150px;">
<div class="progress" style="height: 8px;">
<div class="progress-bar" role="progressbar"
style="width: ${(torrent.progress * 100).toFixed(1)}%"
aria-valuenow="${(torrent.progress * 100).toFixed(1)}"
aria-valuemin="0"
aria-valuemax="100"></div>
</div>
<small class="text-muted">${(torrent.progress * 100).toFixed(1)}%</small>
</td>
<td>${formatSpeed(torrent.dlspeed)}</td>
<td><span class="badge bg-secondary">${torrent.category || 'None'}</span></td>
<td>${torrent.debrid || 'None'}</td>
<td><span class="badge ${getStateColor(torrent.state)}">${torrent.state}</span></td>
<td>
<button class="btn btn-sm btn-outline-danger" onclick="deleteTorrent('${torrent.hash}')">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
`;
function formatBytes(bytes) {
if (!bytes) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
}
function formatSpeed(speed) {
return `${formatBytes(speed)}/s`;
}
function getStateColor(state) {
const stateColors = {
'downloading': 'bg-primary',
'pausedup': 'bg-success',
'error': 'bg-danger',
};
return stateColors[state?.toLowerCase()] || 'bg-secondary';
}
let refreshInterval;
let selectedTorrents = new Set();
async function loadTorrents() {
try {
const response = await fetch('/internal/torrents');
const torrents = await response.json();
const tbody = document.getElementById('torrentsList');
tbody.innerHTML = torrents.map(torrent => torrentRowTemplate(torrent)).join('');
restoreSelections();
// Update category filter options
let category = document.getElementById('categoryFilter').value;
document.querySelectorAll('#torrentsList tr').forEach(row => {
const rowCategory = row.querySelector('td:nth-child(6)').textContent;
row.style.display = (!category || rowCategory.includes(category)) ? '' : 'none';
});
updateCategoryFilter(torrents);
} catch (error) {
console.error('Error loading torrents:', error);
}
}
function updateCategoryFilter(torrents) {
const categories = [...new Set(torrents.map(t => t.category).filter(Boolean))];
const select = document.getElementById('categoryFilter');
const currentValue = select.value;
select.innerHTML = '<option value="">All Categories</option>' +
categories.map(cat => `<option value="${cat}" ${cat === currentValue ? 'selected' : ''}>${cat}</option>`).join('');
}
function restoreSelections() {
selectedTorrents.forEach(hash => {
const checkbox = document.querySelector(`input[data-hash="${hash}"]`);
if (checkbox) {
checkbox.checked = true;
}
});
updateBatchDeleteButton();
}
function updateBatchDeleteButton() {
const batchDeleteBtn = document.getElementById('batchDeleteBtn');
batchDeleteBtn.style.display = selectedTorrents.size > 0 ? '' : 'none';
}
async function deleteTorrent(hash) {
if (!confirm('Are you sure you want to delete this torrent?')) return;
try {
await fetch(`/internal/torrents/${hash}`, {
method: 'DELETE'
});
selectedTorrents.delete(hash);
await loadTorrents();
} catch (error) {
console.error('Error deleting torrent:', error);
alert('Failed to delete torrent');
}
}
async function deleteSelectedTorrents() {
if (!confirm(`Are you sure you want to delete ${selectedTorrents.size} selected torrents?`)) return;
try {
const deletePromises = Array.from(selectedTorrents).map(hash =>
fetch(`/internal/torrents/${hash}`, { method: 'DELETE' })
);
await Promise.all(deletePromises);
selectedTorrents.clear();
await loadTorrents();
document.getElementById('selectAll').checked = false;
} catch (error) {
console.error('Error deleting torrents:', error);
alert('Failed to delete some torrents');
}
}
document.addEventListener('DOMContentLoaded', () => {
loadTorrents();
refreshInterval = setInterval(loadTorrents, 5000);
document.getElementById('refreshBtn').addEventListener('click', loadTorrents);
document.getElementById('batchDeleteBtn').addEventListener('click', deleteSelectedTorrents);
document.getElementById('selectAll').addEventListener('change', (e) => {
const checkboxes = document.querySelectorAll('.torrent-select');
checkboxes.forEach(checkbox => {
checkbox.checked = e.target.checked;
const hash = checkbox.dataset.hash;
if (e.target.checked) {
selectedTorrents.add(hash);
} else {
selectedTorrents.delete(hash);
}
});
updateBatchDeleteButton();
});
document.getElementById('torrentsList').addEventListener('change', (e) => {
if (e.target.classList.contains('torrent-select')) {
const hash = e.target.dataset.hash;
if (e.target.checked) {
selectedTorrents.add(hash);
} else {
selectedTorrents.delete(hash);
}
updateBatchDeleteButton();
}
});
document.getElementById('categoryFilter').addEventListener('change', (e) => {
const category = e.target.value;
document.querySelectorAll('#torrentsList tr').forEach(row => {
const rowCategory = row.querySelector('td:nth-child(6)').textContent;
row.style.display = (!category || rowCategory.includes(category)) ? '' : 'none';
});
});
});
window.addEventListener('beforeunload', () => {
clearInterval(refreshInterval);
});
</script>
{{ end }}