docker.videokonverter/video-konverter/app/templates/tv/movie_detail.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

158 lines
6.5 KiB
HTML

{% extends "tv/base.html" %}
{% block title %}{{ movie.title or movie.folder_name }} - VideoKonverter TV{% endblock %}
{% block content %}
<section class="tv-section">
<div class="tv-detail-header">
{% if movie.poster_url %}
<img src="{{ movie.poster_url }}" alt="" class="tv-detail-poster">
{% endif %}
<div class="tv-detail-info">
<h1 class="tv-page-title">{{ movie.title or movie.folder_name }}</h1>
{% if movie.year %}
<p class="tv-detail-year">{{ movie.year }}</p>
{% endif %}
{% if movie.genres %}
<p class="tv-detail-genres">{{ movie.genres }}</p>
{% endif %}
{% if movie.overview %}
<p class="tv-detail-overview">{{ movie.overview }}</p>
{% endif %}
<!-- Bewertungen -->
<div class="tv-rating-section">
<div class="tv-rating-user">
<span class="tv-rating-label">{{ t('rating.your_rating') }}:</span>
<div class="tv-stars-input" id="user-stars"
data-movie-id="{{ movie.id }}" data-rating="{{ user_rating }}">
{% for i in range(1, 6) %}
<span class="tv-star {% if i <= user_rating %}active{% endif %}"
data-value="{{ i }}" data-focusable
onclick="setRating({{ i }})">&#9733;</span>
{% endfor %}
{% if user_rating > 0 %}
<span class="tv-rating-remove" onclick="setRating(0)"
data-focusable title="{{ t('rating.remove') }}">&#10005;</span>
{% endif %}
</div>
</div>
{% if avg_rating.count > 0 %}
<div class="tv-rating-avg">
<span class="tv-stars-display">
{% for i in range(1, 6) %}
<span class="tv-star {% if i <= avg_rating.avg|round|int %}active{% endif %}">&#9733;</span>
{% endfor %}
</span>
<span class="tv-rating-text">{{ avg_rating.avg }} ({{ avg_rating.count }})</span>
</div>
{% endif %}
{% if tvdb_score %}
<div class="tv-rating-external">
<span class="tv-rating-badge tvdb">TVDB {{ "%.0f"|format(tvdb_score) }}%</span>
</div>
{% endif %}
</div>
<div class="tv-detail-actions">
{% if videos %}
<a href="/tv/player?v={{ videos[0].id }}" class="tv-play-btn" data-focusable>
&#9654; {{ t('player.play') }}
</a>
{% endif %}
<button class="tv-watchlist-btn {% if in_watchlist %}active{% endif %}"
id="btn-watchlist"
data-focusable
data-movie-id="{{ movie.id }}"
onclick="toggleWatchlist(this)">
<span class="watchlist-icon">{% if in_watchlist %}&#9829;{% else %}&#9825;{% endif %}</span>
<span class="watchlist-text">{{ t('watchlist.title') }}</span>
</button>
</div>
</div>
</div>
{% if videos|length > 1 %}
<h3 class="tv-section-title">{{ t('movies.versions') }}</h3>
<div class="tv-episode-list">
{% for v in videos %}
<a href="/tv/player?v={{ v.id }}" class="tv-episode-card" data-focusable>
<div class="tv-ep-thumb">
<img src="/api/library/videos/{{ v.id }}/thumbnail" alt="" loading="lazy">
<div class="tv-ep-duration">
{% if v.duration_sec %}{{ (v.duration_sec / 60)|round|int }} Min{% endif %}
</div>
</div>
<div class="tv-ep-info">
<div class="tv-ep-header">
<span class="tv-ep-title">{{ v.file_name }}</span>
</div>
<div class="tv-ep-meta">
{% if v.width %}{{ v.width }}x{{ v.height }}{% endif %}
&middot; {{ v.container|upper }}
{% if v.video_codec %} &middot; {{ v.video_codec }}{% endif %}
</div>
</div>
</a>
{% endfor %}
</div>
{% endif %}
</section>
{% endblock %}
{% block scripts %}
<script>
function toggleWatchlist(btn) {
const movieId = btn.dataset.movieId;
fetch('/tv/api/watchlist', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ movie_id: parseInt(movieId) }),
})
.then(r => r.json())
.then(data => {
if (data.in_watchlist) {
btn.classList.add('active');
btn.querySelector('.watchlist-icon').innerHTML = '&#9829;';
} else {
btn.classList.remove('active');
btn.querySelector('.watchlist-icon').innerHTML = '&#9825;';
}
})
.catch(() => {});
}
function setRating(value) {
const container = document.getElementById('user-stars');
const movieId = container.dataset.movieId;
fetch('/tv/api/rating', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ movie_id: parseInt(movieId), rating: value }),
})
.then(r => r.json())
.then(data => {
container.dataset.rating = data.user_rating;
container.querySelectorAll('.tv-star').forEach(star => {
const v = parseInt(star.dataset.value);
star.classList.toggle('active', v <= data.user_rating);
});
let removeBtn = container.querySelector('.tv-rating-remove');
if (data.user_rating > 0 && !removeBtn) {
removeBtn = document.createElement('span');
removeBtn.className = 'tv-rating-remove';
removeBtn.setAttribute('data-focusable', '');
removeBtn.innerHTML = '&#10005;';
removeBtn.onclick = () => setRating(0);
container.appendChild(removeBtn);
} else if (data.user_rating === 0 && removeBtn) {
removeBtn.remove();
}
if (data.avg_rating !== undefined) {
const avgEl = document.querySelector('.tv-rating-avg .tv-rating-text');
if (avgEl) avgEl.textContent = data.avg_rating + ' (' + data.rating_count + ')';
}
})
.catch(() => {});
}
</script>
{% endblock %}