Files
BitManager/templates/home.html
OusmBlueNinja 1966af1ae3 Adds update notification feature
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.
2025-06-03 23:19:44 -05:00

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>