mirror of
https://github.com/ceratic/MediaCollectorLibary.git
synced 2026-05-14 08:06:47 +02:00
1195 lines
73 KiB
Twig
1195 lines
73 KiB
Twig
{% extends "layouts/app.twig" %}
|
|
|
|
{% block content %}
|
|
<!-- Modern Hero Section -->
|
|
<div class="relative min-h-[60vh] bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 overflow-hidden">
|
|
<!-- Background Image with Overlay -->
|
|
<div class="absolute inset-0 bg-cover bg-center bg-no-repeat opacity-20"
|
|
style="background-image: url('{{ platform_versions[0].banner_url ? "/images/playnite/" ~ platform_versions[0].banner_url : "/images/hero-bg.jpg" }}')"></div>
|
|
<div class="absolute inset-0 bg-gradient-to-r from-slate-900/90 via-slate-900/70 to-slate-900/90"></div>
|
|
|
|
<!-- Content -->
|
|
<div class="relative z-10 container mx-auto px-6 py-16">
|
|
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8 items-center min-h-[50vh]">
|
|
|
|
<!-- Game Cover -->
|
|
<div class="lg:col-span-3 flex justify-center lg:justify-start">
|
|
<div id="gameCover" class="relative group">
|
|
{% if platform_versions[0].cover_image_url %}
|
|
<img src="/images/playnite/{{ platform_versions[0].cover_image_url }}"
|
|
class="w-48 h-72 object-cover rounded-xl shadow-2xl transform group-hover:scale-105 transition-transform duration-300"
|
|
alt="{{ main_game.title }}">
|
|
<div class="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
|
{% elseif main_game.image_url %}
|
|
<img src="/images/playnite/{{ main_game.image_url }}"
|
|
class="w-48 h-72 object-cover rounded-xl shadow-2xl transform group-hover:scale-105 transition-transform duration-300"
|
|
alt="{{ main_game.title }}">
|
|
<div class="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
|
{% else %}
|
|
<div class="w-48 h-72 bg-slate-800 rounded-xl flex items-center justify-center shadow-2xl">
|
|
<svg class="w-16 h-16 text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"/>
|
|
</svg>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Game Info -->
|
|
<div class="lg:col-span-6 text-white">
|
|
<!-- Breadcrumb -->
|
|
<nav class="mb-4">
|
|
<ol class="flex items-center space-x-2 text-sm">
|
|
<li><a href="{{ path_for('games.index') }}" class="text-slate-300 hover:text-white transition-colors">Games</a></li>
|
|
<li class="text-slate-500">/</li>
|
|
<li class="text-white font-medium">{{ main_game.title }}</li>
|
|
</ol>
|
|
</nav>
|
|
|
|
<!-- Title -->
|
|
<h1 class="text-4xl lg:text-5xl font-bold mb-4 bg-gradient-to-r from-white to-slate-200 bg-clip-text text-transparent">
|
|
{{ main_game.title }}
|
|
</h1>
|
|
|
|
<!-- Badges -->
|
|
<div class="flex flex-wrap gap-2 mb-6">
|
|
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-600 text-white">
|
|
<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="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2"/>
|
|
</svg>
|
|
{{ platform_versions|length }} Platform{{ platform_versions|length > 1 ? 's' : '' }}
|
|
</span>
|
|
{% if main_game.genre %}
|
|
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-slate-700 text-slate-200">
|
|
{{ main_game.genre }}
|
|
</span>
|
|
{% endif %}
|
|
{% set total_playtime = 0 %}
|
|
{% for platform in platform_versions %}
|
|
{% if platform.playtime_minutes %}
|
|
{% set total_playtime = total_playtime + platform.playtime_minutes %}
|
|
{% endif %}
|
|
{% endfor %}
|
|
{% set total_hours = total_playtime // 60 %}
|
|
{% if total_hours > 0 %}
|
|
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-600 text-white">
|
|
<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="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
</svg>
|
|
{{ total_hours }}h Played
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Action Buttons -->
|
|
<div class="flex flex-wrap gap-3 mb-8">
|
|
<button class="inline-flex items-center px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors duration-200 shadow-lg">
|
|
<svg class="w-5 h-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-6 8a9 9 0 110-18 9 9 0 010 18z"/>
|
|
</svg>
|
|
Play
|
|
</button>
|
|
<button class="inline-flex items-center px-6 py-3 bg-slate-700 hover:bg-slate-600 text-white font-medium rounded-lg transition-colors duration-200 border border-slate-600">
|
|
<svg class="w-5 h-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"/>
|
|
</svg>
|
|
Favorite
|
|
</button>
|
|
<button class="inline-flex items-center px-6 py-3 bg-slate-700 hover:bg-slate-600 text-white font-medium rounded-lg transition-colors duration-200 border border-slate-600">
|
|
<svg class="w-5 h-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.367 2.684 3 3 0 00-5.367-2.684z"/>
|
|
</svg>
|
|
Share
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Platform Tabs -->
|
|
<ul class="nav nav-tabs border-0 flex flex-wrap gap-2" id="platformTabs" role="tablist">
|
|
{% for version in platform_versions %}
|
|
{% set safePlatformId = version.platform|lower|replace({' ': '-', '(': '', ')': ''}) ~ '-' ~ version.source_id %}
|
|
{% set platform_icons = {
|
|
'windows': 'mdi:microsoft-windows',
|
|
'pc': 'mdi:desktop-classic',
|
|
'playstation': 'mdi:sony-playstation',
|
|
'xbox': 'mdi:microsoft-xbox',
|
|
'nintendo': 'mdi:nintendo-switch',
|
|
'switch': 'mdi:nintendo-switch',
|
|
'wii': 'mdi:nintendo-wii',
|
|
'gamecube': 'mdi:gamepad-square',
|
|
'n64': 'mdi:gamepad-square',
|
|
'snes': 'mdi:gamepad-square',
|
|
'nes': 'mdi:gamepad-square',
|
|
'android': 'mdi:android',
|
|
'ios': 'mdi:apple-ios',
|
|
'linux': 'mdi:linux',
|
|
'macos': 'mdi:apple',
|
|
'steam': 'mdi:steam',
|
|
'gog': 'mdi:gog',
|
|
'epic': 'mdi:epic-games',
|
|
'origin': 'mdi:origin',
|
|
'ubisoft': 'mdi:ubisoft',
|
|
'battlenet': 'mdi:battle-net',
|
|
'ea': 'mdi:ea',
|
|
'humble': 'mdi:humble-bundle',
|
|
'itch': 'mdi:itch-io',
|
|
'browser': 'mdi:web',
|
|
'default': 'mdi:gamepad-variant'
|
|
} %}
|
|
|
|
{% set platform_lower = version.platform|lower %}
|
|
{% set platform_icon = 'default' %}
|
|
{% for key, icon in platform_icons %}
|
|
{% if key in platform_lower %}
|
|
{% set platform_icon = icon %}
|
|
{% endif %}
|
|
{% endfor %}
|
|
|
|
<li class="nav-item" role="presentation">
|
|
<button
|
|
class="nav-link inline-flex items-center px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 {{ loop.first ? 'active bg-blue-600 text-white shadow-lg' : 'text-slate-300 hover:bg-slate-600/50 hover:text-white' }} platform-tab border-0 bg-transparent"
|
|
id="tab-{{ safePlatformId }}"
|
|
data-bs-toggle="tab"
|
|
data-bs-target="#content-{{ safePlatformId }}"
|
|
type="button"
|
|
role="tab"
|
|
data-platform="{{ version.platform }}"
|
|
data-source="{{ version.source_id }}"
|
|
data-last-played="{{ version.last_played_at ? version.last_played_at|date('M d, Y') : 'Never' }}"
|
|
data-playtime="{{ version.playtime_minutes|format_duration }}"
|
|
data-completion="{{ version.completion_percentage ?? 0 }}"
|
|
data-cover-image="{{ version.cover_image_url ? '/images/playnite/' ~ version.cover_image_url : (main_game.image_url ? '/images/playnite/' ~ main_game.image_url : '') }}"
|
|
data-banner-url="{{ version.hero_image_url ? '/images/playnite/' ~ version.hero_image_url : '' }}"
|
|
>
|
|
<iconify-icon icon="{{ platform_icon }}" width="18" height="18" class="mr-2"></iconify-icon>
|
|
<span>{{ version.platform }}</span>
|
|
{% if version.source_name %}
|
|
<span class="text-slate-400 ml-1">({{ version.source_name }})</span>
|
|
{% endif %}
|
|
</button>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Stats Sidebar -->
|
|
<div class="lg:col-span-3">
|
|
<div class="bg-slate-800/50 backdrop-blur-sm rounded-xl p-6 border border-slate-700/50">
|
|
<h3 class="text-xl font-semibold text-white mb-4">Game Stats</h3>
|
|
<div class="space-y-4">
|
|
<div class="flex justify-between items-center">
|
|
<span class="text-slate-400 text-sm">Last Played</span>
|
|
<span class="text-white font-medium stats-last-played">{{ platform_versions[0].last_played_at ? platform_versions[0].last_played_at|date('M d, Y') : 'Never' }}</span>
|
|
</div>
|
|
<div class="flex justify-between items-center">
|
|
<span class="text-slate-400 text-sm">Playtime</span>
|
|
{% set total_playtime_minutes = 0 %}
|
|
{% for platform in platform_versions %}
|
|
{% if platform.playtime_minutes %}
|
|
{% set total_playtime_minutes = total_playtime_minutes + platform.playtime_minutes %}
|
|
{% endif %}
|
|
{% endfor %}
|
|
<span class="text-white font-medium stats-playtime">{{ total_playtime_minutes|format_duration }}</span>
|
|
</div>
|
|
<div class="stats-completion {{ platform_versions[0].completion_percentage > 0 ? '' : 'hidden' }}">
|
|
<div class="flex justify-between items-center mb-2">
|
|
<span class="text-slate-400 text-sm">Completion</span>
|
|
<span class="text-white font-medium stats-completion-percent">{{ platform_versions[0].completion_percentage }}%</span>
|
|
</div>
|
|
<div class="w-full bg-slate-700 rounded-full h-2">
|
|
<div class="bg-green-500 h-2 rounded-full stats-progress-bar transition-all duration-300"
|
|
style="width: {{ platform_versions[0].completion_percentage }}%"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% set metadata = platform_versions[0].metadata|json_decode %}
|
|
{% if metadata %}
|
|
<div class="border-t border-slate-600/50 mt-4 pt-4">
|
|
<h4 class="text-slate-300 font-medium mb-3">Platform Details</h4>
|
|
{% if metadata.appid %}
|
|
<div class="flex justify-between items-center">
|
|
<span class="text-slate-400 text-sm">App ID</span>
|
|
<code class="text-blue-400 bg-slate-700/50 px-2 py-1 rounded text-xs">{{ metadata.appid }}</code>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main Content -->
|
|
<div class="container mx-auto px-6 py-8">
|
|
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8">
|
|
<!-- Left Column -->
|
|
<div class="lg:col-span-8">
|
|
<!-- Tab Content -->
|
|
<div class="tab-content" id="platformTabsContent">
|
|
{% for version in platform_versions %}
|
|
{% set safePlatformId = version.platform|lower|replace({' ': '-', '(': '', ')': ''}) ~ '-' ~ version.source_id %}
|
|
<div
|
|
class="tab-pane fade{% if loop.first %} show active{% endif %}"
|
|
id="content-{{ safePlatformId }}"
|
|
role="tabpanel"
|
|
data-platform="{{ version.platform }}"
|
|
data-source="{{ version.source_id }}"
|
|
>
|
|
<!-- Game Description -->
|
|
{% if version.description %}
|
|
<div class="bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden mb-6">
|
|
<div class="p-6">
|
|
<h3 class="text-xl font-semibold text-gray-900 mb-4">About</h3>
|
|
<div class="game-description text-gray-700 leading-relaxed">
|
|
{{ version.description|nl2br }}
|
|
</div>
|
|
<button class="inline-flex items-center mt-3 px-3 py-2 text-sm font-medium text-blue-600 hover:text-blue-800 hover:bg-blue-50 rounded-lg transition-colors duration-200 read-more-btn">
|
|
<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="M19 9l-7 7-7-7"/>
|
|
</svg>
|
|
Read more
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Screenshots Section -->
|
|
{% if main_game.screenshots is not empty %}
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<h4 class="mb-0">Screenshots</h4>
|
|
</div>
|
|
<div class="card-body">
|
|
<div id="screenshotsCarousel" class="carousel slide" data-bs-ride="carousel">
|
|
<div class="carousel-inner rounded" style="max-height: 400px; overflow: hidden;">
|
|
{% for screenshot in main_game.screenshots %}
|
|
<div class="carousel-item {{ loop.first ? 'active' : '' }}">
|
|
<img src="/images/playnite/{{ screenshot }}" class="d-block w-100" alt="Screenshot {{ loop.index }}" style="object-fit: contain; width: 100%; height: 100%;">
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% if main_game.screenshots|length > 1 %}
|
|
<button class="carousel-control-prev" type="button" data-bs-target="#screenshotsCarousel" data-bs-slide="prev">
|
|
<span class="carousel-control-prev-icon bg-dark bg-opacity-50 rounded" aria-hidden="true"></span>
|
|
<span class="visually-hidden">Previous</span>
|
|
</button>
|
|
<button class="carousel-control-next" type="button" data-bs-target="#screenshotsCarousel" data-bs-slide="next">
|
|
<span class="carousel-control-next-icon bg-dark bg-opacity-50 rounded" aria-hidden="true"></span>
|
|
<span class="visually-hidden">Next</span>
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if main_game.screenshots|length > 1 %}
|
|
<div class="d-flex justify-content-center mt-3">
|
|
<div class="d-flex flex-wrap gap-2" style="max-width: 100%; overflow-x: auto; padding-bottom: 10px;">
|
|
{% for screenshot in main_game.screenshots %}
|
|
<img
|
|
src="/images/playnite/{{ screenshot }}"
|
|
class="screenshot-thumbnail {{ loop.first ? 'active' : '' }}"
|
|
style="width: 80px; height: 45px; object-fit: cover; cursor: pointer; opacity: 0.7; transition: opacity 0.2s;"
|
|
data-bs-target="#screenshotsCarousel"
|
|
data-bs-slide-to="{{ loop.index0 }}"
|
|
onmouseover="this.style.opacity='1'"
|
|
onmouseout="this.style.opacity=this.classList.contains('active') ? '1' : '0.7'"
|
|
>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Game Details - Split into Multiple Cards -->
|
|
<div class="space-y-6">
|
|
{# Basic Information Card #}
|
|
{% if version.developer or version.publisher or version.release_date or version.age_rating or version.region or version.version %}
|
|
<div class="bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden">
|
|
<div class="p-6">
|
|
<div class="flex items-center mb-4">
|
|
<svg class="w-5 h-5 text-blue-600 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
</svg>
|
|
<h3 class="text-lg font-semibold text-gray-900">Basic Information</h3>
|
|
</div>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{% if version.developer or (version.metadata and version.metadata.developers is defined and version.metadata.developers|length > 0) %}
|
|
<div class="space-y-1">
|
|
<h4 class="text-sm font-medium text-gray-600 uppercase tracking-wide">Developer</h4>
|
|
<p class="text-gray-900 font-medium">
|
|
{% if version.developer %}
|
|
{{ version.developer }}
|
|
{% elseif version.metadata and version.metadata.developers is defined and version.metadata.developers|length > 0 %}
|
|
{{ version.metadata.developers|join(', ') }}
|
|
{% endif %}
|
|
</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if version.publisher or (version.metadata and version.metadata.publishers is defined and version.metadata.publishers|length > 0) %}
|
|
<div class="space-y-1">
|
|
<h4 class="text-sm font-medium text-gray-600 uppercase tracking-wide">Publisher</h4>
|
|
<p class="text-gray-900 font-medium">
|
|
{% if version.publisher %}
|
|
{{ version.publisher }}
|
|
{% elseif version.metadata and version.metadata.publishers is defined and version.metadata.publishers|length > 0 %}
|
|
{{ version.metadata.publishers|join(', ') }}
|
|
{% endif %}
|
|
</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if version.release_date %}
|
|
<div class="space-y-1">
|
|
<h4 class="text-sm font-medium text-gray-600 uppercase tracking-wide">Release Date</h4>
|
|
<p class="text-gray-900 font-medium">{{ version.release_date|date('M d, Y') }}</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if version.age_rating %}
|
|
<div class="space-y-1">
|
|
<h4 class="text-sm font-medium text-gray-600 uppercase tracking-wide">Age Rating</h4>
|
|
<p class="text-gray-900 font-medium">{{ version.age_rating }}</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if version.region %}
|
|
<div class="space-y-1">
|
|
<h4 class="text-sm font-medium text-gray-600 uppercase tracking-wide">Region</h4>
|
|
<p class="text-gray-900 font-medium">{{ version.region }}</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if version.version %}
|
|
<div class="space-y-1">
|
|
<h4 class="text-sm font-medium text-gray-600 uppercase tracking-wide">Version</h4>
|
|
<p class="text-gray-900 font-medium">{{ version.version }}</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# Play Statistics Card #}
|
|
{% set has_play_stats = false %}
|
|
{% for platform_version in platform_versions %}
|
|
{% if platform_version.playtime_minutes is defined and platform_version.playtime_minutes > 0 %}
|
|
{% set has_play_stats = true %}
|
|
{% endif %}
|
|
{% endfor %}
|
|
{% if has_play_stats or version.play_count is not null or version.save_count is not null %}
|
|
<div class="bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden">
|
|
<div class="p-6">
|
|
<div class="flex items-center mb-4">
|
|
<svg class="w-5 h-5 text-green-600 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
|
|
</svg>
|
|
<h3 class="text-lg font-semibold text-gray-900">Play Statistics</h3>
|
|
</div>
|
|
<div class="space-y-4">
|
|
{# Play Time for All Platforms #}
|
|
<div class="space-y-2">
|
|
<h4 class="text-sm font-medium text-gray-600 uppercase tracking-wide">Play Time by Platform</h4>
|
|
<div class="space-y-1">
|
|
{% for platform_version in platform_versions %}
|
|
{% if platform_version.playtime_minutes is defined and platform_version.playtime_minutes > 0 %}
|
|
{% set hours = platform_version.playtime_minutes // 60 %}
|
|
{% set minutes = platform_version.playtime_minutes % 60 %}
|
|
<div class="flex justify-between items-center py-2 px-3 bg-gray-50 rounded-lg border border-gray-100">
|
|
<span class="text-sm font-medium text-gray-700">{{ platform_version.platform }}</span>
|
|
<span class="text-sm font-semibold text-gray-900 bg-green-100 text-green-800 px-2 py-1 rounded">
|
|
{% if hours > 0 %}{{ hours }}h {% endif %}{% if minutes > 0 or hours == 0 %}{{ minutes }}m{% endif %}
|
|
</span>
|
|
</div>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
{% if version.play_count is not null %}
|
|
<div class="flex justify-between items-center py-2">
|
|
<h4 class="text-sm font-medium text-gray-600 uppercase tracking-wide">Play Count</h4>
|
|
<span class="text-lg font-bold text-blue-600">{{ version.play_count }}</span>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if version.save_count is not null %}
|
|
<div class="flex justify-between items-center py-2">
|
|
<h4 class="text-sm font-medium text-gray-600 uppercase tracking-wide">Save Files</h4>
|
|
<span class="text-lg font-bold text-purple-600">{{ version.save_count }}</span>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# Technical Information Card #}
|
|
{% if version.install_size or version.completion_status %}
|
|
<div class="bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden">
|
|
<div class="p-6">
|
|
<div class="flex items-center mb-4">
|
|
<svg class="w-5 h-5 text-purple-600 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
|
</svg>
|
|
<h3 class="text-lg font-semibold text-gray-900">Technical Information</h3>
|
|
</div>
|
|
<div class="space-y-4">
|
|
{% if version.install_size %}
|
|
<div class="flex justify-between items-center py-2">
|
|
<h4 class="text-sm font-medium text-gray-600 uppercase tracking-wide">Install Size</h4>
|
|
<span class="text-lg font-bold text-orange-600">
|
|
{% if version.install_size >= 1073741824 %}
|
|
{{ (version.install_size / 1073741824)|number_format(1) }} GB
|
|
{% else %}
|
|
{{ (version.install_size / 1048576)|number_format(0) }} MB
|
|
{% endif %}
|
|
</span>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if version.completion_status %}
|
|
<div class="space-y-2">
|
|
<h4 class="text-sm font-medium text-gray-600 uppercase tracking-wide">Completion Status</h4>
|
|
<div class="flex items-center space-x-3">
|
|
{% set completionStatus = version.completion_status|lower %}
|
|
{% if completionStatus == 'completed' %}
|
|
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-100 text-green-800">
|
|
<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="M5 13l4 4L19 7"/>
|
|
</svg>
|
|
Completed
|
|
</span>
|
|
{% elseif completionStatus == 'playing' %}
|
|
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800">
|
|
<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="M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-6 8a9 9 0 110-18 9 9 0 010 18z"/>
|
|
</svg>
|
|
In Progress
|
|
</span>
|
|
{% elseif completionStatus == 'notplayed' %}
|
|
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-gray-100 text-gray-800">
|
|
<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="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
</svg>
|
|
Not Played
|
|
</span>
|
|
{% else %}
|
|
<span class="text-gray-900 font-medium">{{ completionStatus|capitalize }}</span>
|
|
{% endif %}
|
|
|
|
{% if version.completion_percentage is not null %}
|
|
<span class="text-sm font-semibold text-gray-700 bg-gray-100 px-2 py-1 rounded">
|
|
{{ version.completion_percentage }}%
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# Library Information Card #}
|
|
{% if version.added_at or version.modified_at or version.last_played_at %}
|
|
<div class="bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden">
|
|
<div class="p-6">
|
|
<div class="flex items-center mb-4">
|
|
<svg class="w-5 h-5 text-indigo-600 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.746 0 3.332.477 4.5 1.253v13C19.832 18.477 18.246 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/>
|
|
</svg>
|
|
<h3 class="text-lg font-semibold text-gray-900">Library Information</h3>
|
|
</div>
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
{% if version.added_at %}
|
|
<div class="space-y-1">
|
|
<h4 class="text-sm font-medium text-gray-600 uppercase tracking-wide">Added to Library</h4>
|
|
<p class="text-gray-900 font-medium">{{ version.added_at|date('M d, Y') }}</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if version.modified_at %}
|
|
<div class="space-y-1">
|
|
<h4 class="text-sm font-medium text-gray-600 uppercase tracking-wide">Last Modified</h4>
|
|
<p class="text-gray-900 font-medium">{{ version.modified_at|date('M d, Y') }}</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if version.last_played_at %}
|
|
<div class="space-y-1">
|
|
<h4 class="text-sm font-medium text-gray-600 uppercase tracking-wide">Last Played</h4>
|
|
<p class="text-gray-900 font-medium">{{ version.last_played_at|date('M d, Y') }}</p>
|
|
{% if version.last_activity %}
|
|
<p class="text-sm text-gray-600">{{ version.last_activity }}</p>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# Metadata & Tags Card #}
|
|
{% if version.metadata is defined and version.metadata is not empty %}
|
|
{% if version.metadata is iterable %}
|
|
{% set metadata = version.metadata %}
|
|
{% else %}
|
|
{% set metadata = version.metadata|json_decode(true) %}
|
|
{% if metadata is null or metadata is not iterable %}
|
|
{% set metadata = [] %}
|
|
{% endif %}
|
|
{% endif %}
|
|
|
|
{% set has_metadata = false %}
|
|
{% if metadata.genres is defined and metadata.genres is iterable and metadata.genres|length > 0 %}{% set has_metadata = true %}{% endif %}
|
|
{% if metadata.tags is defined and metadata.tags is iterable and metadata.tags|length > 0 %}{% set has_metadata = true %}{% endif %}
|
|
{% if metadata.features is defined and metadata.features is iterable and metadata.features|length > 0 %}{% set has_metadata = true %}{% endif %}
|
|
{% if metadata.platforms is defined and metadata.platforms is iterable and metadata.platforms|length > 0 %}{% set has_metadata = true %}{% endif %}
|
|
{% if metadata.series is defined and metadata.series is iterable and metadata.series|length > 0 %}{% set has_metadata = true %}{% endif %}
|
|
{% if metadata.source is defined and metadata.source is not empty %}{% set has_metadata = true %}{% endif %}
|
|
{% if metadata.user_score is defined and metadata.user_score is not empty %}{% set has_metadata = true %}{% endif %}
|
|
{% if metadata.community_score is defined and metadata.community_score is not empty %}{% set has_metadata = true %}{% endif %}
|
|
{% if metadata.critic_score is defined and metadata.critic_score is not empty %}{% set has_metadata = true %}{% endif %}
|
|
{% if metadata.rating is defined and metadata.rating is not empty %}{% set has_metadata = true %}{% endif %}
|
|
{% if metadata.release_date is defined and metadata.release_date is not empty %}{% set has_metadata = true %}{% endif %}
|
|
{% if metadata.links is defined and metadata.links is iterable and metadata.links|length > 0 %}{% set has_metadata = true %}{% endif %}
|
|
|
|
{% if has_metadata %}
|
|
<div class="bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden">
|
|
<div class="p-6">
|
|
<div class="flex items-center mb-4">
|
|
<svg class="w-5 h-5 text-pink-600 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"/>
|
|
</svg>
|
|
<h3 class="text-lg font-semibold text-gray-900">Tags & Metadata</h3>
|
|
</div>
|
|
<div class="space-y-4">
|
|
{# Genres #}
|
|
{% if metadata.genres is defined and metadata.genres is iterable and metadata.genres|length > 0 %}
|
|
<div class="space-y-2">
|
|
<h4 class="text-sm font-medium text-gray-600 uppercase tracking-wide">Genres</h4>
|
|
<div class="flex flex-wrap gap-2">
|
|
{% for genre in metadata.genres %}
|
|
{% if genre is not empty %}
|
|
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800 border border-blue-200">
|
|
<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="M7 4V2a1 1 0 011-1h8a1 1 0 011 1v2m-9 0h10m-9 0V1m10 3V1m0 3l1 1v16a2 2 0 01-2 2H6a2 2 0 01-2-2V5l1-1z"/>
|
|
</svg>
|
|
{{ genre }}
|
|
</span>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# Tags (filtered) #}
|
|
{% if metadata.tags is defined and metadata.tags is iterable and metadata.tags|length > 0 %}
|
|
{% set filteredTags = metadata.tags|filter(tag => tag is not empty and not (tag starts with '[' and ']' in tag)) %}
|
|
{% if filteredTags|length > 0 %}
|
|
<div class="space-y-2">
|
|
<h4 class="text-sm font-medium text-gray-600 uppercase tracking-wide">Tags</h4>
|
|
<div class="flex flex-wrap gap-2">
|
|
{% for tag in filteredTags %}
|
|
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-gray-100 text-gray-800 border border-gray-200">
|
|
<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="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"/>
|
|
</svg>
|
|
{{ tag }}
|
|
</span>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
{% endif %}
|
|
|
|
{# Features #}
|
|
{% if metadata.features is defined and metadata.features is iterable and metadata.features|length > 0 %}
|
|
<div class="space-y-2">
|
|
<h4 class="text-sm font-medium text-gray-600 uppercase tracking-wide">Features</h4>
|
|
<div class="flex flex-wrap gap-2">
|
|
{% for feature in metadata.features %}
|
|
{% if feature is not empty %}
|
|
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-100 text-green-800 border border-green-200">
|
|
<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="M13 10V3L4 14h7v7l9-11h-7z"/>
|
|
</svg>
|
|
{{ feature }}
|
|
</span>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# Series #}
|
|
{% if metadata.series is defined and metadata.series is iterable and metadata.series|length > 0 %}
|
|
<div class="space-y-2">
|
|
<h4 class="text-sm font-medium text-gray-600 uppercase tracking-wide">Series</h4>
|
|
<div class="flex flex-wrap gap-2">
|
|
{% for series in metadata.series %}
|
|
{% if series is not empty %}
|
|
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-purple-100 text-purple-800 border border-purple-200">
|
|
<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="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
|
|
</svg>
|
|
{{ series }}
|
|
</span>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# Available Platforms #}
|
|
{% if metadata.platforms is defined and metadata.platforms is iterable and metadata.platforms|length > 0 %}
|
|
<div class="space-y-2">
|
|
<h4 class="text-sm font-medium text-gray-600 uppercase tracking-wide">Available Platforms</h4>
|
|
<div class="flex flex-wrap gap-2">
|
|
{% for platform in metadata.platforms %}
|
|
{% if platform is not empty %}
|
|
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-indigo-100 text-indigo-800 border border-indigo-200">
|
|
<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="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>
|
|
{{ platform }}
|
|
</span>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# Scores Row #}
|
|
{% if metadata.user_score is defined and metadata.user_score is not empty or metadata.community_score is defined and metadata.community_score is not empty or metadata.critic_score is defined and metadata.critic_score is not empty %}
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 pt-2 border-t border-gray-100">
|
|
{% if metadata.user_score is defined and metadata.user_score is not empty %}
|
|
<div class="space-y-2">
|
|
<h4 class="text-sm font-medium text-gray-600 uppercase tracking-wide">User Score</h4>
|
|
<div class="flex items-center space-x-2">
|
|
<div class="flex-1 bg-gray-200 rounded-full h-2">
|
|
<div class="bg-yellow-500 h-2 rounded-full" style="width: {{ metadata.user_score * 10 }}%"></div>
|
|
</div>
|
|
<span class="text-sm font-medium text-gray-700">{{ metadata.user_score }}/10</span>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if metadata.community_score is defined and metadata.community_score is not empty %}
|
|
<div class="space-y-2">
|
|
<h4 class="text-sm font-medium text-gray-600 uppercase tracking-wide">Community Score</h4>
|
|
<div class="flex items-center space-x-2">
|
|
<div class="flex-1 bg-gray-200 rounded-full h-2">
|
|
<div class="bg-blue-500 h-2 rounded-full" style="width: {{ metadata.community_score }}%"></div>
|
|
</div>
|
|
<span class="text-sm font-medium text-gray-700">{{ metadata.community_score }}%</span>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if metadata.critic_score is defined and metadata.critic_score is not empty %}
|
|
<div class="space-y-2">
|
|
<h4 class="text-sm font-medium text-gray-600 uppercase tracking-wide">Critic Score</h4>
|
|
<div class="flex items-center space-x-2">
|
|
<div class="flex-1 bg-gray-200 rounded-full h-2">
|
|
<div class="bg-green-500 h-2 rounded-full" style="width: {{ metadata.critic_score }}%"></div>
|
|
</div>
|
|
<span class="text-sm font-medium text-gray-700">{{ metadata.critic_score }}%</span>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# Rating #}
|
|
{% if metadata.rating is defined and metadata.rating is not empty %}
|
|
<div class="space-y-2">
|
|
<h4 class="text-sm font-medium text-gray-600 uppercase tracking-wide">Rating</h4>
|
|
<p class="text-gray-900 font-medium">
|
|
{% if metadata.rating == 'RP' %}
|
|
Rating Pending
|
|
{% elseif metadata.rating == 'EC' %}
|
|
Early Childhood
|
|
{% elseif metadata.rating == 'E' %}
|
|
Everyone
|
|
{% elseif metadata.rating == 'E10+' %}
|
|
Everyone 10+
|
|
{% elseif metadata.rating == 'T' %}
|
|
Teen
|
|
{% elseif metadata.rating == 'M' %}
|
|
Mature 17+
|
|
{% elseif metadata.rating == 'AO' %}
|
|
Adults Only 18+
|
|
{% else %}
|
|
{{ metadata.rating }}
|
|
{% endif %}
|
|
{% if metadata.rating_description is defined and metadata.rating_description is not empty %}
|
|
<small class="text-gray-600 block mt-1">{{ metadata.rating_description }}</small>
|
|
{% endif %}
|
|
</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# Release Date #}
|
|
{% if metadata.release_date is defined and metadata.release_date is not empty %}
|
|
<div class="space-y-2">
|
|
<h4 class="text-sm font-medium text-gray-600 uppercase tracking-wide">Original Release</h4>
|
|
<p class="text-gray-900 font-medium">
|
|
{{ metadata.release_date }}
|
|
{% if metadata.release_year is defined and metadata.release_year is not empty %}
|
|
<small class="text-gray-600">({{ metadata.release_year }})</small>
|
|
{% endif %}
|
|
</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# Links #}
|
|
{% if metadata.links is defined and metadata.links is iterable and metadata.links|length > 0 %}
|
|
<div class="space-y-2">
|
|
<h4 class="text-sm font-medium text-gray-600 uppercase tracking-wide">Links</h4>
|
|
<div class="flex flex-wrap gap-2">
|
|
{% for link in metadata.links %}
|
|
{% if link.name is defined and link.url is defined %}
|
|
<a href="{{ link.url }}" target="_blank" class="inline-flex items-center px-3 py-2 border border-gray-300 rounded-lg text-sm text-gray-700 hover:bg-gray-50 hover:border-gray-400 transition-colors duration-200">
|
|
<svg class="w-4 h-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
|
|
</svg>
|
|
{{ link.name }}
|
|
</a>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right Column -->
|
|
<div class="lg:col-span-4 space-y-6">
|
|
<!-- Platform Versions -->
|
|
<div class="bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden">
|
|
<div class="p-6">
|
|
<h3 class="text-xl font-semibold text-gray-900 mb-4">Available Platforms</h3>
|
|
<div class="space-y-3">
|
|
{% for version in platform_versions %}
|
|
<div class="flex justify-between items-center py-2 border-b border-gray-100 last:border-b-0">
|
|
<div>
|
|
<h4 class="font-medium text-gray-900">{{ version.platform }}</h4>
|
|
{% if version.source_name %}
|
|
<p class="text-sm text-gray-600">{{ version.source_name }}</p>
|
|
{% endif %}
|
|
</div>
|
|
{% if version.is_installed %}
|
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">Installed</span>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Game Links -->
|
|
{% set playniteLinks = platform_versions[0].links_json|json_decode %}
|
|
{% if playniteLinks %}
|
|
<div class="bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden">
|
|
<div class="p-6">
|
|
<h3 class="text-xl font-semibold text-gray-900 mb-4">Links</h3>
|
|
<div class="space-y-2">
|
|
{% for link in playniteLinks %}
|
|
<a href="{{ link.Url }}" target="_blank" class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 hover:border-gray-400 transition-colors duration-200">
|
|
<svg class="w-4 h-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
|
|
</svg>
|
|
{{ link.Name }}
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
/* Tab functionality - ensure inactive tabs are hidden */
|
|
.tab-pane {
|
|
display: none;
|
|
}
|
|
|
|
.tab-pane.show.active {
|
|
display: block;
|
|
}
|
|
|
|
.game-hero {
|
|
padding: 2rem 0;
|
|
position: relative;
|
|
margin-bottom: 2rem;
|
|
background-size: cover !important;
|
|
background-position: center !important;
|
|
}
|
|
|
|
.game-hero::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0.6) 100%);
|
|
z-index: 0;
|
|
}
|
|
|
|
.game-hero .container {
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
.game-description {
|
|
max-height: 150px;
|
|
overflow: hidden;
|
|
position: relative;
|
|
transition: max-height 0.3s ease;
|
|
}
|
|
|
|
.game-description.expanded {
|
|
max-height: 1000px;
|
|
}
|
|
|
|
.game-description::after {
|
|
content: '';
|
|
position: absolute;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 50px;
|
|
background: linear-gradient(to bottom, transparent, var(--bs-body-bg, #fff));
|
|
pointer-events: none;
|
|
transition: opacity 0.3s ease;
|
|
}
|
|
|
|
.game-description.expanded::after {
|
|
opacity: 0;
|
|
}
|
|
|
|
.nav-tabs {
|
|
--bs-nav-tabs-border-width: 0;
|
|
gap: 0.5rem;
|
|
margin-bottom: 1rem;
|
|
flex-wrap: wrap;
|
|
padding-bottom: 4px;
|
|
align-items: center;
|
|
}
|
|
|
|
/* Only show scrollbar on mobile */
|
|
@media (max-width: 991.98px) {
|
|
.nav-tabs {
|
|
flex-wrap: nowrap;
|
|
overflow-x: auto;
|
|
scrollbar-width: thin;
|
|
}
|
|
|
|
.nav-tabs::-webkit-scrollbar {
|
|
height: 4px;
|
|
}
|
|
|
|
.nav-tabs::-webkit-scrollbar-thumb {
|
|
background-color: rgba(255, 255, 255, 0.2);
|
|
border-radius: 2px;
|
|
}
|
|
}
|
|
|
|
.nav-tabs .nav-link {
|
|
color: rgba(255, 255, 255, 0.7);
|
|
padding: 0.5rem 1rem;
|
|
border: none;
|
|
border-bottom: 2px solid transparent;
|
|
border-radius: 4px 4px 0 0;
|
|
transition: all 0.2s ease;
|
|
white-space: nowrap;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
background: rgba(255, 255, 255, 0.05);
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.nav-tabs .nav-link:hover:not(.active) {
|
|
color: white;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-color: rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
.nav-tabs .nav-link.active {
|
|
color: white;
|
|
background: rgba(13, 110, 253, 0.15);
|
|
border-color: var(--bs-primary, #0d6efd);
|
|
font-weight: 500;
|
|
}
|
|
|
|
/* Platform icon colors */
|
|
.nav-tabs .nav-link iconify-icon[icon^="mdi:microsoft-windows"] {
|
|
color: #00a4ef;
|
|
}
|
|
|
|
.nav-tabs .nav-link iconify-icon[icon^="mdi:sony-playstation"] {
|
|
color: #003087;
|
|
}
|
|
|
|
.nav-tabs .nav-link iconify-icon[icon^="mdi:microsoft-xbox"] {
|
|
color: #107c10;
|
|
}
|
|
|
|
.nav-tabs .nav-link iconify-icon[icon^="mdi:nintendo-switch"],
|
|
.nav-tabs .nav-link iconify-icon[icon^="mdi:nintendo-wii"] {
|
|
color: #e60012;
|
|
}
|
|
|
|
.nav-tabs .nav-link iconify-icon[icon^="mdi:steam"] {
|
|
color: #00adee;
|
|
}
|
|
|
|
.nav-tabs .nav-link iconify-icon[icon^="mdi:apple"],
|
|
.nav-tabs .nav-link iconify-icon[icon^="mdi:apple-ios"] {
|
|
color: #a2aaad;
|
|
}
|
|
|
|
.nav-tabs .nav-link iconify-icon[icon^="mdi:android"] {
|
|
color: #3ddc84;
|
|
}
|
|
|
|
.nav-tabs .nav-link iconify-icon[icon^="mdi:linux"] {
|
|
color: #fcc624;
|
|
}
|
|
|
|
.nav-tabs .nav-link iconify-icon[icon^="mdi:gamepad"] {
|
|
color: #8e8e93;
|
|
}
|
|
|
|
/* Screenshot carousel styles */
|
|
.carousel-item {
|
|
height: 400px;
|
|
background-color: #1a1a1a;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.screenshot-thumbnail {
|
|
border: 2px solid transparent;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.screenshot-thumbnail:hover,
|
|
.screenshot-thumbnail.active {
|
|
border-color: var(--bs-primary);
|
|
opacity: 1 !important;
|
|
}
|
|
|
|
/* Custom scrollbar for thumbnails */
|
|
.d-flex.flex-wrap.gap-2::-webkit-scrollbar {
|
|
height: 6px;
|
|
}
|
|
|
|
.d-flex.flex-wrap.gap-2::-webkit-scrollbar-thumb {
|
|
background-color: rgba(255, 255, 255, 0.2);
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.card {
|
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
.card-title {
|
|
font-weight: 600;
|
|
color: var(--bs-heading-color, #212529);
|
|
}
|
|
|
|
.breadcrumb {
|
|
background: transparent;
|
|
padding: 0;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.breadcrumb-item a {
|
|
text-decoration: none;
|
|
}
|
|
|
|
.breadcrumb-item.active {
|
|
color: white;
|
|
}
|
|
|
|
.list-group-item {
|
|
border-left: none;
|
|
border-right: none;
|
|
padding: 0.75rem 0;
|
|
}
|
|
|
|
.list-group-item:first-child {
|
|
border-top: none;
|
|
padding-top: 0;
|
|
}
|
|
|
|
.list-group-item:last-child {
|
|
border-bottom: none;
|
|
padding-bottom: 0;
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Initialize screenshot thumbnails
|
|
initScreenshotThumbnails();
|
|
|
|
// Initialize all tooltips
|
|
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
|
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
|
return new bootstrap.Tooltip(tooltipTriggerEl);
|
|
});
|
|
|
|
// Function to update game cover image
|
|
function updateGameCover(imageUrl) {
|
|
const coverContainer = document.getElementById('gameCover');
|
|
if (!imageUrl) {
|
|
coverContainer.innerHTML = `
|
|
<div class="bg-dark rounded-3 d-flex align-items-center justify-content-center" style="width: 200px; height: 300px;">
|
|
<i class="bi bi-controller text-muted" style="font-size: 3rem;"></i>
|
|
</div>`;
|
|
return;
|
|
}
|
|
|
|
coverContainer.innerHTML = `
|
|
<img src="${imageUrl}"
|
|
class="img-fluid rounded-3 shadow-lg game-cover"
|
|
alt="${document.title}"
|
|
style="max-height: 300px; width: auto;">`;
|
|
}
|
|
|
|
// Function to update hero background
|
|
function updateHeroBackground(imageUrl) {
|
|
const hero = document.getElementById('gameHero');
|
|
if (imageUrl) {
|
|
hero.style.backgroundImage = `linear-gradient(rgba(0,0,0,0.7), rgba(0,0,0,0.7)), url('${imageUrl}')`;
|
|
} else {
|
|
hero.style.backgroundImage = "linear-gradient(rgba(0,0,0,0.7), rgba(0,0,0,0.7)), url('/images/hero-bg.jpg')";
|
|
}
|
|
}
|
|
|
|
// Function to update stats in the sidebar
|
|
function updateStats(platformData) {
|
|
// Update last played
|
|
const lastPlayedEl = document.querySelector('.stats-last-played');
|
|
if (lastPlayedEl) {
|
|
lastPlayedEl.textContent = platformData.lastPlayed || 'Never';
|
|
}
|
|
|
|
// Update playtime
|
|
const playtimeEl = document.querySelector('.stats-playtime');
|
|
if (playtimeEl) {
|
|
playtimeEl.textContent = platformData.playtime || '0h 0m';
|
|
}
|
|
|
|
// Update completion
|
|
const completionEl = document.querySelector('.stats-completion');
|
|
const completionPctEl = document.querySelector('.stats-completion-percent');
|
|
const progressBar = document.querySelector('.stats-progress-bar');
|
|
|
|
if (platformData.completionPercentage > 0) {
|
|
if (completionEl) completionEl.style.display = 'flex';
|
|
if (completionPctEl) completionPctEl.textContent = `${platformData.completionPercentage}%`;
|
|
if (progressBar) progressBar.style.width = `${platformData.completionPercentage}%`;
|
|
} else {
|
|
if (completionEl) completionEl.style.display = 'none';
|
|
}
|
|
|
|
// Update cover image if available
|
|
if (platformData.coverImage) {
|
|
updateGameCover(platformData.coverImage);
|
|
} else {
|
|
// Fallback to main game image if no platform-specific image
|
|
updateGameCover('{{ main_game.image_url ? "/images/playnite/" ~ main_game.image_url : "" }}');
|
|
}
|
|
|
|
// Update hero background if available
|
|
if (platformData.heroImage) {
|
|
updateHeroBackground(platformData.heroImage);
|
|
} else {
|
|
updateHeroBackground('');
|
|
}
|
|
}
|
|
|
|
// Manual tab switching since Bootstrap JS may not be loaded
|
|
const platformTabs = document.querySelectorAll('.platform-tab');
|
|
const tabPanes = document.querySelectorAll('.tab-pane');
|
|
|
|
platformTabs.forEach(tab => {
|
|
tab.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
|
|
// Remove active class from all tabs
|
|
platformTabs.forEach(t => {
|
|
t.classList.remove('active', 'bg-blue-600', 'text-white', 'shadow-lg');
|
|
t.classList.add('text-slate-300');
|
|
});
|
|
|
|
// Add active class to clicked tab
|
|
this.classList.add('active', 'bg-blue-600', 'text-white', 'shadow-lg');
|
|
this.classList.remove('text-slate-300');
|
|
|
|
// Hide all tab panes
|
|
tabPanes.forEach(pane => {
|
|
pane.classList.remove('show', 'active');
|
|
pane.style.display = 'none';
|
|
});
|
|
|
|
// Show the target tab pane
|
|
const targetId = this.getAttribute('data-bs-target');
|
|
const targetPane = document.querySelector(targetId);
|
|
if (targetPane) {
|
|
targetPane.classList.add('show', 'active');
|
|
targetPane.style.display = 'block';
|
|
}
|
|
|
|
// Get the platform data from the tab and update stats
|
|
const platformData = {
|
|
lastPlayed: this.dataset.lastPlayed || 'Never',
|
|
playtime: this.dataset.playtime || '0h 0m',
|
|
completionPercentage: parseInt(this.dataset.completion || '0'),
|
|
coverImage: this.dataset.coverImage || '',
|
|
heroImage: this.dataset.banner_url || ''
|
|
};
|
|
|
|
// Update the UI with platform-specific data
|
|
updateStats(platformData);
|
|
});
|
|
});
|
|
|
|
// Read more/less functionality
|
|
const readMoreBtns = document.querySelectorAll('.read-more-btn');
|
|
readMoreBtns.forEach(btn => {
|
|
const description = btn.previousElementSibling;
|
|
|
|
btn.addEventListener('click', function() {
|
|
description.classList.toggle('expanded');
|
|
this.textContent = description.classList.contains('expanded') ? 'Show less' : 'Read more';
|
|
});
|
|
});
|
|
});
|
|
</script>
|
|
|
|
{% endblock %}
|