TV-App komplett ueberarbeitet: i18n (DE/EN), Multi-User Quick-Switch, 3 Themes (Dark/Medium/Light), 3 Ansichten (Grid/Liste/Detail), Filter (Quellen/Genre/Rating/Sortierung), Merkliste, 5-Sterne-Bewertung, Watch-Status, Player-Overlay (Audio/Untertitel/Qualitaet/Naechste Episode), Episoden-Thumbnails, Suchverlauf, Queue-Bugfix (delete_source). 5 neue DB-Tabellen, 10+ neue API-Endpunkte, ~3800 neue Zeilen Code. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
179 lines
9 KiB
HTML
179 lines
9 KiB
HTML
{% extends "tv/base.html" %}
|
|
{% block title %}{{ t('series.title') }} - VideoKonverter TV{% endblock %}
|
|
|
|
{% block content %}
|
|
<section class="tv-section">
|
|
<div class="tv-list-header">
|
|
<h1 class="tv-page-title">{{ t('series.title') }}</h1>
|
|
<div class="tv-view-switch" id="view-switch">
|
|
<button class="tv-view-btn {% if view == 'grid' %}active{% endif %}"
|
|
data-focusable data-view="grid" onclick="switchView('grid')"
|
|
title="{{ t('settings.view_grid') }}">
|
|
<svg width="18" height="18" viewBox="0 0 18 18"><rect x="1" y="1" width="7" height="7" rx="1"/><rect x="10" y="1" width="7" height="7" rx="1"/><rect x="1" y="10" width="7" height="7" rx="1"/><rect x="10" y="10" width="7" height="7" rx="1"/></svg>
|
|
</button>
|
|
<button class="tv-view-btn {% if view == 'list' %}active{% endif %}"
|
|
data-focusable data-view="list" onclick="switchView('list')"
|
|
title="{{ t('settings.view_list') }}">
|
|
<svg width="18" height="18" viewBox="0 0 18 18"><rect x="1" y="2" width="16" height="3" rx="1"/><rect x="1" y="7.5" width="16" height="3" rx="1"/><rect x="1" y="13" width="16" height="3" rx="1"/></svg>
|
|
</button>
|
|
<button class="tv-view-btn {% if view == 'detail' %}active{% endif %}"
|
|
data-focusable data-view="detail" onclick="switchView('detail')"
|
|
title="{{ t('settings.view_detail') }}">
|
|
<svg width="18" height="18" viewBox="0 0 18 18"><rect x="1" y="1.5" width="5" height="6" rx="1"/><rect x="8" y="2" width="9" height="2" rx="0.5"/><rect x="8" y="5" width="6" height="1.5" rx="0.5"/><rect x="1" y="10.5" width="5" height="6" rx="1"/><rect x="8" y="11" width="9" height="2" rx="0.5"/><rect x="8" y="14" width="6" height="1.5" rx="0.5"/></svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quellen-Tabs -->
|
|
{% if sources|length > 1 %}
|
|
<div class="tv-tabs tv-source-tabs">
|
|
<a href="/tv/series?sort={{ current_sort }}{% if current_genre %}&genre={{ current_genre }}{% endif %}"
|
|
class="tv-tab {% if not current_source %}active{% endif %}" data-focusable>
|
|
{{ t('filter.all') }}
|
|
</a>
|
|
{% for src in sources %}
|
|
<a href="/tv/series?source={{ src.id }}&sort={{ current_sort }}{% if current_genre %}&genre={{ current_genre }}{% endif %}"
|
|
class="tv-tab {% if current_source == src.id|string %}active{% endif %}" data-focusable>
|
|
{{ src.name }}
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Filter-Leiste -->
|
|
<div class="tv-filter-bar">
|
|
{% if genres %}
|
|
<div class="tv-genre-chips">
|
|
<a href="/tv/series?sort={{ current_sort }}{% if current_source %}&source={{ current_source }}{% endif %}"
|
|
class="tv-chip {% if not current_genre %}active{% endif %}" data-focusable>
|
|
{{ t('filter.all') }}
|
|
</a>
|
|
{% for g in genres %}
|
|
<a href="/tv/series?genre={{ g }}&sort={{ current_sort }}{% if current_source %}&source={{ current_source }}{% endif %}"
|
|
class="tv-chip {% if current_genre == g %}active{% endif %}" data-focusable>
|
|
{{ g }}
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
<!-- Rating-Filter -->
|
|
<select class="tv-sort-select tv-rating-filter" data-focusable onchange="applyRating(this.value)">
|
|
<option value="">{{ t('filter.min_rating') }}</option>
|
|
{% for n in range(1, 6) %}
|
|
<option value="{{ n }}" {% if current_rating == n|string %}selected{% endif %}>
|
|
{% for s in range(n) %}★{% endfor %}{% for s in range(5 - n) %}☆{% endfor %} {{ n }}+
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
<select class="tv-sort-select" data-focusable onchange="applySort(this.value)">
|
|
<option value="title" {% if current_sort == 'title' %}selected{% endif %}>{{ t('filter.sort_title') }}</option>
|
|
<option value="title_desc" {% if current_sort == 'title_desc' %}selected{% endif %}>{{ t('filter.sort_title_desc') }}</option>
|
|
<option value="newest" {% if current_sort == 'newest' %}selected{% endif %}>{{ t('filter.sort_newest') }}</option>
|
|
<option value="episodes" {% if current_sort == 'episodes' %}selected{% endif %}>{{ t('filter.sort_episodes') }}</option>
|
|
<option value="rating" {% if current_sort == 'rating' %}selected{% endif %}>{{ t('filter.sort_rating') }}</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- === Grid-Ansicht === -->
|
|
<div class="tv-grid tv-view-grid" id="view-grid" {% if view != 'grid' %}style="display:none"{% endif %}>
|
|
{% for s in series %}
|
|
<a href="/tv/series/{{ s.id }}" class="tv-card" data-focusable>
|
|
{% if s.poster_url %}
|
|
<img src="{{ s.poster_url }}" alt="" class="tv-card-img" loading="lazy">
|
|
{% else %}
|
|
<div class="tv-card-placeholder">{{ s.title or s.folder_name }}</div>
|
|
{% endif %}
|
|
<div class="tv-card-info">
|
|
<span class="tv-card-title">{{ s.title or s.folder_name }}</span>
|
|
<span class="tv-card-meta">
|
|
{% if s.avg_rating > 0 %}<span class="tv-card-stars">{% for i in range(1, 6) %}<span class="tv-star-sm {% if i <= s.avg_rating|round|int %}active{% endif %}">★</span>{% endfor %}</span> {% endif %}
|
|
{{ s.episode_count or 0 }} {{ t('series.episodes') }}{% if s.genres %} · {{ s.genres }}{% endif %}
|
|
</span>
|
|
</div>
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<!-- === Liste (kompakt) === -->
|
|
<div class="tv-list-compact tv-view-list" id="view-list" {% if view != 'list' %}style="display:none"{% endif %}>
|
|
{% for s in series %}
|
|
<a href="/tv/series/{{ s.id }}" class="tv-list-item" data-focusable>
|
|
<div class="tv-list-poster">
|
|
{% if s.poster_url %}
|
|
<img src="{{ s.poster_url }}" alt="" loading="lazy">
|
|
{% endif %}
|
|
</div>
|
|
<span class="tv-list-title">{{ s.title or s.folder_name }}</span>
|
|
<span class="tv-list-rating">{% if s.avg_rating > 0 %}{% for i in range(1, 6) %}<span class="tv-star-sm {% if i <= s.avg_rating|round|int %}active{% endif %}">★</span>{% endfor %}{% endif %}</span>
|
|
<span class="tv-list-genre">{{ s.genres or '' }}</span>
|
|
<span class="tv-list-count">{{ s.episode_count or 0 }} Ep.</span>
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<!-- === Detail-Liste === -->
|
|
<div class="tv-detail-list tv-view-detail" id="view-detail" {% if view != 'detail' %}style="display:none"{% endif %}>
|
|
{% for s in series %}
|
|
<a href="/tv/series/{{ s.id }}" class="tv-detail-item" data-focusable>
|
|
<div class="tv-detail-thumb">
|
|
{% if s.poster_url %}
|
|
<img src="{{ s.poster_url }}" alt="" loading="lazy">
|
|
{% endif %}
|
|
</div>
|
|
<div class="tv-detail-content">
|
|
<span class="tv-detail-title">{{ s.title or s.folder_name }}</span>
|
|
{% if s.overview %}
|
|
<p class="tv-detail-desc">{{ s.overview }}</p>
|
|
{% endif %}
|
|
<span class="tv-detail-meta">
|
|
{% if s.avg_rating > 0 %}<span class="tv-card-stars">{% for i in range(1, 6) %}<span class="tv-star-sm {% if i <= s.avg_rating|round|int %}active{% endif %}">★</span>{% endfor %} {{ s.avg_rating }}</span> · {% endif %}
|
|
{{ s.episode_count or 0 }} {{ t('series.episodes') }}
|
|
{% if s.genres %} · {{ s.genres }}{% endif %}
|
|
{% if s.status %} · {{ s.status }}{% endif %}
|
|
</span>
|
|
</div>
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
{% if not series %}
|
|
<div class="tv-empty">{{ t('series.no_series') }}</div>
|
|
{% endif %}
|
|
</section>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
function switchView(mode) {
|
|
document.querySelectorAll('[id^="view-"]').forEach(el => {
|
|
if (el.id !== 'view-switch') el.style.display = 'none';
|
|
});
|
|
const target = document.getElementById('view-' + mode);
|
|
if (target) target.style.display = '';
|
|
document.querySelectorAll('.tv-view-btn').forEach(btn => {
|
|
btn.classList.toggle('active', btn.dataset.view === mode);
|
|
});
|
|
fetch('/tv/settings', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: 'series_view=' + mode,
|
|
}).catch(() => {});
|
|
}
|
|
|
|
function applySort(sort) {
|
|
const url = new URL(window.location);
|
|
url.searchParams.set('sort', sort);
|
|
window.location.href = url.toString();
|
|
}
|
|
|
|
function applyRating(rating) {
|
|
const url = new URL(window.location);
|
|
if (rating) {
|
|
url.searchParams.set('rating', rating);
|
|
} else {
|
|
url.searchParams.delete('rating');
|
|
}
|
|
window.location.href = url.toString();
|
|
}
|
|
</script>
|
|
{% endblock %}
|