mirror of
https://github.com/ceratic/MediaCollectorLibary.git
synced 2026-05-13 23:56:46 +02:00
xbvr sync
This commit is contained in:
185
resources/views/components/adult-card.twig
Normal file
185
resources/views/components/adult-card.twig
Normal file
@@ -0,0 +1,185 @@
|
||||
{#
|
||||
Adult Video Card Component
|
||||
|
||||
Parameters:
|
||||
- movie: The adult video object with all properties (uses 'movie' variable name from controller)
|
||||
- view_mode: 'grid', 'list', or 'covers' (optional, defaults to 'grid')
|
||||
- show_rating: Whether to show rating (optional, defaults to true)
|
||||
- show_runtime: Whether to show runtime (optional, defaults to true)
|
||||
- show_genres: Whether to show genre tags (optional, defaults to true)
|
||||
- show_watched: Whether to show watched status (optional, defaults to true)
|
||||
#}
|
||||
|
||||
{% set view_mode = view_mode|default('grid') %}
|
||||
{% set show_rating = show_rating|default(true) %}
|
||||
{% set show_runtime = show_runtime|default(true) %}
|
||||
{% set show_genres = show_genres|default(true) %}
|
||||
{% set show_watched = show_watched|default(true) %}
|
||||
|
||||
{% if view_mode == 'list' %}
|
||||
<!-- List View Card -->
|
||||
<li class="px-4 py-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex items-center">
|
||||
{% if movie.poster_url %}
|
||||
<img class="rounded mr-3" style="width: 64px; height: 96px; object-fit: contain; background-color: #f8f9fa;" src="{{ movie.poster_url }}" alt="{{ movie.title }}">
|
||||
{% else %}
|
||||
<div class="bg-gray-100 rounded mr-3 flex items-center justify-center" style="width: 64px; height: 96px;">
|
||||
<svg class="text-gray-600" width="32" height="32" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="flex-1">
|
||||
<h3 class="text-sm font-semibold mb-1">
|
||||
<a href="{{ path_for('adult.show', {'id': movie.id}) }}" class="no-underline text-gray-900 hover:text-blue-600">
|
||||
{{ movie.title }}
|
||||
</a>
|
||||
</h3>
|
||||
<div class="flex items-center gap-3 text-sm text-gray-600">
|
||||
{% if movie.release_date %}
|
||||
<span>{{ movie.release_date|date('Y') }}</span>
|
||||
{% endif %}
|
||||
{% if show_rating and movie.rating %}
|
||||
<span>⭐ {{ movie.rating }}/10</span>
|
||||
{% endif %}
|
||||
{% if show_runtime and movie.runtime_minutes %}
|
||||
<span>{{ (movie.runtime_minutes / 60)|round(1) }}h {{ movie.runtime_minutes % 60 }}m</span>
|
||||
{% endif %}
|
||||
<span>{{ movie.source_name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
{% if show_watched and movie.watched %}
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||
Watched
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if movie.is_favorite %}
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
|
||||
Favorite
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{% elseif view_mode == 'covers' %}
|
||||
<!-- Cover View Card -->
|
||||
<div class="group relative bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-1">
|
||||
{% if movie.poster_url %}
|
||||
<div class="relative {{ movie.poster_aspect_ratio ? 'aspect-custom' : 'aspect-[2/3]' }} overflow-hidden">
|
||||
<img src="{{ movie.poster_url }}" alt="{{ movie.title }}" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300">
|
||||
<!-- Overlay with adult video info -->
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||
<div class="absolute bottom-0 left-0 right-0 p-3">
|
||||
{% if show_rating and movie.rating %}
|
||||
<div class="flex items-center mb-2">
|
||||
<svg class="w-4 h-4 text-yellow-400 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
|
||||
</svg>
|
||||
<span class="text-white text-sm font-medium">{{ movie.rating }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if show_watched and movie.watched %}
|
||||
<div class="flex items-center mb-1">
|
||||
<svg class="w-3 h-3 text-green-400 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<span class="text-green-400 text-xs">Watched</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="flex items-center justify-center bg-gradient-to-br from-gray-100 to-gray-200 aspect-[2/3] min-h-[200px]">
|
||||
<svg class="text-gray-400 w-12 h-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="p-4">
|
||||
<h6 class="text-sm font-bold truncate mb-1" title="{{ movie.title }}">
|
||||
<a href="{{ path_for('adult.show', {'id': movie.id}) }}" class="no-underline text-gray-900 hover:text-blue-600 transition-colors">
|
||||
{{ movie.title }}
|
||||
</a>
|
||||
</h6>
|
||||
{% if movie.release_date %}
|
||||
<p class="text-xs text-gray-600 font-medium">{{ movie.release_date|date('Y') }}</p>
|
||||
{% endif %}
|
||||
{% if show_genres and movie.genre %}
|
||||
<div class="mt-2">
|
||||
{% for genre in movie.genre|split(',')|slice(0, 2) %}
|
||||
<span class="inline-block bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full font-medium mr-1 mb-1">{{ genre|trim }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<!-- Grid View Card (Default) -->
|
||||
<div class="bg-white rounded-lg shadow-md border border-gray-200 h-full">
|
||||
<div class="p-4">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
{% if movie.poster_url %}
|
||||
<img class="rounded" style="width: 64px; height: 96px; object-fit: contain; background-color: #f8f9fa;" src="{{ movie.poster_url }}" alt="{{ movie.title }}">
|
||||
{% else %}
|
||||
<div class="bg-gray-100 rounded flex items-center justify-center" style="width: 64px; height: 96px;">
|
||||
<svg class="text-gray-600" width="32" height="32" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="ml-3 flex-1">
|
||||
<h5 class="text-lg font-semibold mb-1">
|
||||
<a href="{{ path_for('adult.show', {'id': movie.id}) }}" class="no-underline text-gray-900 hover:text-blue-600">
|
||||
{{ movie.title }}
|
||||
</a>
|
||||
</h5>
|
||||
<div class="flex items-center gap-2 text-sm text-gray-600 mb-2">
|
||||
{% if movie.release_date %}
|
||||
<span>{{ movie.release_date|date('Y') }}</span>
|
||||
{% endif %}
|
||||
{% if show_rating and movie.rating %}
|
||||
<span>⭐ {{ movie.rating }}/10</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if movie.source_name %}
|
||||
<p class="text-sm text-gray-600 mb-2">
|
||||
{{ movie.source_name }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if movie.overview %}
|
||||
<div class="mt-3">
|
||||
<p class="text-sm text-gray-600" style="display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;">
|
||||
{{ movie.overview|slice(0, 150) }}{% if movie.overview|length > 150 %}...{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="mt-3 flex justify-between items-center text-sm text-gray-600">
|
||||
{% if show_runtime and movie.runtime_minutes %}
|
||||
<span>{{ (movie.runtime_minutes / 60)|round(1) }}h {{ movie.runtime_minutes % 60 }}m</span>
|
||||
{% endif %}
|
||||
<div class="flex gap-1">
|
||||
{% if show_watched and movie.watched %}
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||
Watched
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if movie.is_favorite %}
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
|
||||
Favorite
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
321
resources/views/components/game-card.twig
Normal file
321
resources/views/components/game-card.twig
Normal file
@@ -0,0 +1,321 @@
|
||||
{#
|
||||
Game Card Component
|
||||
|
||||
Parameters:
|
||||
- game: The game object with all properties
|
||||
- view_mode: 'grid', 'list', or 'covers' (optional, defaults to 'grid')
|
||||
- show_completion: Whether to show completion progress (optional, defaults to true)
|
||||
- show_platforms: Whether to show platform badges (optional, defaults to true)
|
||||
- show_genres: Whether to show genre tags (optional, defaults to true)
|
||||
- show_playtime: Whether to show playtime (optional, defaults to true)
|
||||
#}
|
||||
|
||||
{% set view_mode = view_mode|default('grid') %}
|
||||
{% set show_completion = show_completion|default(true) %}
|
||||
{% set show_platforms = show_platforms|default(true) %}
|
||||
{% set show_genres = show_genres|default(true) %}
|
||||
{% set show_playtime = show_playtime|default(true) %}
|
||||
|
||||
{% if view_mode == 'list' %}
|
||||
<!-- List View Card -->
|
||||
<li class="group px-6 py-5 hover:bg-gradient-to-r hover:from-blue-50 hover:to-indigo-50 transition-all duration-200">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex items-center flex-1 min-w-0">
|
||||
<!-- Image Section -->
|
||||
<div class="relative flex-shrink-0 mr-4">
|
||||
{% if game.image_url %}
|
||||
<div class="w-16 h-16 rounded-lg overflow-hidden shadow-sm group-hover:shadow-md transition-shadow duration-200">
|
||||
<img class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" src="/images/playnite/{{ game.image_url }}" alt="{{ game.title }}">
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="w-16 h-16 bg-gradient-to-br from-slate-100 to-slate-200 rounded-lg flex items-center justify-center shadow-sm">
|
||||
<svg class="text-slate-400 w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-7 8h12a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Completion indicator -->
|
||||
{% if show_completion and game.max_completion > 0 %}
|
||||
<div class="absolute -top-1 -right-1 w-5 h-5 bg-green-500 rounded-full flex items-center justify-center shadow-sm">
|
||||
<span class="text-xs font-bold text-white">{{ game.max_completion }}%</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Content Section -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-lg font-bold text-gray-900 mb-1 truncate group-hover:text-blue-600 transition-colors">
|
||||
<a href="{{ path_for('games.show', {'game_key': game.game_key}) }}" class="no-underline">
|
||||
{{ game.title }}
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
<!-- Platform and stats -->
|
||||
<div class="flex items-center gap-4 text-sm text-gray-600 mb-2">
|
||||
{% if show_platforms %}
|
||||
<div class="flex items-center gap-1">
|
||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
<span>{{ game.platform_count }} platform{{ game.platform_count > 1 ? 's' : '' }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if show_playtime and game.total_playtime %}
|
||||
<div class="flex items-center gap-1">
|
||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<span>{{ game.total_playtime|format_duration }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Platforms -->
|
||||
{% if show_platforms and game.platforms %}
|
||||
<div class="flex flex-wrap gap-1 mb-2">
|
||||
{% for platform in game.platforms|slice(0, 3) %}
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium bg-slate-100 text-slate-700">
|
||||
{{ platform }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
{% if game.platforms|length > 3 %}
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium bg-slate-100 text-slate-700">
|
||||
+{{ game.platforms|length - 3 }} more
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Genres Section -->
|
||||
{% if show_genres and game.genres %}
|
||||
<div class="flex flex-wrap gap-1 ml-4 flex-shrink-0">
|
||||
{% for genre in game.genres|slice(0, 2) %}
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-gradient-to-r from-blue-50 to-blue-100 text-blue-700 border border-blue-200">
|
||||
{{ genre }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
{% if game.genres|length > 2 %}
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-gradient-to-r from-gray-50 to-gray-100 text-gray-700 border border-gray-200">
|
||||
+{{ game.genres|length - 2 }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick action button -->
|
||||
<div class="ml-4 flex-shrink-0">
|
||||
<a href="{{ path_for('games.show', {'game_key': game.game_key}) }}"
|
||||
class="inline-flex items-center px-3 py-2 text-sm font-medium text-blue-600 bg-blue-50 rounded-lg hover:bg-blue-100 hover:text-blue-700 transition-colors opacity-0 group-hover:opacity-100 transform translate-x-2 group-hover:translate-x-0 transition-all duration-200">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
||||
</svg>
|
||||
View
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{% elseif view_mode == 'covers' %}
|
||||
<!-- Cover View Card -->
|
||||
<div class="group bg-white rounded-xl shadow-lg hover:shadow-2xl border border-gray-100 overflow-hidden h-full transition-all duration-300 hover:scale-[1.05] hover:-translate-y-2">
|
||||
<!-- Cover Image -->
|
||||
<div class="relative overflow-hidden">
|
||||
{% if game.image_url %}
|
||||
<div class="aspect-[3/4] overflow-hidden">
|
||||
<img src="/images/playnite/{{ game.image_url }}" alt="{{ game.title }}" class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500">
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="aspect-[3/4] bg-gradient-to-br from-slate-100 to-slate-200 flex items-center justify-center">
|
||||
<div class="text-center">
|
||||
<svg class="text-slate-400 w-12 h-12 mx-auto mb-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-7 8h12a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
<div class="text-xs text-slate-500 font-medium">No Cover</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Overlay with quick actions -->
|
||||
<div class="absolute inset-0 bg-opacity-0 group-hover:bg-opacity-30 transition-all duration-300 flex items-center justify-center">
|
||||
<a href="{{ path_for('games.show', {'game_key': game.game_key}) }}"
|
||||
class="opacity-0 group-hover:opacity-100 transform translate-y-4 group-hover:translate-y-0 transition-all duration-300 bg-white text-gray-900 px-3 py-2 rounded-lg font-medium text-sm shadow-lg hover:bg-blue-50 hover:text-blue-700">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
||||
</svg>
|
||||
View
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Completion badge -->
|
||||
{% if show_completion and game.max_completion > 0 %}
|
||||
<div class="absolute top-2 right-2">
|
||||
<div class="bg-green-500 text-white text-xs font-bold px-2 py-1 rounded-full shadow-sm">
|
||||
{{ game.max_completion }}%
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Platform count badge -->
|
||||
{% if show_platforms %}
|
||||
<div class="absolute bottom-2 left-2">
|
||||
<div class="bg-black bg-opacity-70 text-white text-xs font-medium px-2 py-1 rounded-full backdrop-blur-sm">
|
||||
{{ game.platform_count }}P
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Game Info -->
|
||||
<div class="p-4">
|
||||
<h3 class="text-sm font-bold text-gray-900 mb-1 line-clamp-2 group-hover:text-blue-600 transition-colors" title="{{ game.title }}">
|
||||
<a href="{{ path_for('games.show', {'game_key': game.game_key}) }}" class="no-underline">
|
||||
{{ game.title }}
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
<!-- Playtime indicator -->
|
||||
{% if show_playtime and game.total_playtime %}
|
||||
<div class="flex items-center text-xs text-gray-600 mb-2">
|
||||
<svg class="w-3 h-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
{{ game.total_playtime|format_duration }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Platforms -->
|
||||
{% if show_platforms and game.platforms %}
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{% for platform in game.platforms|slice(0, 2) %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-slate-100 text-slate-700">
|
||||
{{ platform }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
{% if game.platforms|length > 2 %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-slate-100 text-slate-700">
|
||||
+{{ game.platforms|length - 2 }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<!-- Grid View Card (Default) -->
|
||||
<div class="group bg-white rounded-xl shadow-lg hover:shadow-2xl border border-gray-100 h-full overflow-hidden transition-all duration-300 hover:scale-[1.02] hover:-translate-y-1">
|
||||
<!-- Image Section -->
|
||||
<div class="relative overflow-hidden">
|
||||
{% if game.image_url %}
|
||||
<div class="aspect-[4/3] overflow-hidden">
|
||||
<img class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500" src="/images/playnite/{{ game.image_url }}" alt="{{ game.title }}">
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="aspect-[4/3] bg-gradient-to-br from-slate-100 to-slate-200 flex items-center justify-center">
|
||||
<div class="text-center">
|
||||
<svg class="text-slate-400 w-16 h-16 mx-auto mb-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-7 8h12a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
<div class="text-xs text-slate-500 font-medium">No Cover</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Platform badge -->
|
||||
{% if show_platforms %}
|
||||
<div class="absolute top-3 right-3 z-10">
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-black bg-opacity-70 text-white backdrop-blur-sm">
|
||||
{{ game.platform_count }} platform{{ game.platform_count > 1 ? 's' : '' }}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Overlay with quick actions -->
|
||||
<div class="absolute inset-0 bg-opacity-0 group-hover:bg-opacity-20 transition-all duration-300 flex items-center justify-center z-20">
|
||||
<a href="{{ path_for('games.show', {'game_key': game.game_key}) }}"
|
||||
class="opacity-0 group-hover:opacity-100 transform translate-y-4 group-hover:translate-y-0 transition-all duration-300 bg-white text-gray-900 px-4 py-2 rounded-lg font-medium text-sm shadow-lg hover:bg-blue-50 hover:text-blue-700">
|
||||
View Details
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Section -->
|
||||
<div class="p-5">
|
||||
<div class="mb-3">
|
||||
<h3 class="text-lg font-bold text-gray-900 mb-2 line-clamp-2 group-hover:text-blue-600 transition-colors">
|
||||
<a href="{{ path_for('games.show', {'game_key': game.game_key}) }}" class="no-underline">
|
||||
{{ game.title }}
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
{% if show_platforms and game.platforms %}
|
||||
<div class="flex flex-wrap gap-1 mb-3">
|
||||
{% for platform in game.platforms|slice(0, 2) %}
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium bg-slate-100 text-slate-700">
|
||||
{{ platform }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
{% if game.platforms|length > 2 %}
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium bg-slate-100 text-slate-700">
|
||||
+{{ game.platforms|length - 2 }} more
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Progress and Stats -->
|
||||
<div class="space-y-3">
|
||||
{% if show_completion and game.max_completion > 0 %}
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-1">
|
||||
<span class="text-xs font-medium text-gray-600">Completion</span>
|
||||
<span class="text-xs font-bold text-gray-900">{{ game.max_completion }}%</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div class="bg-gradient-to-r from-green-400 to-green-600 h-2 rounded-full transition-all duration-500" style="width: {{ game.max_completion }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if show_playtime and game.total_playtime %}
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center text-xs text-gray-600">
|
||||
<svg class="w-3 h-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
Playtime
|
||||
</div>
|
||||
<span class="text-xs font-semibold text-gray-900">{{ game.total_playtime|format_duration }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Genres -->
|
||||
{% if show_genres and game.genres %}
|
||||
<div class="mt-4 flex flex-wrap gap-1">
|
||||
{% for genre in game.genres|slice(0, 2) %}
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-gradient-to-r from-blue-50 to-blue-100 text-blue-700 border border-blue-200">
|
||||
{{ genre }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
{% if game.genres|length > 2 %}
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-gradient-to-r from-gray-50 to-gray-100 text-gray-700 border border-gray-200">
|
||||
+{{ game.genres|length - 2 }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
185
resources/views/components/movie-card.twig
Normal file
185
resources/views/components/movie-card.twig
Normal file
@@ -0,0 +1,185 @@
|
||||
{#
|
||||
Movie Card Component
|
||||
|
||||
Parameters:
|
||||
- movie: The movie object with all properties
|
||||
- view_mode: 'grid', 'list', or 'covers' (optional, defaults to 'grid')
|
||||
- show_rating: Whether to show rating (optional, defaults to true)
|
||||
- show_runtime: Whether to show runtime (optional, defaults to true)
|
||||
- show_genres: Whether to show genre tags (optional, defaults to true)
|
||||
- show_watched: Whether to show watched status (optional, defaults to true)
|
||||
#}
|
||||
|
||||
{% set view_mode = view_mode|default('grid') %}
|
||||
{% set show_rating = show_rating|default(true) %}
|
||||
{% set show_runtime = show_runtime|default(true) %}
|
||||
{% set show_genres = show_genres|default(true) %}
|
||||
{% set show_watched = show_watched|default(true) %}
|
||||
|
||||
{% if view_mode == 'list' %}
|
||||
<!-- List View Card -->
|
||||
<li class="px-4 py-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex items-center">
|
||||
{% if movie.poster_url %}
|
||||
<img class="rounded mr-3" style="width: 64px; height: 96px; object-fit: cover;" src="/images/{{ movie.poster_url }}" alt="{{ movie.title }}">
|
||||
{% else %}
|
||||
<div class="bg-gray-100 rounded mr-3 flex items-center justify-center" style="width: 64px; height: 96px;">
|
||||
<svg class="text-gray-600" width="32" height="32" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 4V2a1 1 0 011-1h8a1 1 0 011 1v2h4a1 1 0 010 2h-1v14a2 2 0 01-2 2H6a2 2 0 01-2-2V6H3a1 1 0 010-2h4z"/>
|
||||
</svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="flex-1">
|
||||
<h3 class="text-sm font-semibold mb-1">
|
||||
<a href="{{ path_for('movies.show', {'id': movie.id}) }}" class="no-underline text-gray-900 hover:text-blue-600">
|
||||
{{ movie.title }}
|
||||
</a>
|
||||
</h3>
|
||||
<div class="flex items-center gap-3 text-sm text-gray-600">
|
||||
{% if movie.release_date %}
|
||||
<span>{{ movie.release_date|date('Y') }}</span>
|
||||
{% endif %}
|
||||
{% if show_rating and movie.rating %}
|
||||
<span>⭐ {{ movie.rating }}/10</span>
|
||||
{% endif %}
|
||||
{% if show_runtime and movie.runtime_minutes %}
|
||||
<span>{{ (movie.runtime_minutes / 60)|round(1) }}h {{ movie.runtime_minutes % 60 }}m</span>
|
||||
{% endif %}
|
||||
<span>{{ movie.source_name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
{% if show_watched and movie.watched %}
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||
Watched
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if movie.is_favorite %}
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
|
||||
Favorite
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{% elseif view_mode == 'covers' %}
|
||||
<!-- Cover View Card -->
|
||||
<div class="group relative bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-1">
|
||||
{% if movie.poster_url %}
|
||||
<div class="relative aspect-[2/3] overflow-hidden">
|
||||
<img src="/images/{{ movie.poster_url }}" alt="{{ movie.title }}" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300">
|
||||
<!-- Overlay with movie info -->
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||
<div class="absolute bottom-0 left-0 right-0 p-3">
|
||||
{% if show_rating and movie.rating %}
|
||||
<div class="flex items-center mb-2">
|
||||
<svg class="w-4 h-4 text-yellow-400 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
|
||||
</svg>
|
||||
<span class="text-white text-sm font-medium">{{ movie.rating }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if show_watched and movie.watched %}
|
||||
<div class="flex items-center mb-1">
|
||||
<svg class="w-3 h-3 text-green-400 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<span class="text-green-400 text-xs">Watched</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="flex items-center justify-center bg-gradient-to-br from-gray-100 to-gray-200 aspect-[2/3] min-h-[200px]">
|
||||
<svg class="text-gray-400 w-12 h-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 4V2a1 1 0 011-1h8a1 1 0 011 1v2h4a1 1 0 010 2h-1v14a2 2 0 01-2 2H6a2 2 0 01-2-2V6H3a1 1 0 010-2h4z"/>
|
||||
</svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="p-4">
|
||||
<h6 class="text-sm font-bold truncate mb-1" title="{{ movie.title }}">
|
||||
<a href="{{ path_for('movies.show', {'id': movie.id}) }}" class="no-underline text-gray-900 hover:text-blue-600 transition-colors">
|
||||
{{ movie.title }}
|
||||
</a>
|
||||
</h6>
|
||||
{% if movie.release_date %}
|
||||
<p class="text-xs text-gray-600 font-medium">{{ movie.release_date|date('Y') }}</p>
|
||||
{% endif %}
|
||||
{% if show_genres and movie.genre %}
|
||||
<div class="mt-2">
|
||||
{% for genre in movie.genre|split(',')|slice(0, 2) %}
|
||||
<span class="inline-block bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full font-medium mr-1 mb-1">{{ genre|trim }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<!-- Grid View Card (Default) -->
|
||||
<div class="bg-white rounded-lg shadow-md border border-gray-200 h-full">
|
||||
<div class="p-4">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
{% if movie.poster_url %}
|
||||
<img class="rounded" style="width: 64px; height: 96px; object-fit: cover;" src="/images/{{ movie.poster_url }}" alt="{{ movie.title }}">
|
||||
{% else %}
|
||||
<div class="bg-gray-100 rounded flex items-center justify-center" style="width: 64px; height: 96px;">
|
||||
<svg class="text-gray-600" width="32" height="32" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 4V2a1 1 0 011-1h8a1 1 0 011 1v2h4a1 1 0 010 2h-1v14a2 2 0 01-2 2H6a2 2 0 01-2-2V6H3a1 1 0 010-2h4z"/>
|
||||
</svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="ml-3 flex-1">
|
||||
<h5 class="text-lg font-semibold mb-1">
|
||||
<a href="{{ path_for('movies.show', {'id': movie.id}) }}" class="no-underline text-gray-900 hover:text-blue-600">
|
||||
{{ movie.title }}
|
||||
</a>
|
||||
</h5>
|
||||
<div class="flex items-center gap-2 text-sm text-gray-600 mb-2">
|
||||
{% if movie.release_date %}
|
||||
<span>{{ movie.release_date|date('Y') }}</span>
|
||||
{% endif %}
|
||||
{% if show_rating and movie.rating %}
|
||||
<span>⭐ {{ movie.rating }}/10</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if movie.source_name %}
|
||||
<p class="text-sm text-gray-600 mb-2">
|
||||
{{ movie.source_name }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if movie.overview %}
|
||||
<div class="mt-3">
|
||||
<p class="text-sm text-gray-600" style="display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;">
|
||||
{{ movie.overview|slice(0, 150) }}{% if movie.overview|length > 150 %}...{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="mt-3 flex justify-between items-center text-sm text-gray-600">
|
||||
{% if show_runtime and movie.runtime_minutes %}
|
||||
<span>{{ (movie.runtime_minutes / 60)|round(1) }}h {{ movie.runtime_minutes % 60 }}m</span>
|
||||
{% endif %}
|
||||
<div class="flex gap-1">
|
||||
{% if show_watched and movie.watched %}
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||
Watched
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if movie.is_favorite %}
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
|
||||
Favorite
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
181
resources/views/components/tvshow-card.twig
Normal file
181
resources/views/components/tvshow-card.twig
Normal file
@@ -0,0 +1,181 @@
|
||||
{#
|
||||
TV Show Card Component
|
||||
|
||||
Parameters:
|
||||
- tvshow: The TV show object with all properties
|
||||
- view_mode: 'grid', 'list', or 'covers' (optional, defaults to 'grid')
|
||||
- show_rating: Whether to show rating (optional, defaults to true)
|
||||
- show_seasons: Whether to show season count (optional, defaults to true)
|
||||
- show_episodes: Whether to show episode count (optional, defaults to true)
|
||||
- show_genres: Whether to show genre tags (optional, defaults to true)
|
||||
#}
|
||||
|
||||
{% set view_mode = view_mode|default('grid') %}
|
||||
{% set show_rating = show_rating|default(true) %}
|
||||
{% set show_seasons = show_seasons|default(true) %}
|
||||
{% set show_episodes = show_episodes|default(true) %}
|
||||
{% set show_genres = show_genres|default(true) %}
|
||||
|
||||
{% if view_mode == 'list' %}
|
||||
<!-- List View Card -->
|
||||
<li class="px-4 py-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex items-center">
|
||||
{% if tvshow.poster_url %}
|
||||
<img class="rounded mr-3" style="width: 64px; height: 96px; object-fit: cover;" src="/images/{{ tvshow.poster_url }}" alt="{{ tvshow.title }}">
|
||||
{% else %}
|
||||
<div class="bg-gray-100 rounded mr-3 flex items-center justify-center" style="width: 64px; height: 96px;">
|
||||
<svg class="text-gray-600" width="32" height="32" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 4V2a1 1 0 011-1h8a1 1 0 011 1v2h4a1 1 0 010 2h-1v14a2 2 0 01-2 2H6a2 2 0 01-2-2V6H3a1 1 0 010-2h4z"/>
|
||||
</svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="flex-1">
|
||||
<h3 class="text-sm font-semibold mb-1">
|
||||
<a href="{{ path_for('tvshows.show', {'id': tvshow.id}) }}" class="no-underline text-gray-900 hover:text-blue-600">
|
||||
{{ tvshow.title }}
|
||||
</a>
|
||||
</h3>
|
||||
<div class="flex items-center gap-3 text-sm text-gray-600">
|
||||
{% if tvshow.first_air_date %}
|
||||
<span>{{ tvshow.first_air_date|date('Y') }}</span>
|
||||
{% endif %}
|
||||
{% if show_rating and tvshow.rating %}
|
||||
<span>⭐ {{ tvshow.rating }}/10</span>
|
||||
{% endif %}
|
||||
{% if show_seasons and tvshow.number_of_seasons %}
|
||||
<span>{{ tvshow.number_of_seasons }} season{{ tvshow.number_of_seasons > 1 ? 's' : '' }}</span>
|
||||
{% endif %}
|
||||
{% if show_episodes and tvshow.number_of_episodes %}
|
||||
<span>{{ tvshow.number_of_episodes }} episode{{ tvshow.number_of_episodes > 1 ? 's' : '' }}</span>
|
||||
{% endif %}
|
||||
<span>{{ tvshow.source_name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
{% if tvshow.is_favorite %}
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
|
||||
Favorite
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{% elseif view_mode == 'covers' %}
|
||||
<!-- Cover View Card -->
|
||||
<div class="group relative bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-1">
|
||||
{% if tvshow.poster_url %}
|
||||
<div class="relative aspect-[2/3] overflow-hidden">
|
||||
<img src="/images/{{ tvshow.poster_url }}" alt="{{ tvshow.title }}" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300">
|
||||
<!-- Overlay with TV show info -->
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||
<div class="absolute bottom-0 left-0 right-0 p-3">
|
||||
{% if show_rating and tvshow.rating %}
|
||||
<div class="flex items-center mb-2">
|
||||
<svg class="w-4 h-4 text-yellow-400 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
|
||||
</svg>
|
||||
<span class="text-white text-sm font-medium">{{ tvshow.rating }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if show_seasons and tvshow.number_of_seasons %}
|
||||
<div class="flex items-center mb-1">
|
||||
<svg class="w-3 h-3 text-blue-400 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zM3 10a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H4a1 1 0 01-1-1v-6zM14 9a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1v-6a1 1 0 00-1-1h-2z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<span class="text-blue-400 text-xs">{{ tvshow.number_of_seasons }} season{{ tvshow.number_of_seasons > 1 ? 's' : '' }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="flex items-center justify-center bg-gradient-to-br from-gray-100 to-gray-200 aspect-[2/3] min-h-[200px]">
|
||||
<svg class="text-gray-400 w-12 h-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 4V2a1 1 0 011-1h8a1 1 0 011 1v2h4a1 1 0 010 2h-1v14a2 2 0 01-2 2H6a2 2 0 01-2-2V6H3a1 1 0 010-2h4z"/>
|
||||
</svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="p-4">
|
||||
<h6 class="text-sm font-bold truncate mb-1" title="{{ tvshow.title }}">
|
||||
<a href="{{ path_for('tvshows.show', {'id': tvshow.id}) }}" class="no-underline text-gray-900 hover:text-blue-600 transition-colors">
|
||||
{{ tvshow.title }}
|
||||
</a>
|
||||
</h6>
|
||||
{% if tvshow.first_air_date %}
|
||||
<p class="text-xs text-gray-600 font-medium">{{ tvshow.first_air_date|date('Y') }}</p>
|
||||
{% endif %}
|
||||
{% if show_genres and tvshow.genre %}
|
||||
<div class="mt-2">
|
||||
{% for genre in tvshow.genre|split(',')|slice(0, 2) %}
|
||||
<span class="inline-block bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full font-medium mr-1 mb-1">{{ genre|trim }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<!-- Grid View Card (Default) -->
|
||||
<div class="bg-white rounded-lg shadow-md border border-gray-200 h-full">
|
||||
<div class="p-4">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
{% if tvshow.poster_url %}
|
||||
<img class="rounded" style="width: 64px; height: 96px; object-fit: cover;" src="/images/{{ tvshow.poster_url }}" alt="{{ tvshow.title }}">
|
||||
{% else %}
|
||||
<div class="bg-gray-100 rounded flex items-center justify-center" style="width: 64px; height: 96px;">
|
||||
<svg class="text-gray-600" width="32" height="32" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 4V2a1 1 0 011-1h8a1 1 0 011 1v2h4a1 1 0 010 2h-1v14a2 2 0 01-2 2H6a2 2 0 01-2-2V6H3a1 1 0 010-2h4z"/>
|
||||
</svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="ml-3 flex-1">
|
||||
<h5 class="text-lg font-semibold mb-1">
|
||||
<a href="{{ path_for('tvshows.show', {'id': tvshow.id}) }}" class="no-underline text-gray-900 hover:text-blue-600">
|
||||
{{ tvshow.title }}
|
||||
</a>
|
||||
</h5>
|
||||
<div class="flex items-center gap-2 text-sm text-gray-600 mb-2">
|
||||
{% if tvshow.first_air_date %}
|
||||
<span>{{ tvshow.first_air_date|date('Y') }}</span>
|
||||
{% endif %}
|
||||
{% if show_rating and tvshow.rating %}
|
||||
<span>⭐ {{ tvshow.rating }}/10</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if tvshow.source_name %}
|
||||
<p class="text-sm text-gray-600 mb-2">
|
||||
{{ tvshow.source_name }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if tvshow.overview %}
|
||||
<div class="mt-3">
|
||||
<p class="text-sm text-gray-600" style="display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;">
|
||||
{{ tvshow.overview|slice(0, 150) }}{% if tvshow.overview|length > 150 %}...{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="mt-3 flex justify-between items-center text-sm text-gray-600">
|
||||
{% if show_seasons and tvshow.number_of_seasons %}
|
||||
<span>{{ tvshow.number_of_seasons }} season{{ tvshow.number_of_seasons > 1 ? 's' : '' }}</span>
|
||||
{% endif %}
|
||||
{% if show_episodes and tvshow.number_of_episodes %}
|
||||
<span>{{ tvshow.number_of_episodes }} episode{{ tvshow.number_of_episodes > 1 ? 's' : '' }}</span>
|
||||
{% endif %}
|
||||
<div class="flex gap-1">
|
||||
{% if tvshow.is_favorite %}
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
|
||||
Favorite
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
Reference in New Issue
Block a user