docker.videokonverter/video-konverter/app/templates/tv/series.html
data 61ca20bf8b fix: TV-App UX-Verbesserungen - Navigation, Ordner-Ansicht, Duplikate
- FocusManager: SELECT-Elemente, sequentielle Nav-Navigation, Zone-basiert
- Ordner-Ansicht (4. View) fuer Serien + Filme mit Quellen-Gruppierung
- Login-Flow: Lade-Spinner statt Form-Flash, Auto-Login bei 1 Profil
- Farbauswahl: Farbkreise statt input type=color (Samsung TV kompatibel)
- Duplikat-Episoden: Orange Markierung + Badge bei gleicher Episodennummer
- i18n: Neue Keys fuer Ordner-Ansicht und Duplikat-Markierung

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 07:57:57 +01:00

211 lines
11 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>
<button class="tv-view-btn {% if view == 'folder' %}active{% endif %}"
data-focusable data-view="folder" onclick="switchView('folder')"
title="{{ t('settings.view_folder') }}">
<svg width="18" height="18" viewBox="0 0 18 18"><path d="M2 4h5l2 2h7v8a1 1 0 01-1 1H2a1 1 0 01-1-1V5a1 1 0 011-1z" fill="currentColor" opacity="0.7"/></svg>
</button>
</div>
</div>
<!-- Quellen-Tabs (immer sichtbar) -->
{% 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 (nicht in Ordner-Ansicht) -->
<div class="tv-filter-bar" id="filter-bar" {% if view == 'folder' %}style="display:none"{% endif %}>
{% 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) %}&#9733;{% endfor %}{% for s in range(5 - n) %}&#9734;{% 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 %}">&#9733;</span>{% endfor %}</span> {% endif %}
{{ s.episode_count or 0 }} {{ t('series.episodes') }}{% if s.genres %} &middot; {{ 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 %}">&#9733;</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 %}">&#9733;</span>{% endfor %} {{ s.avg_rating }}</span> &middot; {% endif %}
{{ s.episode_count or 0 }} {{ t('series.episodes') }}
{% if s.genres %} &middot; {{ s.genres }}{% endif %}
{% if s.status %} &middot; {{ s.status }}{% endif %}
</span>
</div>
</a>
{% endfor %}
</div>
<!-- === Ordner-Ansicht === -->
<div class="tv-folder-view tv-view-folder" id="view-folder" {% if view != 'folder' %}style="display:none"{% endif %}>
{% for src in folder_data %}
<div class="tv-folder-source">
{% if folder_data|length > 1 %}
<h3 class="tv-folder-source-title">{{ src.name }}</h3>
{% endif %}
<div class="tv-folder-list">
{% for s in src.items %}
<a href="/tv/series/{{ s.id }}" class="tv-folder-item" data-focusable>
<span class="tv-folder-icon">&#128193;</span>
<span class="tv-folder-name">{{ s.folder_name }}</span>
<span class="tv-folder-meta">
{% if s.title and s.title != s.folder_name %}{{ s.title }} &middot; {% endif %}
{{ s.episode_count or 0 }} {{ t('series.episodes') }}
</span>
</a>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
{% if not series and view != 'folder' %}
<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);
});
// Filter-Leiste in Ordner-Ansicht verstecken
const filterBar = document.getElementById('filter-bar');
if (filterBar) filterBar.style.display = mode === 'folder' ? 'none' : '';
fetch('/tv/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest' },
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 %}