255 lines
13 KiB
HTML
255 lines
13 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 %}
|
|
<select class="tv-sort-select tv-genre-filter" data-focusable onchange="applyGenre(this.value)">
|
|
<option value="">{{ t('filter.all_genres') }}</option>
|
|
{% for g in genres %}
|
|
<option value="{{ g }}" {% if current_genre == g %}selected{% endif %}>{{ g }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
{% 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 data-letter="{{ (s.title or s.folder_name)[:1]|upper }}">
|
|
{% 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 data-letter="{{ (s.title or s.folder_name)[:1]|upper }}">
|
|
<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 data-letter="{{ (s.title or s.folder_name)[:1]|upper }}">
|
|
<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>
|
|
|
|
<!-- === 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.entries %}
|
|
<a href="/tv/series/{{ s.id }}" class="tv-folder-item" data-focusable>
|
|
<span class="tv-folder-icon">📁</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 }} · {% endif %}
|
|
{{ s.episode_count or 0 }} {{ t('series.episodes') }}
|
|
</span>
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<!-- Alphabet-Seitenleiste -->
|
|
<nav class="tv-alpha-sidebar" id="alpha-sidebar" {% if view == 'folder' %}style="display:none"{% endif %}>
|
|
{% for letter in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' %}
|
|
<span class="tv-alpha-letter" data-letter="{{ letter }}" onclick="filterByLetter('{{ letter }}')" data-focusable>{{ letter }}</span>
|
|
{% endfor %}
|
|
<span class="tv-alpha-letter" data-letter="#" onclick="filterByLetter('#')" data-focusable>#</span>
|
|
</nav>
|
|
|
|
{% 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 und Alphabet in Ordner-Ansicht verstecken
|
|
const filterBar = document.getElementById('filter-bar');
|
|
if (filterBar) filterBar.style.display = mode === 'folder' ? 'none' : '';
|
|
var alphaSidebar = document.getElementById('alpha-sidebar');
|
|
if (alphaSidebar) alphaSidebar.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 applyGenre(genre) {
|
|
const url = new URL(window.location);
|
|
if (genre) {
|
|
url.searchParams.set('genre', genre);
|
|
} else {
|
|
url.searchParams.delete('genre');
|
|
}
|
|
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();
|
|
}
|
|
|
|
// Alphabet-Filter
|
|
var _currentLetter = null;
|
|
function filterByLetter(letter) {
|
|
_currentLetter = (_currentLetter === letter) ? null : letter;
|
|
['grid', 'list', 'detail'].forEach(function(v) {
|
|
var c = document.getElementById('view-' + v);
|
|
if (!c) return;
|
|
c.querySelectorAll('[data-letter]').forEach(function(item) {
|
|
if (!_currentLetter) { item.style.display = ''; return; }
|
|
var raw = item.dataset.letter;
|
|
var norm = /^[A-Z]$/.test(raw) ? raw : '#';
|
|
item.style.display = (norm === _currentLetter) ? '' : 'none';
|
|
});
|
|
});
|
|
document.querySelectorAll('.tv-alpha-letter').forEach(function(el) {
|
|
el.classList.toggle('active', el.dataset.letter === _currentLetter);
|
|
});
|
|
}
|
|
// Buchstaben ohne Treffer abdunkeln
|
|
(function() {
|
|
var avail = {};
|
|
document.querySelectorAll('.tv-view-grid [data-letter], .tv-view-list [data-letter], .tv-view-detail [data-letter]').forEach(function(item) {
|
|
var raw = item.dataset.letter;
|
|
avail[/^[A-Z]$/.test(raw) ? raw : '#'] = true;
|
|
});
|
|
document.querySelectorAll('.tv-alpha-letter').forEach(function(el) {
|
|
if (!avail[el.dataset.letter]) el.remove();
|
|
});
|
|
})();
|
|
</script>
|
|
{% endblock %}
|