docker.videokonverter/video-konverter/app/templates/tv/movies.html
data 6d0b8936c5 feat: VideoKonverter v4.0 - Streaming-Client Ausbau
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>
2026-03-01 07:39:12 +01:00

178 lines
8.8 KiB
HTML

{% extends "tv/base.html" %}
{% block title %}{{ t('movies.title') }} - VideoKonverter TV{% endblock %}
{% block content %}
<section class="tv-section">
<div class="tv-list-header">
<h1 class="tv-page-title">{{ t('movies.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/movies?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/movies?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/movies?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/movies?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="year" {% if current_sort == 'year' %}selected{% endif %}>{{ t('filter.sort_newest') }} (Jahr)</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 m in movies %}
<a href="/tv/movies/{{ m.id }}" class="tv-card" data-focusable>
{% if m.poster_url %}
<img src="{{ m.poster_url }}" alt="" class="tv-card-img" loading="lazy">
{% else %}
<div class="tv-card-placeholder">{{ m.title or m.folder_name }}</div>
{% endif %}
<div class="tv-card-info">
<span class="tv-card-title">{{ m.title or m.folder_name }}</span>
<span class="tv-card-meta">
{% if m.avg_rating > 0 %}<span class="tv-card-stars">{% for i in range(1, 6) %}<span class="tv-star-sm {% if i <= m.avg_rating|round|int %}active{% endif %}">&#9733;</span>{% endfor %}</span> {% endif %}
{{ m.year or "" }}{% if m.genres %} &middot; {{ m.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 m in movies %}
<a href="/tv/movies/{{ m.id }}" class="tv-list-item" data-focusable>
<div class="tv-list-poster">
{% if m.poster_url %}
<img src="{{ m.poster_url }}" alt="" loading="lazy">
{% endif %}
</div>
<span class="tv-list-title">{{ m.title or m.folder_name }}</span>
<span class="tv-list-rating">{% if m.avg_rating > 0 %}{% for i in range(1, 6) %}<span class="tv-star-sm {% if i <= m.avg_rating|round|int %}active{% endif %}">&#9733;</span>{% endfor %}{% endif %}</span>
<span class="tv-list-genre">{{ m.genres or '' }}</span>
<span class="tv-list-count">{{ m.year or '' }}</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 m in movies %}
<a href="/tv/movies/{{ m.id }}" class="tv-detail-item" data-focusable>
<div class="tv-detail-thumb">
{% if m.poster_url %}
<img src="{{ m.poster_url }}" alt="" loading="lazy">
{% endif %}
</div>
<div class="tv-detail-content">
<span class="tv-detail-title">{{ m.title or m.folder_name }}</span>
{% if m.overview %}
<p class="tv-detail-desc">{{ m.overview }}</p>
{% endif %}
<span class="tv-detail-meta">
{% if m.avg_rating > 0 %}<span class="tv-card-stars">{% for i in range(1, 6) %}<span class="tv-star-sm {% if i <= m.avg_rating|round|int %}active{% endif %}">&#9733;</span>{% endfor %} {{ m.avg_rating }}</span> &middot; {% endif %}
{% if m.year %}{{ m.year }}{% endif %}
{% if m.genres %} &middot; {{ m.genres }}{% endif %}
</span>
</div>
</a>
{% endfor %}
</div>
{% if not movies %}
<div class="tv-empty">{{ t('movies.no_movies') }}</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: 'movies_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 %}