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.
This commit is contained in:
40
main.py
40
main.py
@@ -6,6 +6,7 @@ import threading
|
||||
import time
|
||||
import datetime
|
||||
from collections import Counter
|
||||
import requests
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = 'change_this_secret'
|
||||
@@ -37,14 +38,17 @@ qb = None
|
||||
torrent_data, torrent_news, torrent_stats, bandwidth_history = [], [], {}, []
|
||||
auto_resume_enabled = True
|
||||
data_lock = threading.Lock()
|
||||
local_version = "1.0.1"
|
||||
remote_version = ""
|
||||
|
||||
import requests
|
||||
def check_outdated():
|
||||
global remote_version
|
||||
|
||||
url = "https://dock-it.dev/GigabiteStudios/BitManager/raw/branch/main/version.txt"
|
||||
|
||||
def check_outdated(local_version, url):
|
||||
try:
|
||||
response = requests.get(url, timeout=5)
|
||||
response.raise_for_status()
|
||||
|
||||
|
||||
remote_version = response.text.strip()
|
||||
|
||||
if local_version != remote_version:
|
||||
@@ -55,10 +59,10 @@ def check_outdated(local_version, url):
|
||||
return False
|
||||
|
||||
except requests.RequestException as e:
|
||||
print(f"❌ Failed to fetch version file: {e}")
|
||||
return False
|
||||
|
||||
outdated = check_outdated("1.0.1", "https://dock-it.dev/GigabiteStudios/BitManager/raw/branch/main/version.txt")
|
||||
|
||||
outdated = check_outdated()
|
||||
|
||||
|
||||
# qBittorrent polling
|
||||
@@ -169,7 +173,14 @@ def setup():
|
||||
db.session.add(config)
|
||||
db.session.commit()
|
||||
return redirect(url_for('register'))
|
||||
return render_template('setup.html')
|
||||
|
||||
return render_template(
|
||||
'setup.html',
|
||||
outdated=outdated,
|
||||
current_version=local_version,
|
||||
latest_version=remote_version
|
||||
)
|
||||
|
||||
|
||||
@app.route('/test-connection', methods=['POST'])
|
||||
def test_connection():
|
||||
@@ -192,8 +203,7 @@ def login():
|
||||
user = User.query.filter_by(username=request.form['username']).first()
|
||||
if user and bcrypt.check_password_hash(user.password, request.form['password']):
|
||||
session['user'] = user.username
|
||||
if connect_to_qb():
|
||||
threading.Thread(target=poll_torrents, daemon=True).start()
|
||||
|
||||
return redirect(url_for('home'))
|
||||
return render_template("login.html", error="Invalid credentials")
|
||||
return render_template("login.html")
|
||||
@@ -218,8 +228,7 @@ def register():
|
||||
db.session.add(new_user)
|
||||
db.session.commit()
|
||||
session['user'] = new_user.username
|
||||
if connect_to_qb():
|
||||
threading.Thread(target=poll_torrents, daemon=True).start()
|
||||
|
||||
return redirect(url_for('home'))
|
||||
|
||||
return render_template("register.html")
|
||||
@@ -227,7 +236,12 @@ def register():
|
||||
# Views
|
||||
@app.route('/')
|
||||
def home():
|
||||
return render_template("home.html", outdated = outdated)
|
||||
return render_template(
|
||||
'home.html',
|
||||
outdated=outdated,
|
||||
current_version=local_version,
|
||||
latest_version=remote_version
|
||||
)
|
||||
|
||||
@app.route('/dashboard')
|
||||
def dashboard():
|
||||
@@ -325,4 +339,6 @@ def api_auto_resume():
|
||||
if __name__ == '__main__':
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
if connect_to_qb():
|
||||
threading.Thread(target=poll_torrents, daemon=True).start()
|
||||
app.run(debug=True, host='0.0.0.0', port=5000)
|
||||
|
||||
40
setup.bat
Normal file
40
setup.bat
Normal file
@@ -0,0 +1,40 @@
|
||||
@echo off
|
||||
setlocal
|
||||
|
||||
echo Checking for Python...
|
||||
|
||||
python --version >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo.
|
||||
echo [ERROR] Python is not installed or not in PATH.
|
||||
echo Please install Python 3.8 or later and try again.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo Python is installed.
|
||||
echo.
|
||||
|
||||
echo Ensuring pip is available...
|
||||
python -m ensurepip --upgrade >nul 2>&1
|
||||
|
||||
echo Upgrading pip...
|
||||
python -m pip install --upgrade pip
|
||||
|
||||
echo Installing requirements...
|
||||
if exist requirements.txt (
|
||||
python -m pip install -r requirements.txt
|
||||
) else (
|
||||
echo [ERROR] requirements.txt not found in the current directory.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo ✅ Requirements installed successfully.
|
||||
|
||||
echo.
|
||||
echo ▶️ Starting Flask app...
|
||||
python app.py
|
||||
|
||||
endlocal
|
||||
@@ -1,268 +1,310 @@
|
||||
<!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);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<a href="/">Home</a>
|
||||
<a href="/dashboard">Dashboard</a>
|
||||
<a href="/news">News</a>
|
||||
<a href="/settings">Settings</a>
|
||||
{% if outdated %}
|
||||
<a>⚠️ You're running an outdated version!</div>
|
||||
{% endif %}
|
||||
|
||||
</nav>
|
||||
|
||||
<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);
|
||||
<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;
|
||||
}
|
||||
|
||||
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();
|
||||
nav {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
nav a {
|
||||
color: #80dfff;
|
||||
margin-right: 1.5rem;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
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' } }
|
||||
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 }],
|
||||
},
|
||||
plugins: { legend: { labels: { color: '#eee' } } }
|
||||
}
|
||||
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 }),
|
||||
});
|
||||
} 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);
|
||||
document
|
||||
.getElementById("resume-toggle")
|
||||
.addEventListener("change", (e) => {
|
||||
setResumeSetting(e.target.checked);
|
||||
});
|
||||
|
||||
async function refresh() {
|
||||
await Promise.all([loadStats(), loadBandwidth(), loadNews()]);
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
setInterval(refresh, 1000);
|
||||
loadResumeSetting();
|
||||
refresh();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -126,11 +126,30 @@
|
||||
color: #ccc;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.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>
|
||||
<div class="card">
|
||||
<h2>qBittorrent Setup</h2>
|
||||
{% 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 %}
|
||||
<form method="POST">
|
||||
<label>Web UI URL:</label>
|
||||
<input name="url" required value="http://192.168.0.236:8080/">
|
||||
@@ -152,9 +171,12 @@
|
||||
<span id="test-message"></span>
|
||||
<div class="info" id="test-info"></div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
function testConnection() {
|
||||
const url = document.querySelector('input[name="url"]').value;
|
||||
const username = document.querySelector('input[name="username"]').value;
|
||||
@@ -165,6 +187,9 @@
|
||||
const info = document.getElementById('test-info');
|
||||
const checkmark = document.querySelector('.checkmark');
|
||||
const testBtn = document.getElementById('testBtn');
|
||||
const outdated = document.getElementById('outdated-warning');
|
||||
const currVer = document.getElementById('current-version');
|
||||
const latestVer = document.getElementById('latest-version');
|
||||
|
||||
testBtn.disabled = true;
|
||||
testBtn.innerHTML = '<span class="spinner"></span>';
|
||||
@@ -174,6 +199,7 @@
|
||||
msg.innerText = '';
|
||||
checkmark.textContent = '';
|
||||
info.innerText = '';
|
||||
outdated.style.display = 'none';
|
||||
|
||||
fetch('/test-connection', {
|
||||
method: 'POST',
|
||||
@@ -188,6 +214,7 @@
|
||||
checkmark.textContent = '✔';
|
||||
msg.innerText = 'Connected successfully!';
|
||||
info.innerHTML = `Version: <b>${data.version}</b><br>Port: <b>${new URL(url).port || '8080'}</b>`;
|
||||
|
||||
} else {
|
||||
box.className = 'result-box error';
|
||||
checkmark.textContent = '✖';
|
||||
|
||||
Reference in New Issue
Block a user