Implements update notifications by fetching the latest version from a remote source. Displays a warning message on the home and setup pages if the current version is outdated. The update check is performed on application startup.
311 lines
7.6 KiB
HTML
311 lines
7.6 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>qBittorrent Overview</title>
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<style>
|
|
body {
|
|
background: #121212;
|
|
color: #e0e0e0;
|
|
font-family: "Segoe UI", sans-serif;
|
|
margin: 0;
|
|
padding: 2rem;
|
|
}
|
|
|
|
nav {
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
nav a {
|
|
color: #80dfff;
|
|
margin-right: 1.5rem;
|
|
text-decoration: none;
|
|
font-weight: bold;
|
|
}
|
|
|
|
h1,
|
|
h2 {
|
|
margin-top: 0;
|
|
}
|
|
|
|
.grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
|
gap: 1rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.card {
|
|
background: #1e1e1e;
|
|
border-radius: 8px;
|
|
padding: 1rem;
|
|
text-align: center;
|
|
}
|
|
|
|
.card h3 {
|
|
margin: 0;
|
|
font-size: 1.8rem;
|
|
color: #4bc0c0;
|
|
}
|
|
|
|
.card p {
|
|
margin: 0.2rem;
|
|
color: #aaa;
|
|
}
|
|
|
|
.flex {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 2rem;
|
|
}
|
|
|
|
.chart-container,
|
|
.news-feed {
|
|
background: #1e1e1e;
|
|
padding: 1rem;
|
|
border-radius: 8px;
|
|
flex: 1;
|
|
min-width: 380px;
|
|
}
|
|
|
|
.news-feed {
|
|
max-height: 500px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.event {
|
|
margin-bottom: 0.75rem;
|
|
padding-left: 0.5rem;
|
|
border-left: 4px solid #444;
|
|
}
|
|
|
|
.event .time {
|
|
font-size: 0.9rem;
|
|
color: #999;
|
|
}
|
|
|
|
.event .state {
|
|
font-weight: bold;
|
|
color: #00e676;
|
|
}
|
|
|
|
.toggle {
|
|
background: #222;
|
|
padding: 1rem;
|
|
border-radius: 8px;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.toggle label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.toggle input {
|
|
transform: scale(1.3);
|
|
}
|
|
|
|
.warning {
|
|
background: #2a1b1b;
|
|
color: #ff6f61;
|
|
padding: 0.75rem 1rem;
|
|
margin-top: 1rem;
|
|
border-left: 4px solid #ff6f61;
|
|
border-radius: 6px;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.warning strong {
|
|
color: #ffab91;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
|
|
<nav>
|
|
<a href="/">Home</a>
|
|
<a href="/dashboard">Dashboard</a>
|
|
<a href="/news">News</a>
|
|
<a href="/settings">Settings</a>
|
|
</nav>
|
|
|
|
{% if outdated %}
|
|
<div class="warning">
|
|
<strong>Outdated:</strong> You are running version
|
|
<b>{{ current_version }}</b>. The latest is <b>{{ latest_version }}</b>.
|
|
</div>
|
|
{% endif %}
|
|
|
|
<h1>Overview</h1>
|
|
|
|
<div class="toggle">
|
|
<label>
|
|
<input type="checkbox" id="resume-toggle" />
|
|
Auto-resume errored torrents
|
|
</label>
|
|
</div>
|
|
|
|
<div class="grid" id="metrics"></div>
|
|
|
|
<div class="flex">
|
|
<div class="chart-container">
|
|
<h2>Torrent State Distribution</h2>
|
|
<canvas id="stateChart"></canvas>
|
|
</div>
|
|
<div class="chart-container">
|
|
<h2>Bandwidth Usage</h2>
|
|
<canvas id="bandwidthChart"></canvas>
|
|
</div>
|
|
<div class="news-feed" id="news-feed">
|
|
<h2>Status Feed</h2>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let stateChart, bandwidthChart;
|
|
|
|
async function loadStats() {
|
|
const res = await fetch("/api/stats");
|
|
const stats = await res.json();
|
|
const grid = document.getElementById("metrics");
|
|
grid.innerHTML = "";
|
|
|
|
const stateCounts = stats.states || {};
|
|
const avg = stats.average_progress || 0;
|
|
|
|
for (const [state, count] of Object.entries(stateCounts)) {
|
|
const div = document.createElement("div");
|
|
div.className = "card";
|
|
div.innerHTML = `<h3>${count}</h3><p>${state}</p>`;
|
|
grid.appendChild(div);
|
|
}
|
|
|
|
const avgCard = document.createElement("div");
|
|
avgCard.className = "card";
|
|
avgCard.innerHTML = `<h3>${avg}%</h3><p>Overall Progress</p>`;
|
|
grid.appendChild(avgCard);
|
|
|
|
updateStateChart(stateCounts);
|
|
}
|
|
|
|
function updateStateChart(stateData) {
|
|
const labels = Object.keys(stateData);
|
|
const values = Object.values(stateData);
|
|
const colors = [
|
|
"#4bc0c0",
|
|
"#36a2eb",
|
|
"#9966ff",
|
|
"#ff6384",
|
|
"#ffcd56",
|
|
"#7fdb7f",
|
|
];
|
|
|
|
if (!stateChart) {
|
|
stateChart = new Chart(document.getElementById("stateChart"), {
|
|
type: "pie",
|
|
data: {
|
|
labels: labels,
|
|
datasets: [{ data: values, backgroundColor: colors }],
|
|
},
|
|
options: { plugins: { legend: { labels: { color: "#eee" } } } },
|
|
});
|
|
} else {
|
|
stateChart.data.labels = labels;
|
|
stateChart.data.datasets[0].data = values;
|
|
stateChart.update();
|
|
}
|
|
}
|
|
|
|
async function loadBandwidth() {
|
|
const res = await fetch("/api/bandwidth");
|
|
const history = await res.json();
|
|
const labels = history.map((h) => h.time);
|
|
const down = history.map((h) => h.download);
|
|
const up = history.map((h) => h.upload);
|
|
|
|
if (!bandwidthChart) {
|
|
bandwidthChart = new Chart(
|
|
document.getElementById("bandwidthChart"),
|
|
{
|
|
type: "line",
|
|
data: {
|
|
labels,
|
|
datasets: [
|
|
{
|
|
label: "Download KB/s",
|
|
data: down,
|
|
borderColor: "#4bc0c0",
|
|
tension: 0.3,
|
|
},
|
|
{
|
|
label: "Upload KB/s",
|
|
data: up,
|
|
borderColor: "#ff6384",
|
|
tension: 0.3,
|
|
},
|
|
],
|
|
},
|
|
options: {
|
|
scales: {
|
|
x: { ticks: { color: "#aaa" } },
|
|
y: { ticks: { color: "#aaa" } },
|
|
},
|
|
plugins: { legend: { labels: { color: "#eee" } } },
|
|
},
|
|
}
|
|
);
|
|
} else {
|
|
bandwidthChart.data.labels = labels;
|
|
bandwidthChart.data.datasets[0].data = down;
|
|
bandwidthChart.data.datasets[1].data = up;
|
|
bandwidthChart.update();
|
|
}
|
|
}
|
|
|
|
async function loadNews() {
|
|
const res = await fetch("/api/news");
|
|
const news = await res.json();
|
|
const feed = document.getElementById("news-feed");
|
|
feed.innerHTML = "<h2>Status Feed</h2>";
|
|
for (const event of news.slice(0, 15)) {
|
|
const div = document.createElement("div");
|
|
div.className = "event";
|
|
div.innerHTML = `<div><span class="state">${event.state}</span> — <strong>${event.name}</strong></div><div class="time">${event.time}</div>`;
|
|
feed.appendChild(div);
|
|
}
|
|
}
|
|
|
|
async function loadResumeSetting() {
|
|
const res = await fetch("/api/auto_resume");
|
|
const json = await res.json();
|
|
document.getElementById("resume-toggle").checked = json.enabled;
|
|
}
|
|
|
|
async function setResumeSetting(value) {
|
|
await fetch("/api/auto_resume", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ enabled: value }),
|
|
});
|
|
}
|
|
|
|
document
|
|
.getElementById("resume-toggle")
|
|
.addEventListener("change", (e) => {
|
|
setResumeSetting(e.target.checked);
|
|
});
|
|
|
|
async function refresh() {
|
|
await Promise.all([loadStats(), loadBandwidth(), loadNews()]);
|
|
}
|
|
|
|
setInterval(refresh, 1000);
|
|
loadResumeSetting();
|
|
refresh();
|
|
</script>
|
|
</body>
|
|
</html>
|