usermanagement/translation/calendar
This commit is contained in:
@@ -7,28 +7,28 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="setup-container">
|
||||
<h1>Willkommen bei Subscribarr</h1>
|
||||
<p class="setup-intro">Lass uns deine Installation einrichten. Du brauchst mindestens einen Jellyfin-Server.</p>
|
||||
<h1>Welcome to Subscribarr</h1>
|
||||
<p class="setup-intro">Let's set up your installation. You need at least one Jellyfin server.</p>
|
||||
|
||||
<form method="post" class="setup-form">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="setup-section">
|
||||
<h2>Jellyfin Server (Erforderlich)</h2>
|
||||
<h2>Jellyfin server (required)</h2>
|
||||
<div class="form-group">
|
||||
<label>Server URL</label>
|
||||
{{ form.jellyfin_server_url }}
|
||||
<div class="help">z.B. http://192.168.1.100:8096 oder http://jellyfin.local:8096</div>
|
||||
<div class="help">e.g., http://192.168.1.100:8096 or http://jellyfin.local:8096</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>API Key</label>
|
||||
{{ form.jellyfin_api_key }}
|
||||
<div class="help">Admin API Key aus den Jellyfin-Einstellungen</div>
|
||||
<div class="help">Admin API key from Jellyfin settings</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setup-section">
|
||||
<h2>Sonarr (Optional)</h2>
|
||||
<h2>Sonarr (optional)</h2>
|
||||
<div class="form-group">
|
||||
<label>Server URL</label>
|
||||
{{ form.sonarr_url }}
|
||||
@@ -40,7 +40,7 @@
|
||||
</div>
|
||||
|
||||
<div class="setup-section">
|
||||
<h2>Radarr (Optional)</h2>
|
||||
<h2>Radarr (optional)</h2>
|
||||
<div class="form-group">
|
||||
<label>Server URL</label>
|
||||
{{ form.radarr_url }}
|
||||
@@ -51,7 +51,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="setup-submit">Installation abschließen</button>
|
||||
<button type="submit" class="setup-submit">Finish setup</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
@@ -1,19 +1,22 @@
|
||||
{% load static %}
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Einstellungen – Subscribarr</title>
|
||||
<title>Settings – Subscribarr</title>
|
||||
<link rel="stylesheet" href="{% static 'css/settings.css' %}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<div class="topbar">
|
||||
<div><a href="/" class="btn">← Zurück</a></div>
|
||||
<div><strong>Einstellungen</strong></div>
|
||||
<div style="display:flex; gap:8px; align-items:center;">
|
||||
<a href="/" class="btn">← Back</a>
|
||||
<a href="{% url 'settingspanel:subscriptions' %}" class="btn">👥 Subscriptions</a>
|
||||
</div>
|
||||
<div><strong>Settings</strong></div>
|
||||
<div></div>
|
||||
</div>
|
||||
|
||||
@@ -29,12 +32,12 @@
|
||||
<div class="row">
|
||||
<label>Jellyfin Server URL</label>
|
||||
{{ jellyfin_form.jellyfin_server_url }}
|
||||
<div class="help">z.B. http://localhost:8096</div>
|
||||
<div class="help">e.g., http://localhost:8096</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Jellyfin API Key</label>
|
||||
{{ jellyfin_form.jellyfin_api_key }}
|
||||
<div class="help">Admin API Key aus den Jellyfin Einstellungen</div>
|
||||
<div class="help">Admin API key from your Jellyfin settings</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -46,8 +49,7 @@
|
||||
<div class="inline">
|
||||
<div class="field">{{ arr_form.sonarr_url }}</div>
|
||||
<div class="inline-actions">
|
||||
<button class="btn" type="button" onclick="testConnection('sonarr', this)">Test
|
||||
Sonarr</button>
|
||||
<button class="btn" type="button" onclick="testConnection('sonarr', this)">Test Sonarr</button>
|
||||
<span id="sonarrStatus" class="badge muted">—</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -64,8 +66,7 @@
|
||||
<div class="inline">
|
||||
<div class="field">{{ arr_form.radarr_url }}</div>
|
||||
<div class="inline-actions">
|
||||
<button class="btn" type="button" onclick="testConnection('radarr', this)">Test
|
||||
Radarr</button>
|
||||
<button class="btn" type="button" onclick="testConnection('radarr', this)">Test Radarr</button>
|
||||
<span id="radarrStatus" class="badge muted">—</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -76,32 +77,31 @@
|
||||
{{ arr_form.radarr_api_key }}
|
||||
</div>
|
||||
|
||||
<div class="help">Klicke „Test …“, um die Verbindung gegen <code>/api/v3/system/status</code> zu
|
||||
prüfen.</div>
|
||||
<div class="help">Click “Test …” to verify against <code>/api/v3/system/status</code>.</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Mailserver</h2>
|
||||
<h2>Mail server</h2>
|
||||
<div class="row"><label>Host</label>{{ mail_form.mail_host }}</div>
|
||||
<div class="row"><label>Port</label>{{ mail_form.mail_port }}</div>
|
||||
<div class="row"><label>Sicherheit</label>{{ mail_form.mail_secure }}</div>
|
||||
<div class="row"><label>Benutzer</label>{{ mail_form.mail_user }}</div>
|
||||
<div class="row"><label>Passwort</label>{{ mail_form.mail_password }}</div>
|
||||
<div class="row"><label>Absender</label>{{ mail_form.mail_from }}</div>
|
||||
<div class="row"><label>Security</label>{{ mail_form.mail_secure }}</div>
|
||||
<div class="row"><label>User</label>{{ mail_form.mail_user }}</div>
|
||||
<div class="row"><label>Password</label>{{ mail_form.mail_password }}</div>
|
||||
<div class="row"><label>From</label>{{ mail_form.mail_from }}</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Konto</h2>
|
||||
<div class="row"><label>Benutzername</label>{{ account_form.username }}</div>
|
||||
<div class="row"><label>E-Mail</label>{{ account_form.email }}</div>
|
||||
<div class="row"><label>Neues Passwort</label>{{ account_form.new_password }}</div>
|
||||
<div class="row"><label>Passwort wiederholen</label>{{ account_form.repeat_password }}</div>
|
||||
<div class="help">Nur Oberfläche – Umsetzung Passwortänderung später.</div>
|
||||
<h2>Account</h2>
|
||||
<div class="row"><label>Username</label>{{ account_form.username }}</div>
|
||||
<div class="row"><label>Email</label>{{ account_form.email }}</div>
|
||||
<div class="row"><label>New password</label>{{ account_form.new_password }}</div>
|
||||
<div class="row"><label>Repeat password</label>{{ account_form.repeat_password }}</div>
|
||||
<div class="help">Only UI – implementing password change later.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:16px">
|
||||
<button class="btn btn-primary" type="submit">Speichern</button>
|
||||
<button class="btn btn-primary" type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -116,25 +116,25 @@
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.ok) {
|
||||
alert(kind + " Verbindung erfolgreich!");
|
||||
alert(kind + " connection successful!");
|
||||
} else {
|
||||
alert(kind + " Fehler: " + data.error);
|
||||
alert(kind + " error: " + data.error);
|
||||
}
|
||||
})
|
||||
.catch(err => alert(kind + " Fehler: " + err));
|
||||
.catch(err => alert(kind + " error: " + err));
|
||||
}
|
||||
|
||||
function setBadge(kind, state, text, tooltip) {
|
||||
const el = document.getElementById(kind + "Status");
|
||||
if (!el) return;
|
||||
el.classList.remove("ok", "err", "muted");
|
||||
el.title = tooltip || ""; // voller Fehlertext im Tooltip
|
||||
el.title = tooltip || ""; // full error text in tooltip
|
||||
if (state === "ok") {
|
||||
el.classList.add("ok");
|
||||
el.textContent = "Verbunden";
|
||||
el.textContent = "Connected";
|
||||
} else if (state === "err") {
|
||||
el.classList.add("err");
|
||||
el.textContent = "Fehler";
|
||||
el.textContent = "Error";
|
||||
} else {
|
||||
el.classList.add("muted");
|
||||
el.textContent = "—";
|
||||
@@ -147,19 +147,19 @@
|
||||
const url = urlEl ? urlEl.value.trim() : "";
|
||||
const key = keyEl ? keyEl.value.trim() : "";
|
||||
|
||||
setBadge(kind, "muted", "Teste…");
|
||||
setBadge(kind, "muted", "Testing…");
|
||||
if (btnEl) { btnEl.disabled = true; }
|
||||
|
||||
fetch(`/settings/test-connection/?kind=${encodeURIComponent(kind)}&url=${encodeURIComponent(url)}&key=${encodeURIComponent(key)}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.ok) {
|
||||
setBadge(kind, "ok", "Verbunden", "");
|
||||
setBadge(kind, "ok", "Connected", "");
|
||||
} else {
|
||||
setBadge(kind, "err", "Fehler", data.error || "Unbekannter Fehler");
|
||||
setBadge(kind, "err", "Error", data.error || "Unknown error");
|
||||
}
|
||||
})
|
||||
.catch(err => setBadge(kind, "err", "Fehler", String(err)))
|
||||
.catch(err => setBadge(kind, "err", "Error", String(err)))
|
||||
.finally(() => { if (btnEl) btnEl.disabled = false; });
|
||||
}
|
||||
|
||||
|
218
settingspanel/templates/settingspanel/subscriptions.html
Normal file
218
settingspanel/templates/settingspanel/subscriptions.html
Normal file
@@ -0,0 +1,218 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Subscriptions – Admin{% endblock %}
|
||||
{% block extra_style %}
|
||||
<style>
|
||||
.wrap { max-width: 1200px; margin: 0 auto; padding: 16px; }
|
||||
.filters { display:flex; gap:8px; margin: 10px 0 16px; }
|
||||
.filters input { padding: 10px 12px; border-radius: 10px; border: 1px solid #2a2a34; background:#111119; color:#e6e6e6; min-width: 240px; }
|
||||
.section { margin-top: 20px; }
|
||||
.grid { display:grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; }
|
||||
.card { background:#12121a; border:1px solid #1f2030; border-radius:12px; padding:12px; display:flex; gap:12px; }
|
||||
.poster { width: 90px; height: 135px; border-radius:8px; overflow:hidden; background:#222233; flex:0 0 auto; }
|
||||
.poster img { width:100%; height:100%; object-fit: cover; display:block; }
|
||||
.meta { flex:1 1 auto; min-width:0; }
|
||||
.title { font-weight:600; margin-bottom:4px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
||||
.muted { color:#9aa0b4; font-size:.9rem; }
|
||||
.user { font-weight:600; }
|
||||
|
||||
/* user list */
|
||||
.users { display:grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap:10px; }
|
||||
.user-item { background:#10121b; border:1px solid #1f2030; border-radius:10px; padding:10px 12px; display:flex; justify-content:space-between; align-items:center; cursor:pointer; transition: transform .08s ease, border-color .12s ease; }
|
||||
.user-item:hover { transform: translateY(-1px); border-color:#2a2b44; }
|
||||
.badge { background:#171a26; border:1px solid #2a2b44; color:#cfd3ea; border-radius:999px; padding:2px 8px; font-size:.85rem; }
|
||||
|
||||
/* modal fix */
|
||||
.modal-backdrop { position:fixed; inset:0; display:none; align-items:center; justify-content:center; background: rgba(10,12,20,.55); backdrop-filter: blur(4px); z-index: 1000; }
|
||||
.modal { width:min(720px, 100%); max-height:92vh; overflow:auto; background: linear-gradient(180deg, #10121b 0%, #0d0f16 100%); border:1px solid #2a2b44; border-radius: 14px; }
|
||||
.modal-header { display:flex; align-items:center; gap:10px; padding:12px 14px; border-bottom:1px solid #20223a; background: rgba(13,15,22,.85); position:sticky; top:0; }
|
||||
.modal-close { margin-left:auto; background:#1a1f33; color:#c9cbe3; border:1px solid #2a2b44; width:34px; height:34px; border-radius:10px; cursor:pointer; }
|
||||
.section-block { background:#101327; border:1px solid #20223a; border-radius:12px; padding:14px; margin:12px; }
|
||||
.section-title { font-weight:650; margin-bottom:8px; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="wrap">
|
||||
<h1>Subscriptions overview</h1>
|
||||
<div class="filters">
|
||||
<input type="text" id="q" placeholder="Search user/series/movies…">
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Users</h2>
|
||||
<p class="muted">Tap a user to view all subscriptions.</p>
|
||||
<div class="users" id="usersList">
|
||||
{% for u in user_stats %}
|
||||
<div class="user-item" data-user="{{ u.username_lower }}">
|
||||
<div>{{ u.username }}</div>
|
||||
<div class="badge">{{ u.total_count }}</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<p class="muted">No subscriptions yet.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Series</h2>
|
||||
<div class="grid" id="seriesGrid">
|
||||
{% for s in series %}
|
||||
<div class="card" data-user="{{ s.user.username|lower }}" data-title="{{ s.series_title|lower }}" data-id="{{ s.series_id }}">
|
||||
<div class="poster">
|
||||
{% if s.series_poster %}
|
||||
<img src="{{ s.series_poster }}" alt="{{ s.series_title }}">
|
||||
{% else %}
|
||||
<img src="https://via.placeholder.com/90x135?text=No+Poster" alt="">
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="meta">
|
||||
<div class="title">{{ s.series_title }}</div>
|
||||
<div class="muted">User: <span class="user">{{ s.user.username }}</span></div>
|
||||
<div class="muted">SeriesId: {{ s.series_id }}</div>
|
||||
<div class="muted">Since: {{ s.created_at }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<p class="muted">No series subscriptions.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Movies</h2>
|
||||
<div class="grid" id="moviesGrid">
|
||||
{% for m in movies %}
|
||||
<div class="card" data-user="{{ m.user.username|lower }}" data-title="{{ m.title|lower }}" data-id="{{ m.movie_id }}">
|
||||
<div class="poster">
|
||||
{% if m.poster %}
|
||||
<img src="{{ m.poster }}" alt="{{ m.title }}">
|
||||
{% else %}
|
||||
<img src="https://via.placeholder.com/90x135?text=No+Poster" alt="">
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="meta">
|
||||
<div class="title">{{ m.title }}</div>
|
||||
<div class="muted">User: <span class="user">{{ m.user.username }}</span></div>
|
||||
<div class="muted">MovieId: {{ m.movie_id }}</div>
|
||||
<div class="muted">Since: {{ m.created_at }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<p class="muted">No movie subscriptions.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function(){
|
||||
const q = document.getElementById('q');
|
||||
function filter(){
|
||||
const v = (q.value||'').toLowerCase();
|
||||
[document.getElementById('seriesGrid'), document.getElementById('moviesGrid')].forEach(grid=>{
|
||||
if(!grid) return; Array.from(grid.children).forEach(card=>{
|
||||
const text = (card.dataset.user + ' ' + card.dataset.title).toLowerCase();
|
||||
card.style.display = text.includes(v) ? '' : 'none';
|
||||
});
|
||||
});
|
||||
const users = document.getElementById('usersList');
|
||||
if(users){ Array.from(users.children).forEach(item=>{
|
||||
const username = item.querySelector('div')?.textContent?.toLowerCase() || '';
|
||||
item.style.display = username.includes(v) ? '' : 'none';
|
||||
}); }
|
||||
}
|
||||
q.addEventListener('input', filter);
|
||||
|
||||
// Group-by-user popup
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'modal-backdrop';
|
||||
modal.style.display = 'none';
|
||||
modal.innerHTML = `
|
||||
<div class="modal" style="max-width:720px">
|
||||
<div class="modal-header">
|
||||
<div style="font-weight:700" id="uTitle"></div>
|
||||
<button class="modal-close" aria-label="Close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="section-block"><div class="section-title">Series</div><div id="uSeries"></div></div>
|
||||
<div class="section-block"><div class="section-title">Movies</div><div id="uMovies"></div></div>
|
||||
</div>
|
||||
</div>`;
|
||||
document.body.appendChild(modal);
|
||||
const closeBtn = modal.querySelector('.modal-close');
|
||||
const uTitle = modal.querySelector('#uTitle');
|
||||
const uSeries = modal.querySelector('#uSeries');
|
||||
const uMovies = modal.querySelector('#uMovies');
|
||||
function open(){ modal.style.display='flex'; document.body.style.overflow='hidden'; }
|
||||
function close(){ modal.style.display='none'; document.body.style.overflow=''; }
|
||||
closeBtn.addEventListener('click', close);
|
||||
modal.addEventListener('click', e=>{ if(e.target===modal) close(); });
|
||||
|
||||
function renderList(container, items){
|
||||
container.innerHTML = '';
|
||||
if(!items.length){ container.innerHTML = '<div class="muted">—</div>'; return; }
|
||||
items.forEach(it=>{
|
||||
const row = document.createElement('div');
|
||||
row.style.display = 'flex'; row.style.gap='10px'; row.style.alignItems='center'; row.style.margin='6px 0';
|
||||
const poster = document.createElement('img');
|
||||
poster.src = it.poster || 'https://via.placeholder.com/50x75?text=No+Poster';
|
||||
poster.style.width='50px'; poster.style.height='75px'; poster.style.objectFit='cover'; poster.style.borderRadius='6px';
|
||||
const meta = document.createElement('div');
|
||||
meta.innerHTML = `<div style="font-weight:600">${it.title}</div><div class="muted">ID: ${it.id}</div>`;
|
||||
row.appendChild(poster); row.appendChild(meta);
|
||||
container.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
function gatherForUser(username){
|
||||
const series = Array.from(document.querySelectorAll('#seriesGrid .card')).filter(c=>c.dataset.user===username).map(c=>{
|
||||
const idFromAttr = c.dataset.id || '';
|
||||
let idFromText = '';
|
||||
if(!idFromAttr){
|
||||
const text = Array.from(c.querySelectorAll('.muted')).map(el=>el.textContent).join(' ');
|
||||
const m = text.match(/SeriesId:\s*(\d+)/i); idFromText = m ? m[1] : '';
|
||||
}
|
||||
return {
|
||||
title: c.querySelector('.title')?.textContent || '',
|
||||
id: idFromAttr || idFromText,
|
||||
poster: c.querySelector('img')?.src || ''
|
||||
};
|
||||
});
|
||||
const movies = Array.from(document.querySelectorAll('#moviesGrid .card')).filter(c=>c.dataset.user===username).map(c=>{
|
||||
const idFromAttr = c.dataset.id || '';
|
||||
let idFromText = '';
|
||||
if(!idFromAttr){
|
||||
const text = Array.from(c.querySelectorAll('.muted')).map(el=>el.textContent).join(' ');
|
||||
const m = text.match(/MovieId:\s*(\d+)/i); idFromText = m ? m[1] : '';
|
||||
}
|
||||
return {
|
||||
title: c.querySelector('.title')?.textContent || '',
|
||||
id: idFromAttr || idFromText,
|
||||
poster: c.querySelector('img')?.src || ''
|
||||
};
|
||||
});
|
||||
return {series, movies};
|
||||
}
|
||||
|
||||
function openForUser(usernameDisplay){
|
||||
const username = usernameDisplay.trim().toLowerCase();
|
||||
const {series, movies} = gatherForUser(username);
|
||||
uTitle.textContent = `Subscriptions for ${usernameDisplay.trim()}`;
|
||||
renderList(uSeries, series);
|
||||
renderList(uMovies, movies);
|
||||
open();
|
||||
}
|
||||
|
||||
document.querySelectorAll('.user').forEach(uEl=>{
|
||||
uEl.style.cursor='pointer';
|
||||
uEl.title='Show all subscriptions for this user';
|
||||
uEl.addEventListener('click', ()=> openForUser(uEl.textContent));
|
||||
});
|
||||
|
||||
document.querySelectorAll('.user-item').forEach(item=>{
|
||||
item.addEventListener('click', ()=>{
|
||||
const name = item.querySelector('div')?.textContent || '';
|
||||
openForUser(name);
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
Reference in New Issue
Block a user