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>
133 lines
4.7 KiB
HTML
133 lines
4.7 KiB
HTML
{% extends "tv/base.html" %}
|
|
{% block title %}{{ t('search.title') }} - VideoKonverter TV{% endblock %}
|
|
|
|
{% block content %}
|
|
<section class="tv-section">
|
|
<h1 class="tv-page-title">{{ t('search.title') }}</h1>
|
|
<form action="/tv/search" method="GET" class="tv-search-form" autocomplete="off">
|
|
<div class="tv-search-wrapper">
|
|
<input type="text" name="q" id="search-input" value="{{ query }}"
|
|
placeholder="{{ t('search.placeholder') }}"
|
|
class="tv-search-input" data-focusable autofocus>
|
|
<div class="tv-autocomplete" id="autocomplete" style="display:none"></div>
|
|
</div>
|
|
<button type="submit" class="tv-search-btn" data-focusable>{{ t('search.button') }}</button>
|
|
</form>
|
|
|
|
{% if query %}
|
|
<!-- Serien-Ergebnisse -->
|
|
{% if series %}
|
|
<h2 class="tv-section-title">{{ t('search.results_series') }} ({{ series|length }})</h2>
|
|
<div class="tv-grid">
|
|
{% 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>
|
|
{% if s.genres %}
|
|
<span class="tv-card-meta">{{ s.genres }}</span>
|
|
{% endif %}
|
|
</div>
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Film-Ergebnisse -->
|
|
{% if movies %}
|
|
<h2 class="tv-section-title">{{ t('search.results_movies') }} ({{ movies|length }})</h2>
|
|
<div class="tv-grid">
|
|
{% 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">{{ m.year or "" }}</span>
|
|
</div>
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if not series and not movies %}
|
|
<div class="tv-empty">{{ t('search.no_results', query=query) }}</div>
|
|
{% endif %}
|
|
|
|
{% else %}
|
|
<!-- Such-History -->
|
|
{% if history %}
|
|
<div class="tv-search-history">
|
|
<div class="tv-search-history-header">
|
|
<h2 class="tv-section-title">{{ t('search.history') }}</h2>
|
|
<button class="tv-link-btn" onclick="clearHistory()" data-focusable>{{ t('search.clear_history') }}</button>
|
|
</div>
|
|
<div class="tv-search-history-list">
|
|
{% for h in history %}
|
|
<a href="/tv/search?q={{ h.query }}" class="tv-search-history-item" data-focusable>
|
|
<span class="tv-search-history-icon">🔍</span>
|
|
{{ h.query }}
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
{% endif %}
|
|
</section>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
// Autocomplete
|
|
const input = document.getElementById('search-input');
|
|
const acBox = document.getElementById('autocomplete');
|
|
let acTimer = null;
|
|
|
|
if (input) {
|
|
input.addEventListener('input', () => {
|
|
clearTimeout(acTimer);
|
|
const q = input.value.trim();
|
|
if (q.length < 2) { acBox.style.display = 'none'; return; }
|
|
acTimer = setTimeout(() => fetchSuggestions(q), 300);
|
|
});
|
|
input.addEventListener('blur', () => {
|
|
setTimeout(() => { acBox.style.display = 'none'; }, 200);
|
|
});
|
|
input.addEventListener('focus', () => {
|
|
if (acBox.children.length > 0) acBox.style.display = '';
|
|
});
|
|
}
|
|
|
|
function fetchSuggestions(q) {
|
|
fetch('/tv/api/search/suggest?q=' + encodeURIComponent(q))
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (!data.suggestions || data.suggestions.length === 0) {
|
|
acBox.style.display = 'none';
|
|
return;
|
|
}
|
|
acBox.innerHTML = data.suggestions.map(s =>
|
|
`<a href="/tv/search?q=${encodeURIComponent(s)}" class="tv-ac-item">${s}</a>`
|
|
).join('');
|
|
acBox.style.display = '';
|
|
})
|
|
.catch(() => { acBox.style.display = 'none'; });
|
|
}
|
|
|
|
function clearHistory() {
|
|
fetch('/tv/api/search/history', { method: 'DELETE' })
|
|
.then(() => {
|
|
const hist = document.querySelector('.tv-search-history');
|
|
if (hist) hist.remove();
|
|
})
|
|
.catch(() => {});
|
|
}
|
|
</script>
|
|
{% endblock %}
|