- Add shinning UI
- Revamp deployment process - Fix Alldebrid file node bug
This commit is contained in:
143
pkg/qbit/server/templates/index.html
Normal file
143
pkg/qbit/server/templates/index.html
Normal file
@@ -0,0 +1,143 @@
|
||||
{{ 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-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>Name</th>
|
||||
<th>Size</th>
|
||||
<th>Progress</th>
|
||||
<th>Speed</th>
|
||||
<th>Category</th>
|
||||
<th>State</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="torrentsList">
|
||||
<!-- Will be populated by JavaScript -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const torrentRowTemplate = (torrent) => `
|
||||
<tr>
|
||||
<td class="text-break">${torrent.name}</td>
|
||||
<td>${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><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;
|
||||
|
||||
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('');
|
||||
|
||||
// Update category filter options
|
||||
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('');
|
||||
}
|
||||
|
||||
async function deleteTorrent(hash) {
|
||||
if (!confirm('Are you sure you want to delete this torrent?')) return;
|
||||
|
||||
try {
|
||||
await fetch(`/internal/torrents/${hash}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
await loadTorrents();
|
||||
} catch (error) {
|
||||
console.error('Error deleting torrent:', error);
|
||||
alert('Failed to delete torrent');
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadTorrents();
|
||||
refreshInterval = setInterval(loadTorrents, 5000); // Refresh every 5 seconds
|
||||
|
||||
document.getElementById('refreshBtn').addEventListener('click', loadTorrents);
|
||||
|
||||
document.getElementById('categoryFilter').addEventListener('change', (e) => {
|
||||
const category = e.target.value;
|
||||
document.querySelectorAll('#torrentsList tr').forEach(row => {
|
||||
const rowCategory = row.querySelector('td:nth-child(5)').textContent;
|
||||
row.style.display = (!category || rowCategory.includes(category)) ? '' : 'none';
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener('beforeunload', () => {
|
||||
clearInterval(refreshInterval);
|
||||
});
|
||||
</script>
|
||||
{{ end }}
|
||||
Reference in New Issue
Block a user