dont know ?

This commit is contained in:
Lars Behrends
2025-11-03 23:34:36 +01:00
parent 7a7977d8b0
commit 1ec6016b10
27 changed files with 6854 additions and 3361 deletions

View File

@@ -21,16 +21,22 @@ class ActorController extends Controller
{
$actorId = $args['id'];
// Get actor details with counts from all media types
// Get actor details with counts from all media types (including episode actors)
$stmt = $this->pdo->prepare("
SELECT a.*,
COUNT(DISTINCT am.movie_id) as movie_count,
COUNT(DISTINCT ats.tv_show_id) as tv_show_count,
COUNT(DISTINCT CASE WHEN ats.tv_show_id IS NOT NULL THEN ats.tv_show_id
WHEN ate.tv_episode_id IS NOT NULL THEN te.tv_show_id END) as tv_show_count,
COUNT(DISTINCT aav.adult_video_id) as adult_video_count,
(COUNT(DISTINCT am.movie_id) + COUNT(DISTINCT ats.tv_show_id) + COUNT(DISTINCT aav.adult_video_id)) as total_media_count
(COUNT(DISTINCT am.movie_id) +
COUNT(DISTINCT CASE WHEN ats.tv_show_id IS NOT NULL THEN ats.tv_show_id
WHEN ate.tv_episode_id IS NOT NULL THEN te.tv_show_id END) +
COUNT(DISTINCT aav.adult_video_id)) as total_media_count
FROM actors a
LEFT JOIN actor_movie am ON a.id = am.actor_id
LEFT JOIN actor_tv_show ats ON a.id = ats.actor_id
LEFT JOIN actor_tv_episode ate ON a.id = ate.actor_id
LEFT JOIN tv_episodes te ON ate.tv_episode_id = te.id
LEFT JOIN actor_adult_video aav ON a.id = aav.actor_id
WHERE a.id = :actor_id
GROUP BY a.id
@@ -66,16 +72,18 @@ class ActorController extends Controller
$stmt->execute(['actor_id' => $actorId]);
$movies = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Get actor's TV shows
// Get actor's TV shows (from main cast and episodes)
$stmt = $this->pdo->prepare("
SELECT ts.*, s.display_name as source_name
SELECT DISTINCT ts.*, s.display_name as source_name
FROM tv_shows ts
JOIN sources s ON ts.source_id = s.id
JOIN actor_tv_show ats ON ts.id = ats.tv_show_id
WHERE ats.actor_id = :actor_id
LEFT JOIN actor_tv_show ats ON ts.id = ats.tv_show_id AND ats.actor_id = :actor_id
LEFT JOIN tv_episodes te ON ts.id = te.tv_show_id
LEFT JOIN actor_tv_episode ate ON te.id = ate.tv_episode_id AND ate.actor_id = :actor_id2
WHERE ats.actor_id = :actor_id4 OR ate.actor_id = :actor_id3
ORDER BY ts.first_air_date DESC, ts.title ASC
");
$stmt->execute(['actor_id' => $actorId]);
$stmt->execute(['actor_id' => $actorId,'actor_id2' => $actorId,'actor_id3' => $actorId, 'actor_id4' => $actorId]);
$tvShows = $stmt->fetchAll(PDO::FETCH_ASSOC);
@@ -108,33 +116,314 @@ class ActorController extends Controller
'tv_shows' => $tvShows
]);
}
public function edit(Request $request, Response $response, $args)
{
$actorId = $args['id'];
// Get actor details
$stmt = $this->pdo->prepare("SELECT * FROM actors WHERE id = :id");
$stmt->execute(['id' => $actorId]);
$actor = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$actor) {
return $response->withStatus(404)->withHeader('Content-Type', 'text/html');
}
// Decode metadata for form population
$metadata = json_decode($actor['metadata'] ?? '{}', true);
// Handle POST request (form submission)
if ($request->getMethod() === 'POST') {
$data = $request->getParsedBody();
$uploadedFiles = $request->getUploadedFiles();
// Validate required fields
$name = trim($data['name'] ?? '');
if (empty($name)) {
return $this->view->render($response, 'actor/edit.twig', [
'title' => 'Edit Actor',
'actor' => $actor,
'metadata' => $metadata,
'error' => 'Name is required'
]);
}
// Handle image upload
$thumbnailPath = $actor['thumbnail_path']; // Keep existing by default
if (!empty($uploadedFiles['thumbnail']) && $uploadedFiles['thumbnail']->getError() === UPLOAD_ERR_OK) {
$uploadedFile = $uploadedFiles['thumbnail'];
// Validate file type
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!in_array($uploadedFile->getClientMediaType(), $allowedTypes)) {
return $this->view->render($response, 'actor/edit.twig', [
'title' => 'Edit Actor',
'actor' => $actor,
'metadata' => $metadata,
'error' => 'Invalid image type. Only JPEG, PNG, GIF, and WebP are allowed.'
]);
}
// Generate filename and move file
$extension = pathinfo($uploadedFile->getClientFilename(), PATHINFO_EXTENSION);
$filename = 'actor_' . $actorId . '_' . time() . '.' . $extension;
$uploadPath = __DIR__ . '/../../public/images/actors/' . $filename;
// Create directory if it doesn't exist
$uploadDir = dirname($uploadPath);
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$uploadedFile->moveTo($uploadPath);
$thumbnailPath = '/images/actors/' . $filename;
}
// Prepare metadata
$actorMetadata = [
'biography' => trim($data['biography'] ?? ''),
'birth_date' => trim($data['birth_date'] ?? ''),
'death_date' => trim($data['death_date'] ?? ''),
'birth_place' => trim($data['birth_place'] ?? ''),
'nationality' => trim($data['nationality'] ?? ''),
'gender' => trim($data['gender'] ?? ''),
'ethnicity' => trim($data['ethnicity'] ?? ''),
'country' => trim($data['nationality'] ?? ''), // Map nationality to country for Stash compatibility
'height' => trim($data['height'] ?? ''),
'measurements' => trim($data['measurements'] ?? ''),
'cup_size' => trim($data['cup_size'] ?? ''),
'piercings' => trim($data['piercings'] ?? ''),
'tattoos' => trim($data['tattoos'] ?? ''),
'hair_color' => trim($data['hair_color'] ?? ''),
'eye_color' => trim($data['eye_color'] ?? ''),
'weight' => trim($data['weight'] ?? ''),
'fake_tits' => trim($data['fake_tits'] ?? ''),
'penis_length' => trim($data['penis_length'] ?? ''),
'circumcised' => trim($data['circumcised'] ?? ''),
'career_length' => trim($data['career_length'] ?? ''),
'aliases' => array_filter(array_map('trim', explode(',', $data['aliases'] ?? ''))),
'favorite' => isset($data['favorite']) ? true : false,
'ignore_auto_tag' => isset($data['ignore_auto_tag']) ? true : false,
'scene_count' => (int)($data['scene_count'] ?? 0),
'details' => trim($data['details'] ?? ''),
'social_media' => [
'twitter' => trim($data['twitter'] ?? ''),
'instagram' => trim($data['instagram'] ?? ''),
'onlyfans' => trim($data['onlyfans'] ?? ''),
'website' => trim($data['website'] ?? '')
],
'adult_specific' => [
'debut_year' => trim($data['debut_year'] ?? ''),
'retirement_year' => trim($data['retirement_year'] ?? ''),
'active' => isset($data['active']) ? true : false,
'genres' => array_filter(array_map('trim', explode(',', $data['adult_genres'] ?? ''))),
'specialties' => array_filter(array_map('trim', explode(',', $data['specialties'] ?? '')))
]
];
// Update actor
$stmt = $this->pdo->prepare("
UPDATE actors
SET name = :name, thumbnail_path = :thumbnail_path, metadata = :metadata, updated_at = NOW()
WHERE id = :id
");
$stmt->execute([
'id' => $actorId,
'name' => $name,
'thumbnail_path' => $thumbnailPath,
'metadata' => json_encode($actorMetadata)
]);
// Redirect back to actor show page
return $response->withHeader('Location', '/media/actors/' . $actorId)->withStatus(302);
}
// GET request - show edit form
return $this->view->render($response, 'actor/edit.twig', [
'title' => 'Edit Actor',
'actor' => $actor,
'metadata' => $metadata
]);
}
public function index(Request $request, Response $response, $args)
{
// Get all actors with their media counts from all types
$stmt = $this->pdo->prepare("
SELECT a.*,
COUNT(DISTINCT aav.adult_video_id) as adult_video_count,
COUNT(DISTINCT am.movie_id) as movie_count,
COUNT(DISTINCT ats.tv_show_id) as tv_show_count,
(COUNT(DISTINCT aav.adult_video_id) + COUNT(DISTINCT am.movie_id) + COUNT(DISTINCT ats.tv_show_id)) as total_media_count,
MAX(COALESCE(av.release_date, m.release_date, ts.first_air_date)) as latest_media_date
$queryParams = $request->getQueryParams();
// Get pagination parameters
$page = max(1, (int)($queryParams['page'] ?? 1));
$perPage = max(12, min(100, (int)($queryParams['per_page'] ?? 24)));
// Get search parameters
$search = trim($queryParams['search'] ?? '');
// Get filter parameters
$hasMovies = $queryParams['has_movies'] ?? null;
$hasTvShows = $queryParams['has_tv_shows'] ?? null;
$hasAdultVideos = $queryParams['has_adult_videos'] ?? null;
// Get sort parameter
$sort = $queryParams['sort'] ?? 'total_media_desc'; // total_media_desc, total_media_asc, name_asc, name_desc
// Build the base query - simplified to ensure all actors are found
$sql = "
SELECT a.id, a.name, a.thumbnail_path,
COALESCE(adult_counts.adult_video_count, 0) as adult_video_count,
COALESCE(movie_counts.movie_count, 0) as movie_count,
COALESCE(tv_counts.tv_show_count, 0) as tv_show_count,
(COALESCE(adult_counts.adult_video_count, 0) + COALESCE(movie_counts.movie_count, 0) + COALESCE(tv_counts.tv_show_count, 0)) as total_media_count,
GREATEST(
COALESCE(adult_dates.latest_adult, '1900-01-01'),
COALESCE(movie_dates.latest_movie, '1900-01-01'),
COALESCE(tv_dates.latest_tv, '1900-01-01')
) as latest_media_date
FROM actors a
LEFT JOIN actor_adult_video aav ON a.id = aav.actor_id
LEFT JOIN adult_videos av ON aav.adult_video_id = av.id
LEFT JOIN actor_movie am ON a.id = am.actor_id
LEFT JOIN movies m ON am.movie_id = m.id
LEFT JOIN actor_tv_show ats ON a.id = ats.actor_id
LEFT JOIN tv_shows ts ON ats.tv_show_id = ts.id
GROUP BY a.id
ORDER BY total_media_count DESC, a.name ASC
LIMIT 50
");
LEFT JOIN (
SELECT actor_id, COUNT(DISTINCT adult_video_id) as adult_video_count
FROM actor_adult_video
GROUP BY actor_id
) adult_counts ON a.id = adult_counts.actor_id
LEFT JOIN (
SELECT actor_id, COUNT(DISTINCT movie_id) as movie_count
FROM actor_movie
GROUP BY actor_id
) movie_counts ON a.id = movie_counts.actor_id
LEFT JOIN (
SELECT actor_id, COUNT(DISTINCT tv_show_id) as tv_show_count
FROM (
SELECT actor_id, tv_show_id FROM actor_tv_show
UNION
SELECT ate.actor_id, te.tv_show_id
FROM actor_tv_episode ate
JOIN tv_episodes te ON ate.tv_episode_id = te.id
) combined_tv
GROUP BY actor_id
) tv_counts ON a.id = tv_counts.actor_id
LEFT JOIN (
SELECT aav.actor_id, MAX(av.release_date) as latest_adult
FROM actor_adult_video aav
JOIN adult_videos av ON aav.adult_video_id = av.id
GROUP BY aav.actor_id
) adult_dates ON a.id = adult_dates.actor_id
LEFT JOIN (
SELECT am.actor_id, MAX(m.release_date) as latest_movie
FROM actor_movie am
JOIN movies m ON am.movie_id = m.id
GROUP BY am.actor_id
) movie_dates ON a.id = movie_dates.actor_id
LEFT JOIN (
SELECT combined_tv.actor_id, MAX(ts.first_air_date) as latest_tv
FROM (
SELECT actor_id, tv_show_id FROM actor_tv_show
UNION
SELECT ate.actor_id, te.tv_show_id
FROM actor_tv_episode ate
JOIN tv_episodes te ON ate.tv_episode_id = te.id
) combined_tv
JOIN tv_shows ts ON combined_tv.tv_show_id = ts.id
GROUP BY combined_tv.actor_id
) tv_dates ON a.id = tv_dates.actor_id
";
$params = [];
$whereClauses = [];
// Add search filter
if (!empty($search)) {
$whereClauses[] = "a.name LIKE :search";
$params['search'] = "%{$search}%";
}
if (!empty($whereClauses)) {
$sql .= ' WHERE ' . implode(' AND ', $whereClauses);
}
$sql .= " GROUP BY a.id";
// Add HAVING clause for filters that require aggregation
$havingClauses = [];
if ($hasMovies === '1') {
$havingClauses[] = "movie_count > 0";
}
if ($hasTvShows === '1') {
$havingClauses[] = "tv_show_count > 0";
}
if ($hasAdultVideos === '1') {
$havingClauses[] = "adult_video_count > 0";
}
if (!empty($havingClauses)) {
$sql .= ' HAVING ' . implode(' AND ', $havingClauses);
}
// Add sorting
$sortMap = [
'total_media_desc' => 'total_media_count DESC, a.name ASC',
'total_media_asc' => 'total_media_count ASC, a.name ASC',
'name_asc' => 'a.name ASC',
'name_desc' => 'a.name DESC',
'latest_desc' => 'latest_media_date DESC NULLS LAST, a.name ASC',
];
$orderBy = $sortMap[$sort] ?? 'total_media_count DESC, a.name ASC';
$sql .= " ORDER BY {$orderBy}";
// Get total count for pagination
$countSql = str_replace('SELECT a.id, a.name, a.thumbnail_path,', 'SELECT COUNT(*) as count,', $sql);
$countSql = preg_replace('/ORDER BY.*$/', '', $countSql);
$countStmt = $this->pdo->prepare($countSql);
foreach ($params as $key => $value) {
$countStmt->bindValue($key, $value);
}
$countStmt->execute();
$countResult = $countStmt->fetch(PDO::FETCH_ASSOC);
$totalCount = (int) ($countResult['count'] ?? 0);
// Add pagination
$offset = ($page - 1) * $perPage;
$sql .= " LIMIT :limit OFFSET :offset";
// Execute main query
$stmt = $this->pdo->prepare($sql);
$stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
foreach ($params as $key => $value) {
$stmt->bindValue($key, $value);
}
$stmt->execute();
$actors = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Calculate pagination info
$totalPages = ceil($totalCount / $perPage);
$hasNextPage = $page < $totalPages;
$hasPrevPage = $page > 1;
return $this->view->render($response, 'actor/index.twig', [
'title' => 'Actors & Performers',
'actors' => $actors
'actors' => $actors,
'pagination' => [
'current_page' => $page,
'per_page' => $perPage,
'total_pages' => $totalPages,
'total_items' => $totalCount,
'has_next' => $hasNextPage,
'has_prev' => $hasPrevPage,
'next_page' => $page + 1,
'prev_page' => $page - 1
],
'search' => $search,
'sort' => $sort,
'sort_options' => [
'total_media_desc' => 'Most Media',
'total_media_asc' => 'Least Media',
'name_asc' => 'Name A-Z',
'name_desc' => 'Name Z-A',
'latest_desc' => 'Recently Active'
],
'filters' => [
'has_movies' => $hasMovies,
'has_tv_shows' => $hasTvShows,
'has_adult_videos' => $hasAdultVideos
]
]);
}
}

View File

@@ -41,12 +41,18 @@ class AdultController extends Controller
}
$directors = array_filter($directors);
$sources = $queryParams['sources'] ?? [];
if (!is_array($sources)) {
$sources = [$sources];
}
$sources = array_filter($sources);
// Get view mode and sort
$viewMode = $queryParams['view'] ?? 'grid'; // grid, list, covers
$sort = $queryParams['sort'] ?? 'recent';
// Get adult videos with pagination, filters, and sorting
$adultVideos = AdultVideo::getAllWithPagination($this->pdo, $page, $perPage, $search, $genres, $directors, $sort);
$adultVideos = AdultVideo::getAllWithPagination($this->pdo, $page, $perPage, $search, $genres, $directors, $sources, $sort);
// Process metadata to extract local image paths for template compatibility
foreach ($adultVideos as &$video) {
@@ -68,11 +74,12 @@ class AdultController extends Controller
}
// Get total count for pagination
$totalCount = AdultVideo::getTotalCount($this->pdo, $search, $genres, $directors);
$totalCount = AdultVideo::getTotalCount($this->pdo, $search, $genres, $directors, $sources);
// Get available filter options
$availableGenres = AdultVideo::getAvailableGenres($this->pdo);
$availableDirectors = AdultVideo::getAvailableDirectors($this->pdo);
$availableSources = AdultVideo::getAvailableSources($this->pdo);
// Calculate pagination info
$totalPages = ceil($totalCount / $perPage);
@@ -112,11 +119,13 @@ class AdultController extends Controller
],
'filters' => [
'genres' => $genres,
'directors' => $directors
'directors' => $directors,
'sources' => $sources
],
'available_filters' => [
'genres' => $availableGenres,
'directors' => $availableDirectors
'directors' => $availableDirectors,
'sources' => $availableSources
]
]);
}
@@ -144,13 +153,13 @@ class AdultController extends Controller
// Add local image paths and other metadata to the video data for template compatibility
if (!empty($metadata['local_cover_path'])) {
$adultVideo['poster_url'] = '/images/'.$metadata['local_cover_path'];
$adultVideo['poster_url'] = $metadata['local_cover_path'];
} elseif (!empty($metadata['cover_url'])) {
$adultVideo['poster_url'] = $metadata['cover_url'];
}
if (!empty($metadata['local_screenshot_path'])) {
$adultVideo['screenshot_url'] = '/images/'.$metadata['local_screenshot_path'];
$adultVideo['screenshot_url'] = $metadata['local_screenshot_path'];
}
// Add actors data if available

View File

@@ -122,6 +122,34 @@ class MovieController extends Controller
// Decode metadata for display
$metadata = json_decode($movie['metadata'], true);
// Extract additional fields from metadata if available
if ($metadata) {
// Production companies
if (isset($metadata['production_companies']) && is_array($metadata['production_companies'])) {
$companies = array_column($metadata['production_companies'], 'name');
$movie['production_companies'] = implode(', ', $companies);
}
// Production countries
if (isset($metadata['production_countries']) && is_array($metadata['production_countries'])) {
$countries = array_column($metadata['production_countries'], 'name');
$movie['production_countries'] = implode(', ', $countries);
}
// Collection info
if (isset($metadata['belongs_to_collection']) && is_array($metadata['belongs_to_collection'])) {
$movie['belongs_to_collection'] = $metadata['belongs_to_collection']['name'] ?? null;
}
// Additional metadata fields
//$movie['budget'] = $metadata['budget'] ?? $movie['budget'];
//$movie['revenue'] = $metadata['revenue'] ?? $movie['revenue'];
//$movie['original_language'] = $metadata['original_language'] ?? $movie['original_language'];
//$movie['tagline'] = $metadata['tagline'] ?? $movie['tagline'];
//$movie['status'] = $metadata['status'] ?? $movie['status'];
//$movie['vote_count'] = $metadata['vote_count'] ?? $movie['vote_count'];
}
// Get actors for this movie
$stmt = $this->pdo->prepare("
SELECT a.*

View File

@@ -109,7 +109,7 @@ class TvShowController extends Controller
$cast = json_decode($tvShow['cast'] ?? '[]', true);
$genre = json_decode($tvShow['genre'] ?? '[]', true);
// Get actors for this TV show
// Get actors for this TV show (from main cast)
$stmt = $this->pdo->prepare("
SELECT a.*
FROM actors a
@@ -118,10 +118,59 @@ class TvShowController extends Controller
ORDER BY a.name ASC
");
$stmt->execute(['tv_show_id' => $tvShowId]);
$actors = $stmt->fetchAll(\PDO::FETCH_ASSOC);
$mainActors = $stmt->fetchAll(\PDO::FETCH_ASSOC);
// Get all actors from episodes
$stmt = $this->pdo->prepare("
SELECT DISTINCT a.*
FROM actors a
JOIN actor_tv_episode ate ON a.id = ate.actor_id
JOIN tv_episodes e ON ate.tv_episode_id = e.id
WHERE e.tv_show_id = :tv_show_id
ORDER BY a.name ASC
");
$stmt->execute(['tv_show_id' => $tvShowId]);
$episodeActors = $stmt->fetchAll(\PDO::FETCH_ASSOC);
// Merge and deduplicate actors
$allActors = array_merge($mainActors, $episodeActors);
$actorsById = [];
foreach ($allActors as $actor) {
$actorsById[$actor['id']] = $actor;
}
$actors = array_values($actorsById);
// Sort by name
usort($actors, function($a, $b) {
return strcmp($a['name'], $b['name']);
});
// Get seasons and episodes for this TV show
$tvShowModel = new TvShow($this->pdo, $tvShow);
$seasons = $tvShowModel->getSeasonsWithEpisodes();
// Get recent episodes (last 5 aired episodes)
$stmt = $this->pdo->prepare("
SELECT e.*
FROM tv_episodes e
WHERE e.tv_show_id = :tv_show_id AND e.air_date IS NOT NULL
ORDER BY e.air_date DESC
LIMIT 5
");
$stmt->execute(['tv_show_id' => $tvShowId]);
$recent_episodes = $stmt->fetchAll(\PDO::FETCH_ASSOC);
// Add actors for recent episodes
foreach ($recent_episodes as &$episode) {
$episodeStmt = $this->pdo->prepare("
SELECT a.*
FROM actors a
JOIN actor_tv_episode ate ON a.id = ate.actor_id
WHERE ate.tv_episode_id = :tv_episode_id
ORDER BY a.name ASC
");
$episodeStmt->execute(['tv_episode_id' => $episode['id']]);
$episode['actors'] = $episodeStmt->fetchAll(\PDO::FETCH_ASSOC);
}
return $this->view->render($response, 'tvshows/show.twig', [
'title' => $tvShow['title'],
'tvshow' => $tvShow,
@@ -129,7 +178,8 @@ class TvShowController extends Controller
'cast' => $cast,
'genre' => $genre,
'actors' => $actors,
'seasons' => $seasons
'seasons' => $seasons,
'recent_episodes' => $recent_episodes
]);
}

View File

@@ -25,7 +25,7 @@ class AdultVideo extends Model
'external_id'
];
public static function getAllWithPagination(\PDO $pdo, int $page, int $perPage, string $search = '', array $genres = [], array $directors = [], string $sort = 'recent'): array
public static function getAllWithPagination(\PDO $pdo, int $page, int $perPage, string $search = '', array $genres = [], array $directors = [], array $sources = [], string $sort = 'recent'): array
{
$offset = ($page - 1) * $perPage;
@@ -61,6 +61,16 @@ class AdultVideo extends Model
$sql .= $whereClause . " av.director IN (" . implode(',', $placeholders) . ")";
}
if (!empty($sources)) {
$placeholders = [];
foreach ($sources as $index => $source) {
$placeholders[] = ":source_{$index}";
$params["source_{$index}"] = $source;
}
$whereClause = (!empty($search) || !empty($genres) || !empty($directors)) ? " AND" : " WHERE";
$sql .= $whereClause . " s.display_name IN (" . implode(',', $placeholders) . ")";
}
// Add sorting
$sortOptions = [
'recent' => 'av.created_at DESC',
@@ -92,7 +102,7 @@ class AdultVideo extends Model
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
public static function getTotalCount(\PDO $pdo, string $search = '', array $genres = [], array $directors = []): int
public static function getTotalCount(\PDO $pdo, string $search = '', array $genres = [], array $directors = [], array $sources = []): int
{
$sql = "SELECT COUNT(*) as count FROM adult_videos av JOIN sources s ON av.source_id = s.id";
$params = [];
@@ -122,6 +132,16 @@ class AdultVideo extends Model
$sql .= $whereClause . " av.director IN (" . implode(',', $placeholders) . ")";
}
if (!empty($sources)) {
$placeholders = [];
foreach ($sources as $index => $source) {
$placeholders[] = ":source_{$index}";
$params["source_{$index}"] = $source;
}
$whereClause = (!empty($search) || !empty($genres) || !empty($directors)) ? " AND" : " WHERE";
$sql .= $whereClause . " s.display_name IN (" . implode(',', $placeholders) . ")";
}
$stmt = $pdo->prepare($sql);
foreach ($params as $key => $value) {
$stmt->bindValue(":{$key}", $value);
@@ -245,6 +265,21 @@ class AdultVideo extends Model
return $stmt->fetchAll(\PDO::FETCH_COLUMN);
}
/**
* Get available sources for filtering
*/
public static function getAvailableSources(\PDO $pdo): array
{
$stmt = $pdo->query("
SELECT DISTINCT s.display_name
FROM sources s
JOIN adult_videos av ON s.id = av.source_id
WHERE s.display_name IS NOT NULL AND s.display_name != ''
ORDER BY s.display_name
");
return $stmt->fetchAll(\PDO::FETCH_COLUMN);
}
/**
* Get TV show statistics
*/

View File

@@ -375,6 +375,19 @@ class TvShow extends Model
]);
$episodes = $stmt->fetchAll(\PDO::FETCH_ASSOC);
// Add actors for each episode
foreach ($episodes as &$episode) {
$episodeStmt = $this->pdo->prepare("
SELECT a.*
FROM actors a
JOIN actor_tv_episode ate ON a.id = ate.actor_id
WHERE ate.tv_episode_id = :tv_episode_id
ORDER BY a.name ASC
");
$episodeStmt->execute(['tv_episode_id' => $episode['id']]);
$episode['actors'] = $episodeStmt->fetchAll(\PDO::FETCH_ASSOC);
}
// Create a season object (simulating the old seasons table structure)
$seasons[] = [
'id' => null, // No seasons table, so no ID

View File

@@ -465,15 +465,9 @@ class StashSyncService extends BaseSyncService
$sceneData['local_cover_path'] = $existingMetadata['local_cover_path'] ?? null;
$sceneData['local_screenshot_path'] = $existingMetadata['local_screenshot_path'] ?? null;
}
// Handle performers/actors
// Handle performers/actors with full metadata
$performers = $sceneData['performers'] ?? [];
$actorNames = [];
$performerImages = [];
foreach ($performers as $performer) {
$actorNames[] = $performer['name'];
$performerImages[$performer['name']] = $performer['image_path'] ?? null;
}
$actors = $this->syncActors($actorNames, $performerImages);
$actors = $this->syncActors($performers);
$sceneData = [
'title' => $sceneData['title'] ?: 'Untitled Scene',
@@ -618,15 +612,14 @@ class StashSyncService extends BaseSyncService
}
}
private function syncActors(array $actorNames, array $performerImages = []): array
private function syncActors(array $performers): array
{
$actors = [];
foreach ($actorNames as $actorName) {
if (empty($actorName)) continue;
foreach ($performers as $performer) {
if (empty($performer['name'])) continue;
$imagePath = $performerImages[$actorName] ?? null;
$actor = $this->getOrCreateActor($actorName, $imagePath);
$actor = $this->getOrCreateActor($performer);
if ($actor) {
$actors[] = $actor;
}
@@ -635,25 +628,63 @@ class StashSyncService extends BaseSyncService
return $actors;
}
private function getOrCreateActor(string $name, ?string $imagePath = null): ?array
private function getOrCreateActor(array $performer): ?array
{
$name = $performer['name'] ?? '';
if (empty($name)) return null;
// Check if actor already exists
$stmt = $this->pdo->prepare("
SELECT id, name, thumbnail_path FROM actors WHERE name = :name
SELECT id, name, thumbnail_path, metadata FROM actors WHERE name = :name
");
$stmt->execute(['name' => $name]);
$existingActor = $stmt->fetch(PDO::FETCH_ASSOC);
if ($existingActor) {
return [
'id' => $existingActor['id'],
'name' => $existingActor['name'],
'thumbnail_path' => $existingActor['thumbnail_path']
// Prepare rich metadata from Stash performer data
$actorMetadata = [
'stash_id' => $performer['id'] ?? null,
'stash_url' => $performer['url'] ?? null,
'disambiguation' => $performer['disambiguation'] ?? '',
'gender' => $performer['gender'] ?? null,
'birth_date' => $performer['birthdate'] ?? null,
'death_date' => $performer['death_date'] ?? null,
'ethnicity' => $performer['ethnicity'] ?? null,
'country' => $performer['country'] ?? null,
'nationality' => $performer['country'] ?? null, // Map country to nationality
'eye_color' => $performer['eye_color'] ?? null,
'hair_color' => $performer['hair_color'] ?? null,
'height' => $performer['height_cm'] ? $performer['height_cm'] . 'cm' : null,
'measurements' => $performer['measurements'] ?? null,
'cup_size' => $this->extractCupSize($performer['measurements'] ?? ''),
'weight' => $performer['weight'] ? $performer['weight'] . 'kg' : null,
'piercings' => $performer['piercings'] ?? null,
'tattoos' => $performer['tattoos'] ?? null,
'fake_tits' => $performer['fake_tits'] ?? null,
'penis_length' => $performer['penis_length'] ?? null,
'circumcised' => $performer['circumcised'] ?? null,
'career_length' => $performer['career_length'] ?? null,
'aliases' => $performer['alias_list'] ?? [],
'favorite' => $performer['favorite'] ?? false,
'ignore_auto_tag' => $performer['ignore_auto_tag'] ?? false,
'scene_count' => $performer['scene_count'] ?? 0,
'details' => $performer['details'] ?? null,
'stash_created_at' => $performer['created_at'] ?? null,
'stash_updated_at' => $performer['updated_at'] ?? null,
'social_media' => [
'website' => $performer['url'] ?? null
],
'adult_specific' => [
'debut_year' => $this->extractDebutYear($performer['career_length'] ?? ''),
'retirement_year' => $this->extractRetirementYear($performer['career_length'] ?? ''),
'active' => $this->isActivePerformer($performer['career_length'] ?? ''),
'genres' => [],
'specialties' => []
]
];
}
// Try to download performer image if available
$thumbnailPath = null;
$imagePath = $performer['image_path'] ?? null;
if ($imagePath) {
// Validate image path before constructing URL
if (!empty(trim($imagePath))) {
@@ -667,7 +698,7 @@ class StashSyncService extends BaseSyncService
$imageUrl = "{$this->baseUrl}" . $imagePath;
} else {
// Relative path - assume it's in performer images directory
$imageUrl = "{$this->baseUrl}/performer/" . $imagePath;
$imageUrl = "{$this->baseUrl}/performer/" . $performer['id'] . "/" . $imagePath;
}
// Validate the constructed URL
@@ -690,17 +721,56 @@ class StashSyncService extends BaseSyncService
}
}
if ($existingActor) {
// Update existing actor with new metadata if it's more complete
$existingMetadata = json_decode($existingActor['metadata'] ?? '{}', true);
// Check if we should update - prefer more complete data
$shouldUpdate = false;
if (empty($existingMetadata['stash_id']) && !empty($actorMetadata['stash_id'])) {
$shouldUpdate = true;
} elseif (!empty($thumbnailPath) && empty($existingActor['thumbnail_path'])) {
$shouldUpdate = true;
}
if ($shouldUpdate) {
$stmt = $this->pdo->prepare("
UPDATE actors
SET thumbnail_path = COALESCE(:thumbnail_path, thumbnail_path),
metadata = :metadata,
updated_at = NOW()
WHERE id = :id
");
$stmt->execute([
'id' => $existingActor['id'],
'thumbnail_path' => $thumbnailPath ?: $existingActor['thumbnail_path'],
'metadata' => json_encode(array_merge($existingMetadata, $actorMetadata))
]);
$this->logProgress("Updated existing actor {$name} with Stash metadata");
}
return [
'id' => $existingActor['id'],
'name' => $existingActor['name'],
'thumbnail_path' => $thumbnailPath ?: $existingActor['thumbnail_path']
];
}
// Create new actor with full metadata
try {
$stmt = $this->pdo->prepare("
INSERT INTO actors (name, thumbnail_path, created_at, updated_at)
VALUES (:name, :thumbnail_path, NOW(), NOW())
INSERT INTO actors (name, thumbnail_path, metadata, created_at, updated_at)
VALUES (:name, :thumbnail_path, :metadata, NOW(), NOW())
");
$stmt->execute([
'name' => $name,
'thumbnail_path' => $thumbnailPath
'thumbnail_path' => $thumbnailPath,
'metadata' => json_encode($actorMetadata)
]);
$actorId = $this->pdo->lastInsertId();
$this->logProgress("Created new actor {$name} with full Stash metadata");
return [
'id' => $actorId,
'name' => $name,
@@ -712,6 +782,55 @@ class StashSyncService extends BaseSyncService
}
}
private function extractCupSize(string $measurements): ?string
{
if (empty($measurements)) return null;
// Try to extract cup size from measurements like "34C-24-35"
$parts = explode('-', $measurements);
if (count($parts) >= 1) {
$firstPart = trim($parts[0]);
// Look for cup size pattern (number followed by letter)
if (preg_match('/(\d+)([A-Z])/', $firstPart, $matches)) {
return $matches[2];
}
}
return null;
}
private function extractDebutYear(string $careerLength): ?string
{
if (empty($careerLength)) return null;
// Extract debut year from patterns like "2015 -" or "2015 - 2020"
if (preg_match('/(\d{4})\s*-\s*(\d{4})?/', $careerLength, $matches)) {
return $matches[1];
}
return null;
}
private function extractRetirementYear(string $careerLength): ?string
{
if (empty($careerLength)) return null;
// Extract retirement year from patterns like "2015 - 2020"
if (preg_match('/\d{4}\s*-\s*(\d{4})/', $careerLength, $matches)) {
return $matches[1];
}
return null;
}
private function isActivePerformer(string $careerLength): bool
{
if (empty($careerLength)) return false;
// Check if career is still active (ends with " -")
return str_ends_with(trim($careerLength), '-');
}
protected function getProcessedCount(): int
{
return $this->processedCount;

View File

@@ -386,10 +386,16 @@ class XbvrSyncService extends BaseSyncService
{
$actors = [];
foreach ($cast as $actorName) {
foreach ($cast as $actorData) {
// Handle both string names and actor objects
$actorName = is_array($actorData) ? ($actorData['name'] ?? '') : $actorData;
if (empty($actorName)) continue;
$actor = $this->getOrCreateActor($actorName);
// Try to get detailed actor information from XBVR
$detailedActorData = $this->getActorDetails($actorName, $actorData);
$actor = $this->getOrCreateActor($detailedActorData);
if ($actor) {
$actors[] = $actor;
}
@@ -398,37 +404,326 @@ class XbvrSyncService extends BaseSyncService
return $actors;
}
private function getOrCreateActor(string $name): ?array
private function getActorDetails(string $actorName, $actorData): array
{
// If we already have detailed actor data from the scene, use it
if (is_array($actorData) && !empty($actorData)) {
return $actorData;
}
// Try to fetch detailed actor information from XBVR/DeoVR API
// XBVR might have actor detail endpoints, let's try a few possibilities
$actorDetails = ['name' => $actorName];
// Try different XBVR actor API endpoints
$actorApiUrls = [
"{$this->baseUrl}/api/actor/search/" . urlencode($actorName),
"{$this->baseUrl}/actor/" . urlencode($actorName),
"{$this->baseUrl}/api/actors?name=" . urlencode($actorName),
];
foreach ($actorApiUrls as $apiUrl) {
try {
$this->logProgress("Trying to fetch actor details from: {$apiUrl}");
$response = $this->httpClient->get($apiUrl, [
'timeout' => 10,
'connect_timeout' => 5
]);
if ($response->getStatusCode() === 200) {
$actorApiData = json_decode($response->getBody(), true);
if (!empty($actorApiData)) {
$this->logProgress("Successfully fetched actor details for: {$actorName}");
// Merge API data with basic info
$actorDetails = array_merge($actorDetails, $this->mapActorApiData($actorApiData));
break;
}
}
} catch (Exception $e) {
// Continue to next API endpoint
$this->logProgress("Actor API endpoint failed: {$apiUrl} - " . $e->getMessage());
}
}
// If no detailed data found, try to scrape from web search or use basic info
if (count($actorDetails) <= 1) {
$this->logProgress("No detailed actor data found for {$actorName}, using basic info");
$actorDetails = $this->scrapeActorInfo($actorName);
}
return $actorDetails;
}
private function mapActorApiData(array $apiData): array
{
$mapped = [];
// Handle different possible API response formats
if (isset($apiData['actor'])) {
$apiData = $apiData['actor'];
}
// Map common fields
$fieldMappings = [
'id' => 'xbvr_id',
'name' => 'name',
'image' => 'image_path',
'thumbnail' => 'thumbnail_path',
'bio' => 'biography',
'biography' => 'biography',
'birthdate' => 'birth_date',
'age' => 'age',
'height' => 'height',
'weight' => 'weight',
'measurements' => 'measurements',
'nationality' => 'nationality',
'ethnicity' => 'ethnicity',
'eye_color' => 'eye_color',
'hair_color' => 'hair_color',
'tattoos' => 'tattoos',
'piercings' => 'piercings',
'aliases' => 'aliases',
'debut_year' => 'debut_year',
'retirement_year' => 'retirement_year',
'active' => 'active',
'website' => 'website',
'twitter' => 'twitter',
'instagram' => 'instagram',
'scene_count' => 'scene_count'
];
foreach ($fieldMappings as $apiField => $localField) {
if (isset($apiData[$apiField])) {
$mapped[$localField] = $apiData[$apiField];
}
}
return $mapped;
}
private function scrapeActorInfo(string $actorName): array
{
$actorInfo = ['name' => $actorName];
// Try to get basic information from web scraping
// This is a fallback when API doesn't provide details
try {
// Try to search for actor on common adult industry sites
$searchUrls = [
"https://www.adultempire.com/search.php?query=" . urlencode($actorName),
"https://www.brazzers.com/search/" . urlencode($actorName) . "/",
"https://www.naughtyamerica.com/search/" . urlencode($actorName),
];
foreach ($searchUrls as $searchUrl) {
try {
$response = $this->httpClient->get($searchUrl, [
'timeout' => 5,
'headers' => [
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
]
]);
if ($response->getStatusCode() === 200) {
$html = $response->getBody()->getContents();
// Basic HTML parsing to extract information
$actorInfo = array_merge($actorInfo, $this->parseActorHtml($html, $actorName));
break;
}
} catch (Exception $e) {
continue;
}
}
} catch (Exception $e) {
$this->logProgress("Web scraping failed for {$actorName}: " . $e->getMessage());
}
return $actorInfo;
}
private function parseActorHtml(string $html, string $actorName): array
{
$info = [];
// Very basic HTML parsing - look for common patterns
// This is quite fragile and would need improvement for production use
// Look for image URLs
if (preg_match('/<img[^>]+src=["\']([^"\']*?(?:actor|performer|model)[^"\']*?)["\'][^>]*>/i', $html, $matches)) {
$info['image_path'] = $matches[1];
}
// Look for birthdate patterns
if (preg_match('/(?:born|birthdate|birth).*?(\d{4}-\d{2}-\d{2}|\d{1,2}\/\d{1,2}\/\d{4})/i', $html, $matches)) {
$info['birth_date'] = date('Y-m-d', strtotime($matches[1]));
}
// Look for age
if (preg_match('/age.*?(\d+)/i', $html, $matches)) {
$info['age'] = (int)$matches[1];
}
// Look for measurements
if (preg_match('/measurements?.*?(\d+-\d+-\d+)/i', $html, $matches)) {
$info['measurements'] = $matches[1];
}
// Look for height
if (preg_match('/height.*?(\d+\'?\d*)/i', $html, $matches)) {
$info['height'] = $matches[1];
}
return $info;
}
private function getOrCreateActor(array $actorData): ?array
{
$name = $actorData['name'] ?? '';
if (empty($name)) return null;
// Check if actor already exists
$stmt = $this->pdo->prepare("
SELECT id, name, thumbnail_path FROM actors WHERE name = :name
SELECT id, name, thumbnail_path, metadata FROM actors WHERE name = :name
");
$stmt->execute(['name' => $name]);
$existingActor = $stmt->fetch(\PDO::FETCH_ASSOC);
// Prepare metadata from XBVR actor data
$actorMetadata = [
'xbvr_id' => $actorData['xbvr_id'] ?? $actorData['id'] ?? null,
'biography' => $actorData['biography'] ?? null,
'birth_date' => $actorData['birth_date'] ?? null,
'age' => $actorData['age'] ?? null,
'height' => $actorData['height'] ?? null,
'weight' => $actorData['weight'] ?? null,
'measurements' => $actorData['measurements'] ?? null,
'nationality' => $actorData['nationality'] ?? null,
'ethnicity' => $actorData['ethnicity'] ?? null,
'eye_color' => $actorData['eye_color'] ?? null,
'hair_color' => $actorData['hair_color'] ?? null,
'tattoos' => $actorData['tattoos'] ?? null,
'piercings' => $actorData['piercings'] ?? null,
'aliases' => is_array($actorData['aliases'] ?? null) ? $actorData['aliases'] : [],
'debut_year' => $actorData['debut_year'] ?? null,
'retirement_year' => $actorData['retirement_year'] ?? null,
'active' => $actorData['active'] ?? null,
'scene_count' => $actorData['scene_count'] ?? null,
'social_media' => [
'website' => $actorData['website'] ?? null,
'twitter' => $actorData['twitter'] ?? null,
'instagram' => $actorData['instagram'] ?? null
],
'adult_specific' => [
'debut_year' => $actorData['debut_year'] ?? null,
'retirement_year' => $actorData['retirement_year'] ?? null,
'active' => $actorData['active'] ?? null,
'genres' => [],
'specialties' => []
]
];
// Try to download actor image if available
$thumbnailPath = null;
$imagePath = $actorData['image_path'] ?? $actorData['thumbnail_path'] ?? null;
if ($imagePath) {
// Validate image path before constructing URL
if (!empty(trim($imagePath))) {
try {
// Handle different image path formats
if (strpos($imagePath, 'http') === 0) {
// Already a full URL
$imageUrl = $imagePath;
} elseif (strpos($imagePath, '/') === 0) {
// Absolute path from XBVR
$imageUrl = rtrim($this->baseUrl, '/') . $imagePath;
} else {
// Relative path
$imageUrl = rtrim($this->baseUrl, '/') . '/' . $imagePath;
}
// Validate the constructed URL
if (filter_var($imageUrl, FILTER_VALIDATE_URL)) {
$this->logProgress("Actor image URL for {$name}: " . $imageUrl);
$thumbnailFilename = $this->imageDownloader->generateFilename($imageUrl, 'actor');
$localImagePath = $this->imageDownloader->downloadImage($imageUrl, $thumbnailFilename, 'actors');
if ($localImagePath) {
$thumbnailPath = $this->imageDownloader->getPublicUrl($localImagePath);
$this->logProgress("Downloaded actor image: " . $localImagePath);
} else {
$this->logProgress("Failed to download actor image from: " . $imageUrl);
}
} else {
$this->logProgress("Invalid actor image URL constructed: " . $imageUrl);
}
} catch (Exception $e) {
$this->logProgress("Exception downloading actor image for {$name} from {$imagePath}: " . $e->getMessage());
}
}
}
if ($existingActor) {
// Update existing actor with new metadata if it's more complete
$existingMetadata = json_decode($existingActor['metadata'] ?? '{}', true);
// Check if we should update - prefer more complete data
$shouldUpdate = false;
if (empty($existingMetadata['xbvr_id']) && !empty($actorMetadata['xbvr_id'])) {
$shouldUpdate = true;
} elseif (!empty($thumbnailPath) && empty($existingActor['thumbnail_path'])) {
$shouldUpdate = true;
} elseif (count($existingMetadata) < count(array_filter($actorMetadata))) {
$shouldUpdate = true;
}
if ($shouldUpdate) {
$stmt = $this->pdo->prepare("
UPDATE actors
SET thumbnail_path = COALESCE(:thumbnail_path, thumbnail_path),
metadata = :metadata,
updated_at = NOW()
WHERE id = :id
");
$stmt->execute([
'id' => $existingActor['id'],
'thumbnail_path' => $thumbnailPath ?: $existingActor['thumbnail_path'],
'metadata' => json_encode(array_merge($existingMetadata, $actorMetadata))
]);
$this->logProgress("Updated existing actor {$name} with XBVR metadata");
}
return [
'id' => $existingActor['id'],
'name' => $existingActor['name'],
'thumbnail_path' => $existingActor['thumbnail_path']
'thumbnail_path' => $thumbnailPath ?: $existingActor['thumbnail_path']
];
}
// For now, we'll create actor without thumbnail
// In a full implementation, you'd fetch actor details from XBVR API
// Create new actor with XBVR metadata
try {
$stmt = $this->pdo->prepare("
INSERT INTO actors (name, created_at, updated_at)
VALUES (:name, NOW(), NOW())
INSERT INTO actors (name, thumbnail_path, metadata, created_at, updated_at)
VALUES (:name, :thumbnail_path, :metadata, NOW(), NOW())
");
$stmt->execute(['name' => $name]);
$stmt->execute([
'name' => $name,
'thumbnail_path' => $thumbnailPath,
'metadata' => json_encode($actorMetadata)
]);
$actorId = $this->pdo->lastInsertId();
$this->logProgress("Created new actor {$name} with XBVR metadata");
return [
'id' => $actorId,
'name' => $name,
'thumbnail_path' => null
'thumbnail_path' => $thumbnailPath
];
} catch (Exception $e) {
$this->logProgress("Failed to create actor {$name}: " . $e->getMessage());
@@ -498,7 +793,7 @@ class XbvrSyncService extends BaseSyncService
$this->logProgress("Starting cleanup - detecting deleted VR scenes in XBVR...");
// Clean up VR scenes
$this->cleanupScenes();
//$this->cleanupScenes();
$this->logProgress("Cleanup completed. Deleted {$this->deletedCount} VR scenes.");
}
@@ -545,20 +840,41 @@ class XbvrSyncService extends BaseSyncService
private function getXbvrScenesForCleanup(): array
{
try {
$response = $this->httpClient->get("{$this->baseUrl}/api/scene/list", [
'timeout' => 30,
'connect_timeout' => 10
]);
// Use the same DeoVR API as the main sync process to ensure consistency
$scenes = $this->getXbvrScenes();
if ($response->getStatusCode() === 200) {
$data = json_decode($response->getBody(), true);
return $data['scenes'] ?? [];
// Extract scene IDs from the detailed scene data
$sceneIds = array_map(function($scene) {
return $scene['id'] ?? null;
}, $scenes);
// Filter out null IDs
return array_filter($sceneIds, function($id) {
return $id !== null;
});
} catch (Exception $e) {
$this->logProgress("Error fetching XBVR scenes for cleanup: " . $e->getMessage());
$this->logProgress("Skipping cleanup to prevent accidental data loss");
// Return all current scene IDs to prevent deletion
// This is safer than returning empty array which would delete everything
$stmt = $this->pdo->prepare("
SELECT metadata FROM adult_videos WHERE source_id = :source_id
");
$stmt->execute(['source_id' => $this->source['id']]);
$localScenes = $stmt->fetchAll(\PDO::FETCH_ASSOC);
$existingIds = [];
foreach ($localScenes as $scene) {
$metadata = json_decode($scene['metadata'], true);
if (isset($metadata['xbvr_id'])) {
$existingIds[] = $metadata['xbvr_id'];
}
}
return [];
} catch (Exception $e) {
$this->logProgress("Error fetching XBVR scenes: " . $e->getMessage());
return [];
$this->logProgress("Returning " . count($existingIds) . " existing scene IDs to prevent cleanup");
return $existingIds;
}
}

View File

@@ -16,7 +16,6 @@
},
"dependencies": {
"alpinejs": "^3.12.0",
"axios": "^1.4.0",
"bootstrap": "^5.3.0"
"axios": "^1.4.0"
}
}

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -1,118 +1,71 @@
.items,
.item {
flex-flow: row wrap;
}
.items .item,
.item .item {
margin: 20px;
width: 120px;
height: 180px;
overflow: hidden;
box-shadow: 0 5px 10px rgba(0,0,0,0.8);
transform-origin: center top;
transform-style: preserve-3d;
transform: translateZ(0);
transition: 0.3s;
}
.items .item img,
.item .item img {
width: 100%;
min-height: 100%;
}
.items .item figcaption,
.item .item figcaption {
bottom: 0;
left: 0;
right: 0;
padding: 20px;
padding-bottom: 10px;
font-size: 20px;
background: none;
color: #fff;
transform: translateY(100%);
transition: 0.3s;
}
.items .item:after,
.item .item:after {
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Custom styles for media cards */
.items {
@apply flex flex-wrap;
}
.item {
@apply m-5 w-32 h-48 overflow-hidden shadow-lg transform-gpu transition-transform duration-300 origin-top;
}
.item img {
@apply w-full min-h-full;
}
.item figcaption {
@apply absolute bottom-0 left-0 right-0 p-5 pb-2.5 text-xl bg-transparent text-white transform translate-y-full transition-transform duration-300;
}
.item::after {
content: '';
z-index: 10;
width: 200%;
height: 100%;
top: -90%;
left: -20px;
opacity: 0.1;
transform: rotate(45deg);
background: linear-gradient(to top, transparent, #fff 15%, rgba(255,255,255,0.5));
transition: 0.3s;
}
.items .item:hover,
.item .item:hover,
.items .item:focus,
.item .item:focus,
.items .item:active,
.item .item:active {
box-shadow: 0 8px 16px 3px rgba(0,0,0,0.6);
transform: translateY(-3px) scale(1.05) rotateX(15deg);
}
.items .item:hover figcaption,
.item .item:hover figcaption,
.items .item:focus figcaption,
.item .item:focus figcaption,
.items .item:active figcaption,
.item .item:active figcaption {
transform: none;
}
.items .item:hover:after,
.item .item:hover:after,
.items .item:focus:after,
.item .item:focus:after,
.items .item:active:after,
.item .item:active:after {
transform: rotate(25deg);
top: -40%;
opacity: 0.15;
}
.item .article {
overflow: hidden;
width: 350px;
height: 235px;
margin: 20px;
}
.item .article img {
width: 100%;
min-height: 100%;
transition: 0.2s;
}
.item .article figcaption {
font-size: 14px;
text-shadow: 0 1px 0 rgba(51,51,51,0.3);
color: #fff;
left: 0;
right: 0;
top: 0;
bottom: 0;
padding: 40px;
box-shadow: 0 0 2px rgba(0,0,0,0.2);
background: rgba(6,18,53,0.6);
opacity: 0;
transform: scale(1.15);
transition: 0.2s;
}
.item .article figcaption h3 {
color: #3792e3;
font-size: 16px;
margin-bottom: 0;
font-weight: bold;
}
.item .article:hover img,
.item .article:focus img,
.item .article:active img {
filter: blur(3px);
transform: scale(0.97);
}
.item .article:hover figcaption,
.item .article:focus figcaption,
.item .article:active figcaption {
opacity: 1;
transform: none;
}
@apply absolute z-10 w-full h-full -top-9/10 -left-5 opacity-10 rotate-45 bg-gradient-to-t from-transparent via-white/15 to-white/50 transition-all duration-300;
}
.item:hover,
.item:focus,
.item:active {
@apply shadow-2xl -translate-y-1 scale-105 rotate-x-15;
}
.item:hover figcaption,
.item:focus figcaption,
.item:active figcaption {
@apply translate-y-0;
}
.item:hover::after,
.item:focus::after,
.item:active::after {
@apply rotate-25 -top-2/5 opacity-15;
}
.article {
@apply overflow-hidden w-80 h-56 m-5;
}
.article img {
@apply w-full min-h-full transition-all duration-200;
}
.article figcaption {
@apply absolute inset-0 p-10 text-sm text-white bg-blue-900/60 opacity-0 scale-115 transition-all duration-200 shadow-sm;
}
.article figcaption h3 {
@apply text-blue-300 text-base mb-0 font-bold;
}
.article:hover img,
.article:focus img,
.article:active img {
@apply blur-sm scale-95;
}
.article:hover figcaption,
.article:focus figcaption,
.article:active figcaption {
@apply opacity-100 scale-100;
}

View File

@@ -31,6 +31,7 @@ use Slim\Views\TwigMiddleware;
use DI\Container;
use Twig\TwigFunction;
use Twig\TwigFilter;
use \Twig\Extension\DebugExtension;
// Create DI Container
$container = new Container();
@@ -46,6 +47,7 @@ $container->set('view', function () use ($container) {
'cache' => $_ENV['APP_ENV'] === 'production' ? __DIR__ . '/../storage/views' : false,
'debug' => $_ENV['APP_DEBUG'] === 'true',
]);
$twig->addExtension(new \Twig\Extension\DebugExtension());
// Add custom functions
$twig->getEnvironment()->addFunction(new TwigFunction('base_url', function () {
@@ -71,6 +73,9 @@ $container->set('view', function () use ($container) {
case 'home':
$basePath = '/';
break;
case 'dashboard.index':
$basePath = '/';
break;
case 'games.index':
$basePath = '/media/games';
break;
@@ -125,6 +130,9 @@ $container->set('view', function () use ($container) {
case 'actors.show':
$basePath = '/media/actors/' . ($data['id'] ?? '');
break;
case 'actors.edit':
$basePath = '/media/actors/' . ($data['id'] ?? '') .'/edit';
break;
case 'search.index':
$basePath = '/search';
break;

View File

@@ -0,0 +1,280 @@
{% extends "layouts/app.twig" %}
{% block content %}
<div class="p-6">
<!-- Header -->
<div class="mb-6">
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold text-gray-900">Edit Actor</h1>
<p class="text-gray-600 mt-1">Update actor information and metadata</p>
</div>
<a href="{{ path_for('actors.show', {'id': actor.id}) }}" class="bg-gray-500 text-white px-4 py-2 rounded-md hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2">
Cancel
</a>
</div>
</div>
{% if error %}
<div class="bg-red-50 border border-red-200 rounded-md p-4 mb-6">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800">Error</h3>
<div class="mt-2 text-sm text-red-700">
{{ error }}
</div>
</div>
</div>
</div>
{% endif %}
<!-- Edit Form -->
<form method="POST" enctype="multipart/form-data" class="bg-white rounded-lg shadow-md border border-gray-200">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-lg font-medium text-gray-900">Basic Information</h2>
</div>
<div class="p-6 space-y-6">
<!-- Name -->
<div>
<label for="name" class="block text-sm font-medium text-gray-700">Name *</label>
<input type="text" name="name" id="name" value="{{ actor.name }}" required
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm px-3 py-2">
</div>
<!-- Current Image -->
{% if actor.thumbnail_path %}
<div>
<label class="block text-sm font-medium text-gray-700">Current Image</label>
<div class="mt-1 flex items-center space-x-4">
<img src="{{ actor.thumbnail_path }}" alt="{{ actor.name }}" class="w-24 h-32 object-cover rounded border">
<div class="text-sm text-gray-500">
Current actor image. Upload a new image below to replace it.
</div>
</div>
</div>
{% endif %}
<!-- Image Upload -->
<div>
<label for="thumbnail" class="block text-sm font-medium text-gray-700">Upload New Image</label>
<input type="file" name="thumbnail" id="thumbnail" accept="image/*"
class="mt-1 block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100">
<p class="mt-1 text-sm text-gray-500">Supported formats: JPEG, PNG, GIF, WebP. Maximum file size: 5MB.</p>
</div>
<!-- Biography -->
<div>
<label for="biography" class="block text-sm font-medium text-gray-700">Biography</label>
<textarea name="biography" id="biography" rows="4"
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm px-3 py-2"
placeholder="Enter actor biography...">{{ metadata.biography ?? '' }}</textarea>
</div>
<!-- Personal Information -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div>
<label for="birth_date" class="block text-sm font-medium text-gray-700">Birth Date</label>
<input type="date" name="birth_date" id="birth_date" value="{{ metadata.birth_date ?? '' }}"
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm px-3 py-2">
</div>
<div>
<label for="death_date" class="block text-sm font-medium text-gray-700">Death Date</label>
<input type="date" name="death_date" id="death_date" value="{{ metadata.death_date ?? '' }}"
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm px-3 py-2">
</div>
<div>
<label for="gender" class="block text-sm font-medium text-gray-700">Gender</label>
<select name="gender" id="gender"
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm px-3 py-2">
<option value="">Select Gender</option>
<option value="FEMALE" {{ (metadata.gender ?? '') == 'FEMALE' ? 'selected' : '' }}>Female</option>
<option value="MALE" {{ (metadata.gender ?? '') == 'MALE' ? 'selected' : '' }}>Male</option>
<option value="TRANSGENDER_FEMALE" {{ (metadata.gender ?? '') == 'TRANSGENDER_FEMALE' ? 'selected' : '' }}>Transgender Female</option>
<option value="TRANSGENDER_MALE" {{ (metadata.gender ?? '') == 'TRANSGENDER_MALE' ? 'selected' : '' }}>Transgender Male</option>
<option value="INTERSEX" {{ (metadata.gender ?? '') == 'INTERSEX' ? 'selected' : '' }}>Intersex</option>
<option value="NON_BINARY" {{ (metadata.gender ?? '') == 'NON_BINARY' ? 'selected' : '' }}>Non-binary</option>
</select>
</div>
<div>
<label for="birth_place" class="block text-sm font-medium text-gray-700">Birth Place</label>
<input type="text" name="birth_place" id="birth_place" value="{{ metadata.birth_place ?? '' }}"
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm px-3 py-2">
</div>
<div>
<label for="nationality" class="block text-sm font-medium text-gray-700">Nationality</label>
<input type="text" name="nationality" id="nationality" value="{{ metadata.nationality ?? '' }}"
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm px-3 py-2">
</div>
<div>
<label for="ethnicity" class="block text-sm font-medium text-gray-700">Ethnicity</label>
<input type="text" name="ethnicity" id="ethnicity" value="{{ metadata.ethnicity ?? '' }}"
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm px-3 py-2">
</div>
</div>
<!-- Physical Attributes -->
<div class="border-t border-gray-200 pt-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">Physical Attributes</h3>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div>
<label for="height" class="block text-sm font-medium text-gray-700">Height</label>
<input type="text" name="height" id="height" value="{{ metadata.height ?? '' }}"
placeholder="e.g., 5'6" or 168cm"
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm px-3 py-2">
</div>
<div>
<label for="measurements" class="block text-sm font-medium text-gray-700">Measurements</label>
<input type="text" name="measurements" id="measurements" value="{{ metadata.measurements ?? '' }}"
placeholder="e.g., 34-24-34"
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm px-3 py-2">
</div>
<div>
<label for="cup_size" class="block text-sm font-medium text-gray-700">Cup Size</label>
<input type="text" name="cup_size" id="cup_size" value="{{ metadata.cup_size ?? '' }}"
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm px-3 py-2">
</div>
<div>
<label for="ethnicity" class="block text-sm font-medium text-gray-700">Ethnicity</label>
<input type="text" name="ethnicity" id="ethnicity" value="{{ metadata.ethnicity ?? '' }}"
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm px-3 py-2">
</div>
<div>
<label for="hair_color" class="block text-sm font-medium text-gray-700">Hair Color</label>
<input type="text" name="hair_color" id="hair_color" value="{{ metadata.hair_color ?? '' }}"
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm px-3 py-2">
</div>
<div>
<label for="eye_color" class="block text-sm font-medium text-gray-700">Eye Color</label>
<input type="text" name="eye_color" id="eye_color" value="{{ metadata.eye_color ?? '' }}"
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm px-3 py-2">
</div>
</div>
</div>
<!-- Body Modifications -->
<div class="border-t border-gray-200 pt-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">Body Modifications</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label for="piercings" class="block text-sm font-medium text-gray-700">Piercings</label>
<input type="text" name="piercings" id="piercings" value="{{ metadata.piercings ?? '' }}"
placeholder="e.g., navel, nipples"
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm px-3 py-2">
</div>
<div>
<label for="tattoos" class="block text-sm font-medium text-gray-700">Tattoos</label>
<input type="text" name="tattoos" id="tattoos" value="{{ metadata.tattoos ?? '' }}"
placeholder="e.g., lower back, ankle"
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm px-3 py-2">
</div>
</div>
</div>
<!-- Aliases -->
<div class="border-t border-gray-200 pt-6">
<div>
<label for="aliases" class="block text-sm font-medium text-gray-700">Aliases</label>
<input type="text" name="aliases" id="aliases" value="{{ (metadata.aliases ?? [])|join(', ') }}"
placeholder="e.g., Stage Name, Other Names (comma-separated)"
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm px-3 py-2">
<p class="mt-1 text-sm text-gray-500">Separate multiple aliases with commas</p>
</div>
</div>
<!-- Social Media -->
<div class="border-t border-gray-200 pt-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">Social Media</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label for="twitter" class="block text-sm font-medium text-gray-700">Twitter</label>
<input type="url" name="twitter" id="twitter" value="{{ metadata.social_media.twitter ?? '' }}"
placeholder="https://twitter.com/username"
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm px-3 py-2">
</div>
<div>
<label for="instagram" class="block text-sm font-medium text-gray-700">Instagram</label>
<input type="url" name="instagram" id="instagram" value="{{ metadata.social_media.instagram ?? '' }}"
placeholder="https://instagram.com/username"
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm px-3 py-2">
</div>
<div>
<label for="onlyfans" class="block text-sm font-medium text-gray-700">OnlyFans</label>
<input type="url" name="onlyfans" id="onlyfans" value="{{ metadata.social_media.onlyfans ?? '' }}"
placeholder="https://onlyfans.com/username"
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm px-3 py-2">
</div>
<div>
<label for="website" class="block text-sm font-medium text-gray-700">Website</label>
<input type="url" name="website" id="website" value="{{ metadata.social_media.website ?? '' }}"
placeholder="https://example.com"
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm px-3 py-2">
</div>
</div>
</div>
<!-- Adult-Specific Information -->
<div class="border-t border-gray-200 pt-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">Adult Industry Information</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<div>
<label for="debut_year" class="block text-sm font-medium text-gray-700">Debut Year</label>
<input type="number" name="debut_year" id="debut_year" value="{{ metadata.adult_specific.debut_year ?? '' }}"
min="1900" max="{{ 'now'|date('Y') + 5 }}"
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm px-3 py-2">
</div>
<div>
<label for="retirement_year" class="block text-sm font-medium text-gray-700">Retirement Year</label>
<input type="number" name="retirement_year" id="retirement_year" value="{{ metadata.adult_specific.retirement_year ?? '' }}"
min="1900" max="{{ 'now'|date('Y') + 10 }}"
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm px-3 py-2">
</div>
</div>
<div class="mb-6">
<div class="flex items-center">
<input type="checkbox" name="active" id="active" value="1" {{ (metadata.adult_specific.active ?? false) ? 'checked' : '' }}
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
<label for="active" class="ml-2 block text-sm text-gray-900">
Currently Active in Adult Industry
</label>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label for="adult_genres" class="block text-sm font-medium text-gray-700">Adult Genres</label>
<input type="text" name="adult_genres" id="adult_genres" value="{{ (metadata.adult_specific.genres ?? [])|join(', ') }}"
placeholder="e.g., MILF, Anal, POV (comma-separated)"
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm px-3 py-2">
<p class="mt-1 text-sm text-gray-500">Genres this performer specializes in</p>
</div>
<div>
<label for="specialties" class="block text-sm font-medium text-gray-700">Specialties</label>
<input type="text" name="specialties" id="specialties" value="{{ (metadata.adult_specific.specialties ?? [])|join(', ') }}"
placeholder="e.g., Deep Throat, Squirt, Roleplay (comma-separated)"
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm px-3 py-2">
<p class="mt-1 text-sm text-gray-500">Specific skills or specialties</p>
</div>
</div>
</div>
</div>
<!-- Form Actions -->
<div class="px-6 py-4 bg-gray-50 border-t border-gray-200 flex justify-end space-x-3">
<a href="{{ path_for('actors.show', {'id': actor.id}) }}" class="bg-gray-500 text-white px-4 py-2 rounded-md hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2">
Cancel
</a>
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
Save Changes
</button>
</div>
</form>
</div>
{% endblock %}

View File

@@ -1,67 +1,345 @@
{% extends "layouts/app.twig" %}
{% block content %}
<div class="px-4 py-3">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 fw-bold text-dark mb-1">Performers</h1>
<p class="text-muted mb-0">{{ actors|length }} performer{{ actors|length != 1 ? 's' : '' }}</p>
</div>
</div>
<div class="items">
{% if actors %}
<div class="row g-3">
{% for actor in actors %}
<!-- Hero Section -->
<div class="relative">
<div class="h-48 md:h-64 relative overflow-hidden bg-gradient-to-br from-purple-900 via-blue-900 to-indigo-900">
<div class="absolute inset-0 bg-gradient-to-r from-purple-900/90 via-blue-900/70 to-indigo-900/90"></div>
<figure class="item" style="padding:0px">
<a href="{{ path_for('actors.show', {'id': actor.id}) }}" class="text-decoration-none">
<img src="{{ actor.thumbnail_path }}" />
<figcaption>{{ actor.name }}</figcaption>
<!-- Hero Content -->
<div class="absolute inset-0 flex items-center justify-center">
<div class="text-center text-white">
<h1 class="text-3xl md:text-5xl font-bold mb-2">Actors & Performers</h1>
<p class="text-lg md:text-xl opacity-90 mb-4">{{ pagination.total_items }} performer{{ pagination.total_items != 1 ? 's' : '' }}</p>
<!-- Pagination in Hero -->
{% if pagination.total_pages > 0 %}
<div class="flex items-center justify-center space-x-2">
<!-- Previous Button -->
{% if pagination.has_prev %}
<a href="{{ path_for('actors.index') }}?{% for key, value in app.request.query %}{{ key }}={{ value }}{% if not loop.last %}&{% endif %}{% endfor %}{% if app.request.query|length > 0 %}&{% endif %}page={{ pagination.prev_page }}"
class="px-3 py-2 text-sm font-medium text-white bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg hover:bg-white/30 transition-colors flex items-center">
<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 19l-7-7 7-7"/>
</svg>
Prev
</a>
</figure>
<!--
div class="col-md-6 col-lg-4 col-xl-2">
<div class="item h-100">
<div class="item-body text-center">
<a href="{{ path_for('actors.show', {'id': actor.id}) }}" class="text-decoration-none">
{% if actor.thumbnail_path %}
<img src="{{ actor.thumbnail_path }}" alt="{{ actor.name }}" class="rounded-circle mb-3" style="width: 80px; height: 80px; object-fit: cover;">
{% else %}
<div class="rounded-circle bg-light d-flex align-items-center justify-content-center mb-3 mx-auto" style="width: 80px; height: 80px;">
<svg class="text-muted" width="40" height="40" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<span class="px-3 py-2 text-sm font-medium text-white/50 bg-white/10 backdrop-blur-sm border border-white/20 rounded-lg cursor-not-allowed flex items-center">
<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 19l-7-7 7-7"/>
</svg>
Prev
</span>
{% endif %}
<!-- Page Info -->
<span class="px-4 py-2 text-sm font-medium text-white bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg">
Page {{ pagination.current_page }} of {{ pagination.total_pages }}
</span>
<!-- Next Button -->
{% if pagination.has_next %}
<a href="{{ path_for('actors.index') }}?{% for key, value in app.request.query %}{{ key }}={{ value }}{% if not loop.last %}&{% endif %}{% endfor %}{% if app.request.query|length > 0 %}&{% endif %}page={{ pagination.next_page }}"
class="px-3 py-2 text-sm font-medium text-white bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg hover:bg-white/30 transition-colors flex items-center">
Next
<svg class="w-4 h-4 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</a>
{% else %}
<span class="px-3 py-2 text-sm font-medium text-white/50 bg-white/10 backdrop-blur-sm border border-white/20 rounded-lg cursor-not-allowed flex items-center">
Next
<svg class="w-4 h-4 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</span>
{% endif %}
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Main Content -->
<div class="max-w-7xl mx-auto px-4 py-8">
<!-- Search and Filters Bar -->
<div class="bg-white rounded-xl shadow-lg p-6 mb-8">
<form method="GET" class="space-y-4">
<!-- Search Bar -->
<div class="flex flex-col md:flex-row gap-4">
<div class="flex-1">
<label for="search" class="block text-sm font-medium text-gray-700 mb-1">Search Actors</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
</div>
<input type="text" name="search" id="search" value="{{ search }}" placeholder="Search by actor name..."
class="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500">
</div>
</div>
<!-- Sort Options -->
<div class="md:w-48">
<label for="sort" class="block text-sm font-medium text-gray-700 mb-1">Sort By</label>
<select name="sort" id="sort" class="block w-full py-2 px-3 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500">
{% for value, label in sort_options %}
<option value="{{ value }}" {{ sort == value ? 'selected' : '' }}>{{ label }}</option>
{% endfor %}
</select>
</div>
</div>
<!-- Filter Checkboxes -->
<div class="flex flex-wrap gap-6">
<div class="flex items-center">
<input type="checkbox" name="has_movies" value="1" id="has_movies" {{ filters.has_movies == '1' ? 'checked' : '' }}
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
<label for="has_movies" class="ml-2 text-sm text-gray-700 flex items-center">
<svg class="w-4 h-4 mr-1 text-blue-500" 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>
Has Movies
</label>
</div>
<div class="flex items-center">
<input type="checkbox" name="has_tv_shows" value="1" id="has_tv_shows" {{ filters.has_tv_shows == '1' ? 'checked' : '' }}
class="h-4 w-4 text-green-600 focus:ring-green-500 border-gray-300 rounded">
<label for="has_tv_shows" class="ml-2 text-sm text-gray-700 flex items-center">
<svg class="w-4 h-4 mr-1 text-green-500" 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>
Has TV Shows
</label>
</div>
<div class="flex items-center">
<input type="checkbox" name="has_adult_videos" value="1" id="has_adult_videos" {{ filters.has_adult_videos == '1' ? 'checked' : '' }}
class="h-4 w-4 text-red-600 focus:ring-red-500 border-gray-300 rounded">
<label for="has_adult_videos" class="ml-2 text-sm text-gray-700 flex items-center">
<svg class="w-4 h-4 mr-1 text-red-500" 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>
Has Adult Videos
</label>
</div>
</div>
<!-- Action Buttons -->
<div class="flex gap-2">
<button type="submit" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-medium transition-colors flex items-center">
<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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
Search & Filter
</button>
<a href="{{ path_for('actors.index') }}" class="bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded-lg font-medium transition-colors flex items-center">
<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="M6 18L18 6M6 6l12 12"/>
</svg>
Clear Filters
</a>
</div>
</form>
</div>
{% if actors %}
<!-- Stats Overview -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div class="bg-white rounded-xl shadow-lg p-6 text-center">
<div class="w-12 h-12 bg-purple-100 rounded-full flex items-center justify-center mx-auto mb-3">
<svg class="text-purple-600" width="24" height="24" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/>
</svg>
</div>
<div class="text-2xl font-bold text-gray-900 mb-1">{{ actors|length }}</div>
<div class="text-gray-600">Total Performers</div>
</div>
{% set totalMovies = 0 %}
{% set totalShows = 0 %}
{% set totalAdult = 0 %}
{% for actor in actors %}
{% set totalMovies = totalMovies + actor.movie_count %}
{% set totalShows = totalShows + actor.tv_show_count %}
{% set totalAdult = totalAdult + actor.adult_video_count %}
{% endfor %}
<div class="bg-white rounded-xl shadow-lg p-6 text-center">
<div class="w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-3">
<svg class="text-blue-600" width="24" height="24" 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>
<div class="text-2xl font-bold text-gray-900 mb-1">{{ totalMovies }}</div>
<div class="text-gray-600">Movies</div>
</div>
<div class="bg-white rounded-xl shadow-lg p-6 text-center">
<div class="w-12 h-12 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-3">
<svg class="text-green-600" width="24" height="24" 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>
<div class="text-2xl font-bold text-gray-900 mb-1">{{ totalShows }}</div>
<div class="text-gray-600">TV Shows</div>
</div>
<div class="bg-white rounded-xl shadow-lg p-6 text-center">
<div class="w-12 h-12 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-3">
<svg class="text-red-600" width="24" height="24" 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>
</div>
<div class="text-2xl font-bold text-gray-900 mb-1">{{ totalAdult }}</div>
<div class="text-gray-600">Adult Videos</div>
</div>
</div>
<!-- Actors Grid -->
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-6">
{% for actor in actors %}
<a href="{{ path_for('actors.show', {'id': actor.id}) }}" class="group">
<div class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-xl transition-all duration-300 hover:scale-105">
<!-- Actor Avatar -->
<div class="aspect-square bg-gray-200">
{% if actor.thumbnail_path %}
<img src="{% if '/images/' in actor.thumbnail_path %}{{ actor.thumbnail_path }}{% else %}/images/{{ actor.thumbnail_path }}{% endif %}" alt="{{ actor.name }}" class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-300">
{% else %}
<div class="w-full h-full bg-gradient-to-br from-blue-400 to-purple-500 flex items-center justify-center">
<span class="text-2xl md:text-3xl font-bold text-white">{{ actor.name|first|upper }}</span>
</div>
{% endif %}
</div>
<!-- Actor Info -->
<div class="p-4">
<h3 class="font-semibold text-gray-900 text-sm leading-tight mb-2 group-hover:text-blue-600 transition-colors line-clamp-2">{{ actor.name }}</h3>
<!-- Media Counts -->
<div class="space-y-1">
{% if actor.movie_count > 0 %}
<div class="flex items-center text-xs text-gray-600">
<svg class="w-3 h-3 mr-1 text-blue-500" 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>
{{ actor.movie_count }} Movie{{ actor.movie_count != 1 ? 's' : '' }}
</div>
{% endif %}
{% if actor.tv_show_count > 0 %}
<div class="flex items-center text-xs text-gray-600">
<svg class="w-3 h-3 mr-1 text-green-500" 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>
{{ actor.tv_show_count }} Show{{ actor.tv_show_count != 1 ? 's' : '' }}
</div>
{% endif %}
{% if actor.adult_video_count > 0 %}
<div class="flex items-center text-xs text-gray-600">
<svg class="w-3 h-3 mr-1 text-red-500" 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>
{{ actor.adult_video_count }} Adult
</div>
{% endif %}
</div>
</div>
</div>
</a>
{% endfor %}
</div>
<!-- Pagination -->
{% if pagination.total_pages > 1 %}
<div class="mt-12 flex items-center justify-between">
<div class="text-sm text-gray-700">
Showing {{ (pagination.current_page - 1) * pagination.per_page + 1 }} to {{ min(pagination.current_page * pagination.per_page, pagination.total_items) }} of {{ pagination.total_items }} performers
</div>
<div class="flex items-center space-x-2">
<!-- Previous Button -->
{% if pagination.has_prev %}
<a href="{{ path_for('actors.index') }}?{% for key, value in app.request.query %}{{ key }}={{ value }}{% if not loop.last %}&{% endif %}{% endfor %}{% if app.request.query|length > 0 %}&{% endif %}page={{ pagination.prev_page }}"
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-700 transition-colors flex items-center">
<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 19l-7-7 7-7"/>
</svg>
Previous
</a>
{% else %}
<span class="px-3 py-2 text-sm font-medium text-gray-300 bg-gray-100 border border-gray-200 rounded-lg cursor-not-allowed flex items-center">
<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 19l-7-7 7-7"/>
</svg>
Previous
</span>
{% endif %}
<!-- Page Numbers -->
{% set start_page = max(1, pagination.current_page - 2) %}
{% set end_page = min(pagination.total_pages, pagination.current_page + 2) %}
{% if start_page > 1 %}
<a href="{{ path_for('actors.index') }}?{% for key, value in app.request.query %}{{ key }}={{ value }}{% if not loop.last %}&{% endif %}{% endfor %}{% if app.request.query|length > 0 %}&{% endif %}page=1"
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-700 transition-colors">1</a>
{% if start_page > 2 %}
<span class="px-2 py-2 text-sm font-medium text-gray-500">...</span>
{% endif %}
{% endif %}
{% for page_num in start_page..end_page %}
{% if page_num == pagination.current_page %}
<span class="px-3 py-2 text-sm font-medium text-white bg-blue-600 border border-blue-600 rounded-lg">{{ page_num }}</span>
{% else %}
<a href="{{ path_for('actors.index') }}?{% for key, value in app.request.query %}{{ key }}={{ value }}{% if not loop.last %}&{% endif %}{% endfor %}{% if app.request.query|length > 0 %}&{% endif %}page={{ page_num }}"
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-700 transition-colors">{{ page_num }}</a>
{% endif %}
{% endfor %}
{% if end_page < pagination.total_pages %}
{% if end_page < pagination.total_pages - 1 %}
<span class="px-2 py-2 text-sm font-medium text-gray-500">...</span>
{% endif %}
<a href="{{ path_for('actors.index') }}?{% for key, value in app.request.query %}{{ key }}={{ value }}{% if not loop.last %}&{% endif %}{% endfor %}{% if app.request.query|length > 0 %}&{% endif %}page={{ pagination.total_pages }}"
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-700 transition-colors">{{ pagination.total_pages }}</a>
{% endif %}
<!-- Next Button -->
{% if pagination.has_next %}
<a href="{{ path_for('actors.index') }}?{% for key, value in app.request.query %}{{ key }}={{ value }}{% if not loop.last %}&{% endif %}{% endfor %}{% if app.request.query|length > 0 %}&{% endif %}page={{ pagination.next_page }}"
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-700 transition-colors flex items-center">
Next
<svg class="w-4 h-4 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</a>
{% else %}
<span class="px-3 py-2 text-sm font-medium text-gray-300 bg-gray-100 border border-gray-200 rounded-lg cursor-not-allowed flex items-center">
Next
<svg class="w-4 h-4 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</span>
{% endif %}
</div>
</div>
{% endif %}
{% else %}
<!-- Empty State -->
<div class="text-center py-16">
<div class="w-24 h-24 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-6">
<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="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
</div>
{% endif %}
<h5 class="item-title mb-2">{{ actor.name }}</h5>
<p class="item-text small text-muted">
{{ actor.total_media_count }} scene{{ actor.total_media_count != 1 ? 's' : '' }}
</p>
{% if actor.latest_scene_date %}
<small class="text-muted d-block">
Latest: {{ actor.latest_scene_date|date('M j, Y') }}
</small>
{% endif %}
</a>
</div>
</div>
</div
-->
{% endfor %}
</div>
</div>
{% else %}
<div class="text-center py-5">
<svg class="text-muted mb-3" width="64" height="64" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/>
</svg>
<h5 class="text-muted">No performers found</h5>
<p class="text-muted">Performers will appear here once you sync content from your adult video sources.</p>
<h3 class="text-xl font-semibold text-gray-900 mb-2">No Performers Found</h3>
<p class="text-gray-600 max-w-md mx-auto">Performers will appear here once you sync content from your media sources. Import movies, TV shows, or adult videos to start building your actor database.</p>
</div>
{% endif %}
</div>

View File

@@ -1,113 +1,280 @@
{% extends "layouts/app.twig" %}
{% block sidebar %}
{% if actor.metadata %}
{% set metadata = actor.metadata|json_decode %}
<div class="space-y-6">
<!-- Personal Information -->
{% if metadata.biography or metadata.birth_date or metadata.birth_place or metadata.nationality %}
<div class="bg-white rounded-xl shadow-lg p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-3 flex items-center">
<svg class="mr-2 text-blue-600" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
Personal Information
</h3>
<div class="space-y-3">
{% if metadata.birth_date %}
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600">Birth Date</span>
<span class="text-sm font-medium">{{ metadata.birth_date }}</span>
</div>
{% endif %}
{% if metadata.birth_place %}
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600">Birth Place</span>
<span class="text-sm font-medium">{{ metadata.birth_place }}</span>
</div>
{% endif %}
{% if metadata.nationality %}
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600">Nationality</span>
<span class="text-sm font-medium">{{ metadata.nationality }}</span>
</div>
{% endif %}
{% if metadata.death_date %}
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600">Death Date</span>
<span class="text-sm font-medium">{{ metadata.death_date }}</span>
</div>
{% endif %}
</div>
</div>
{% endif %}
<!-- Physical Attributes -->
{% if metadata.gender or metadata.height or metadata.weight or metadata.hair_color or metadata.eye_color %}
<div class="bg-white rounded-xl shadow-lg p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-3 flex items-center">
<svg class="mr-2 text-green-600" width="20" height="20" 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>
Physical Attributes
</h3>
<div class="space-y-3">
{% if metadata.gender %}
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600">Gender</span>
<span class="text-sm font-medium">{{ metadata.gender }}</span>
</div>
{% endif %}
{% if metadata.height %}
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600">Height</span>
<span class="text-sm font-medium">{{ metadata.height }}</span>
</div>
{% endif %}
{% if metadata.weight %}
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600">Weight</span>
<span class="text-sm font-medium">{{ metadata.weight }}</span>
</div>
{% endif %}
{% if metadata.hair_color %}
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600">Hair Color</span>
<span class="text-sm font-medium">{{ metadata.hair_color }}</span>
</div>
{% endif %}
{% if metadata.eye_color %}
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600">Eye Color</span>
<span class="text-sm font-medium">{{ metadata.eye_color }}</span>
</div>
{% endif %}
{% if metadata.ethnicity %}
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600">Ethnicity</span>
<span class="text-sm font-medium">{{ metadata.ethnicity }}</span>
</div>
{% endif %}
</div>
</div>
{% endif %}
<!-- Adult-Specific Information -->
{% if metadata.measurements or metadata.cup_size or metadata.fake_tits or metadata.penis_length or metadata.circumcised %}
<div class="bg-white rounded-xl shadow-lg p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-3 flex items-center">
<svg class="mr-2 text-red-600" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
</svg>
Adult Attributes
</h3>
<div class="space-y-3">
{% if metadata.measurements %}
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600">Measurements</span>
<span class="text-sm font-medium">{{ metadata.measurements }}</span>
</div>
{% endif %}
{% if metadata.cup_size %}
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600">Cup Size</span>
<span class="text-sm font-medium">{{ metadata.cup_size }}</span>
</div>
{% endif %}
{% if metadata.fake_tits %}
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600">Fake Tits</span>
<span class="text-sm font-medium">{{ metadata.fake_tits }}</span>
</div>
{% endif %}
{% if metadata.penis_length %}
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600">Penis Length</span>
<span class="text-sm font-medium">{{ metadata.penis_length }}</span>
</div>
{% endif %}
{% if metadata.circumcised %}
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600">Circumcised</span>
<span class="text-sm font-medium">{{ metadata.circumcised }}</span>
</div>
{% endif %}
</div>
</div>
{% endif %}
<!-- Career Information -->
{% if metadata.career_length or metadata.scene_count %}
<div class="bg-white rounded-xl shadow-lg p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-3 flex items-center">
<svg class="mr-2 text-purple-600" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m8 0V8a2 2 0 01-2 2H8a2 2 0 01-2-2V6m8 0H8"/>
</svg>
Career Information
</h3>
<div class="space-y-3">
{% if metadata.career_length %}
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600">Career Length</span>
<span class="text-sm font-medium">{{ metadata.career_length }}</span>
</div>
{% endif %}
{% if metadata.scene_count %}
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600">Scene Count</span>
<span class="text-sm font-medium">{{ metadata.scene_count }}</span>
</div>
{% endif %}
{% if metadata.adult_specific.debut_year %}
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600">Debut Year</span>
<span class="text-sm font-medium">{{ metadata.adult_specific.debut_year }}</span>
</div>
{% endif %}
{% if metadata.adult_specific.retirement_year %}
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600">Retirement Year</span>
<span class="text-sm font-medium">{{ metadata.adult_specific.retirement_year }}</span>
</div>
{% endif %}
{% if metadata.adult_specific.active is defined %}
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600">Active</span>
<span class="text-sm font-medium">{{ metadata.adult_specific.active ? 'Yes' : 'No' }}</span>
</div>
{% endif %}
</div>
</div>
{% endif %}
<!-- Body Modifications -->
{% if metadata.tattoos or metadata.piercings %}
<div class="bg-white rounded-xl shadow-lg p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-3 flex items-center">
<svg class="mr-2 text-indigo-600" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zM21 5a2 2 0 00-2-2h-4a2 2 0 00-2 2v12a4 4 0 004 4h4a2 2 0 002-2V5z"/>
</svg>
Body Modifications
</h3>
<div class="space-y-3">
{% if metadata.tattoos %}
<div>
<p class="text-sm text-gray-600 mb-1">Tattoos</p>
<p class="text-sm font-medium">{{ metadata.tattoos }}</p>
</div>
{% endif %}
{% if metadata.piercings %}
<div>
<p class="text-sm text-gray-600 mb-1">Piercings</p>
<p class="text-sm font-medium">{{ metadata.piercings }}</p>
</div>
{% endif %}
</div>
</div>
{% endif %}
</div>
{% endif %}
{% endblock %}
{% block content %}
<div class="px-4 py-3">
<!-- Back button -->
<div class="mb-4">
<a href="{{ path_for('actors.index') }}" class="btn btn-outline-secondary btn-sm">
<svg class="me-2" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<!-- Hero Section -->
<div class="relative">
<div class="h-64 md:h-80 relative overflow-hidden bg-gradient-to-br from-purple-900 via-blue-900 to-indigo-900">
<div class="absolute inset-0 bg-gradient-to-r from-purple-900/90 via-blue-900/70 to-indigo-900/90"></div>
<!-- Action buttons -->
<div class="absolute top-4 left-4 right-4 z-10 flex justify-between">
<a href="{{ path_for('actors.index') }}" class="inline-flex items-center text-white hover:text-gray-300 transition-colors bg-black/20 backdrop-blur-sm rounded-full px-4 py-2">
<svg class="mr-2" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
</svg>
Back to Performers
</a>
<a href="{{ path_for('actors.edit', {'id': actor.id}) }}" class="inline-flex items-center text-white hover:text-gray-300 transition-colors bg-black/20 backdrop-blur-sm rounded-full px-4 py-2">
<svg class="mr-2" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
</svg>
Edit
</a>
</div>
<div class="card">
<div class="row g-0">
<!-- Actor image -->
<div class="col-md-4">
<div class="card-body">
<div class="text-center">
<!-- Hero Content -->
<div class="absolute inset-0 flex items-center justify-center">
<div class="text-center text-white">
<!-- Actor Avatar -->
<div class="w-32 h-32 md:w-40 md:h-40 bg-white rounded-full overflow-hidden mb-4 mx-auto shadow-2xl">
{% if actor.thumbnail_path %}
<img src="{{ actor.thumbnail_path }}" alt="{{ actor.name }}" class="rounded-circle mb-3" style="width: 150px; height: 150px; object-fit: cover;">
<img src="{% if '/images/' in actor.thumbnail_path %}{{ actor.thumbnail_path }}{% else %}/images/{{ actor.thumbnail_path }}{% endif %}" alt="{{ actor.name }}" class="w-full h-full object-cover">
{% else %}
<div class="rounded-circle bg-light d-flex align-items-center justify-content-center mb-3 mx-auto" style="width: 150px; height: 150px;">
<svg class="text-muted" width="75" height="75" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
</div>
{% endif %}
<h1 class="h3 fw-bold text-dark mb-2">{{ actor.name }}</h1>
<p class="text-muted">{{ actor.scene_count }} scene{{ actor.scene_count != 1 ? 's' : '' }}</p>
</div>
</div>
</div>
<!-- Actor details and scenes -->
<div class="col-md-8">
<div class="card-body">
<!-- Actor stats -->
<div class="row g-3 mb-4">
<div class="col-sm-6">
<div class="d-flex align-items-center">
<svg class="me-2 text-primary" width="20" height="20" 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>
<div class="fw-semibold">{{ actor.scene_count }}</div>
<small class="text-muted">Total Scenes</small>
</div>
</div>
</div>
{% if actor.scene_count > 0 %}
<div class="col-sm-6">
<div class="d-flex align-items-center">
<svg class="me-2 text-success" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
<div>
<div class="fw-semibold">{{ actor.latest_scene_date|date('M j, Y') }}</div>
<small class="text-muted">Latest Scene</small>
</div>
</div>
<div class="w-full h-full bg-gradient-to-br from-blue-400 to-purple-500 flex items-center justify-center">
<span class="text-4xl md:text-5xl font-bold text-white">{{ actor.name|first|upper }}</span>
</div>
{% endif %}
</div>
<!-- Scenes by this actor -->
{% if scenes %}
<div>
<h3 class="h5 fw-semibold text-dark mb-3">Scenes featuring {{ actor.name }}</h3>
<div class="row g-3">
{% for scene in scenes %}
<div class="col-md-6 col-lg-4">
<div class="card h-100">
<div style="background-color: #f8f9fa; height: 150px; overflow: hidden;">
{% if scene.poster_url %}
<img src="{{ scene.poster_url }}" alt="{{ scene.title }}" class="w-100 h-100" style="background-size: cover;">
{% else %}
<div class="w-100 h-100 d-flex align-items-center justify-content-center">
<svg class="text-muted" width="48" height="48" 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"/>
<!-- Actor Name -->
<h1 class="text-3xl md:text-5xl font-bold mb-2">{{ actor.name }}</h1>
<!-- Quick Stats -->
<div class="flex flex-wrap justify-center gap-6 text-sm">
{% if actor.movie_count > 0 %}
<div class="flex items-center">
<svg class="mr-2" width="18" height="18" 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>
<span class="font-medium">{{ actor.movie_count }} Movie{{ actor.movie_count != 1 ? 's' : '' }}</span>
</div>
{% endif %}
</div>
<div class="card-body">
<h6 class="card-title mb-1">
<a href="{{ path_for('adult.show', {'id': scene.id}) }}" class="text-decoration-none">{{ scene.title }}</a>
</h6>
<p class="card-text small text-muted">
{{ scene.release_date|date('M j, Y') }}
{% if scene.runtime_minutes %}
{{ (scene.runtime_minutes / 60)|round(1) }}h {{ scene.runtime_minutes % 60 }}m
{% endif %}
</p>
<small class="text-muted">{{ scene.source_name }}</small>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% else %}
<div class="text-center py-5">
<svg class="text-muted mb-3" width="64" height="64" fill="none" viewBox="0 0 24 24" stroke="currentColor">
{% if actor.tv_show_count > 0 %}
<div class="flex items-center">
<svg class="mr-2" width="18" height="18" 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>
<h5 class="text-muted">No scenes found</h5>
<p class="text-muted">This performer hasn't appeared in any scenes yet.</p>
<span class="font-medium">{{ actor.tv_show_count }} TV Show{{ actor.tv_show_count != 1 ? 's' : '' }}</span>
</div>
{% endif %}
{% if actor.adult_video_count > 0 %}
<div class="flex items-center">
<svg class="mr-2" width="18" height="18" 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>
<span class="font-medium">{{ actor.adult_video_count }} Adult Video{{ actor.adult_video_count != 1 ? 's' : '' }}</span>
</div>
{% endif %}
</div>
@@ -115,4 +282,171 @@
</div>
</div>
</div>
<!-- Main Content -->
<div class="max-w-7xl mx-auto px-4 py-8">
<!-- Detailed Stats -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
{% if actor.movie_count > 0 %}
<div class="bg-white rounded-xl shadow-lg p-6 text-center">
<div class="w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-3">
<svg class="text-blue-600" width="24" height="24" 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>
<div class="text-2xl font-bold text-gray-900 mb-1">{{ actor.movie_count }}</div>
<div class="text-gray-600">Movies</div>
</div>
{% endif %}
{% if actor.tv_show_count > 0 %}
<div class="bg-white rounded-xl shadow-lg p-6 text-center">
<div class="w-12 h-12 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-3">
<svg class="text-green-600" width="24" height="24" 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>
<div class="text-2xl font-bold text-gray-900 mb-1">{{ actor.tv_show_count }}</div>
<div class="text-gray-600">TV Shows</div>
</div>
{% endif %}
{% if actor.adult_video_count > 0 %}
<div class="bg-white rounded-xl shadow-lg p-6 text-center">
<div class="w-12 h-12 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-3">
<svg class="text-red-600" width="24" height="24" 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>
</div>
<div class="text-2xl font-bold text-gray-900 mb-1">{{ actor.adult_video_count }}</div>
<div class="text-gray-600">Adult Videos</div>
</div>
{% endif %}
</div>
<!-- Movies Section -->
{% if movies %}
<div class="mb-8">
<h2 class="text-2xl font-bold text-gray-900 mb-6 flex items-center">
<svg class="mr-3 text-blue-600" width="24" height="24" 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>
Movies ({{ movies|length }})
</h2>
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
{% for movie in movies %}
<a href="{{ path_for('movies.show', {'id': movie.id}) }}" class="group">
<div class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-xl transition-shadow">
<div class="aspect-[2/3] bg-gray-200">
{% if movie.poster_url %}
<img src="/images/{{ movie.poster_url }}" alt="{{ movie.title }}" class="w-full h-full object-cover group-hover:scale-105 transition-transform">
{% else %}
<div class="w-full h-full flex items-center justify-center bg-gradient-to-br from-gray-300 to-gray-400">
<svg class="text-gray-500 w-8 h-8" 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="p-3">
<h3 class="font-medium text-gray-900 text-sm leading-tight mb-1 group-hover:text-blue-600 transition-colors">{{ movie.title }}</h3>
{% if movie.release_date %}
<p class="text-xs text-gray-600">{{ movie.release_date|date('Y') }}</p>
{% endif %}
</div>
</div>
</a>
{% endfor %}
</div>
</div>
{% endif %}
<!-- TV Shows Section -->
{% if tv_shows %}
<div class="mb-8">
<h2 class="text-2xl font-bold text-gray-900 mb-6 flex items-center">
<svg class="mr-3 text-green-600" width="24" height="24" 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>
TV Shows ({{ tv_shows|length }})
</h2>
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
{% for show in tv_shows %}
<a href="{{ path_for('tvshows.show', {'id': show.id}) }}" class="group">
<div class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-xl transition-shadow">
<div class="aspect-[2/3] bg-gray-200">
{% if show.poster_url %}
<img src="/images/{{ show.poster_url }}" alt="{{ show.title }}" class="w-full h-full object-cover group-hover:scale-105 transition-transform">
{% else %}
<div class="w-full h-full flex items-center justify-center bg-gradient-to-br from-gray-300 to-gray-400">
<svg class="text-gray-500 w-8 h-8" 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="p-3">
<h3 class="font-medium text-gray-900 text-sm leading-tight mb-1 group-hover:text-green-600 transition-colors">{{ show.title }}</h3>
{% if show.first_air_date %}
<p class="text-xs text-gray-600">{{ show.first_air_date|date('Y') }}{% if show.status == 'Ended' and show.last_air_date %} - {{ show.last_air_date|date('Y') }}{% endif %}</p>
{% endif %}
</div>
</div>
</a>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Adult Videos Section -->
{% if scenes %}
<div class="mb-8">
<h2 class="text-2xl font-bold text-gray-900 mb-6 flex items-center">
<svg class="mr-3 text-red-600" width="24" height="24" 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>
Adult Videos ({{ scenes|length }})
</h2>
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
{% for scene in scenes %}
<a href="{{ path_for('adult.show', {'id': scene.id}) }}" class="group">
<div class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-xl transition-shadow">
<div class="aspect-[2/3] bg-gray-200">
{% if scene.poster_url %}
<img src="/images/{{ scene.poster_url }}" alt="{{ scene.title }}" class="w-full h-full object-cover group-hover:scale-105 transition-transform">
{% else %}
<div class="w-full h-full flex items-center justify-center bg-gradient-to-br from-gray-300 to-gray-400">
<svg class="text-gray-500 w-8 h-8" 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="p-3">
<h3 class="font-medium text-gray-900 text-sm leading-tight mb-1 group-hover:text-red-600 transition-colors">{{ scene.title }}</h3>
{% if scene.release_date %}
<p class="text-xs text-gray-600">{{ scene.release_date|date('M j, Y') }}</p>
{% endif %}
</div>
</div>
</a>
{% endfor %}
</div>
</div>
{% endif %}
<!-- No Content Message -->
{% if not movies and not tv_shows and not scenes %}
<div class="text-center py-16">
<div class="w-24 h-24 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-6">
<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="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
</div>
<h3 class="text-xl font-semibold text-gray-900 mb-2">No Media Found</h3>
<p class="text-gray-600">This performer hasn't appeared in any movies, TV shows, or adult videos yet.</p>
</div>
{% endif %}
</div>
{% endblock %}

View File

@@ -1,26 +1,108 @@
{% extends "layouts/app.twig" %}
{% block content %}
<div class="container-fluid">
<div class="row">
<!-- Sidebar with filters -->
<div class="col-lg-3 col-xl-2">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Filters</h5>
{% block nav_controls %}
<!-- Search form -->
<form method="GET" class="flex gap-2">
<input type="hidden" name="view" value="{{ view_mode }}">
<input type="hidden" name="per_page" value="{{ pagination.per_page }}">
{% for genre in filters.genres %}
<input type="hidden" name="genres[]" value="{{ genre }}">
{% endfor %}
{% for director in filters.directors %}
<input type="hidden" name="directors[]" value="{{ director }}">
{% endfor %}
{% for source in filters.sources %}
<input type="hidden" name="sources[]" value="{{ source }}">
{% endfor %}
<div class="relative">
<input
type="text"
name="search"
value="{{ search }}"
placeholder="Search adult videos..."
class="pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500 w-64 bg-gray-800 text-white placeholder-gray-400"
>
<svg class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
</div>
<div class="card-body">
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
Search
</button>
</form>
<!-- View mode switcher -->
<div class="flex gap-1" role="group">
{% for mode in view_modes %}
<a
href="?view={{ mode }}&sort={{ sort }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for director in filters.directors %}&directors[]={{ director }}{% endfor %}{% for source in filters.sources %}&sources[]={{ source }}{% endfor %}"
class="inline-flex items-center px-3 py-2 border border-gray-300 bg-gray-800 text-gray-300 hover:bg-gray-700 hover:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 {{ view_mode == mode ? 'bg-blue-600 border-blue-500 text-white' : '' }}"
title="{{ mode|title }} View"
>
{% if mode == 'grid' %}
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"/>
</svg>
{% endif %}
{% if mode == 'list' %}
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"/>
</svg>
{% endif %}
{% if mode == 'covers' %}
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
{% endif %}
<span class="hidden sm:inline ml-1">{{ mode|title }}</span>
</a>
{% endfor %}
</div>
<!-- Sort dropdown -->
<div class="relative" x-data="{ open: false }">
<button @click="open = !open" class="inline-flex items-center px-3 py-2 border border-gray-300 rounded-md bg-gray-800 text-gray-300 hover:bg-gray-700 hover:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
<svg class="mr-1 h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4h14M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12"/>
</svg>
<span class="hidden sm:inline">Sort</span>
</button>
<div x-show="open" @click.away="open = false" class="absolute right-0 z-50 mt-2 w-56 bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5">
<div class="py-1">
{% for key, label in sort_options %}
<a class="flex items-center justify-between px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 {{ sort == key ? 'bg-gray-50' : '' }}"
href="?sort={{ key }}&view={{ view_mode }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for director in filters.directors %}&directors[]={{ director }}{% endfor %}{% for source in filters.sources %}&sources[]={{ source }}{% endfor %}">
{{ label }}
{% if sort == key %}
<svg class="h-4 w-4 text-blue-600" fill="currentColor" viewBox="0 0 16 16">
<path d="M12.5 15a.5.5 0 0 1-.5-.5v-13a.5.5 0 0 1 1 0v13a.5.5 0 0 1-.5.5zM10 8a.5.5 0 0 1-.5.5H3.707l2.147 2.146a.5.5 0 0 1-.708.708l-3-3a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L3.707 7.5H9.5a.5.5 0 0 1 .5.5z"/>
</svg>
{% endif %}
</a>
{% endfor %}
</div>
</div>
</div>
{% endblock %}
{% block sidebar %}
<div class="space-y-4">
<!-- Filters -->
<div class="bg-gray-50 rounded-lg p-4">
<h3 class="text-sm font-medium text-gray-900 mb-4">Filters</h3>
<!-- Filter form -->
<form method="GET" id="filterForm">
<input type="hidden" name="view" value="{{ view_mode }}">
<input type="hidden" name="per_page" value="{{ pagination.per_page }}">
<input type="hidden" name="search" value="{{ search }}">
<input type="hidden" name="sort" value="{{ sort }}">
<!-- Genre filter -->
{% if available_filters.genres %}
<div class="mb-4">
<h6 class="fw-bold text-dark mb-2">Genres</h6>
<select class="form-select select2" name="genres[]" multiple data-placeholder="Select genres...">
<label class="block text-xs font-medium text-gray-700 mb-1">Genres</label>
<select class="select2 w-full text-sm border border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500" name="genres[]" multiple data-placeholder="Select genres...">
{% for genre in available_filters.genres %}
<option value="{{ genre }}" {{ genre in filters.genres ? 'selected' : '' }}>
{{ genre }}
@@ -33,8 +115,8 @@
<!-- Director filter -->
{% if available_filters.directors %}
<div class="mb-4">
<h6 class="fw-bold text-dark mb-2">Directors</h6>
<select class="form-select select2" name="directors[]" multiple data-placeholder="Select directors...">
<label class="block text-xs font-medium text-gray-700 mb-1">Directors</label>
<select class="select2 w-full text-sm border border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500" name="directors[]" multiple data-placeholder="Select directors...">
{% for director in available_filters.directors %}
<option value="{{ director }}" {{ director in filters.directors ? 'selected' : '' }}>
{{ director }}
@@ -44,132 +126,139 @@
</div>
{% endif %}
<!-- Source filter -->
{% if available_filters.sources %}
<div class="mb-4">
<label class="block text-xs font-medium text-gray-700 mb-1">Sources</label>
<select class="select2 w-full text-sm border border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500" name="sources[]" multiple data-placeholder="Select sources...">
{% for source in available_filters.sources %}
<option value="{{ source }}" {{ source in filters.sources ? 'selected' : '' }}>
{{ source }}
</option>
{% endfor %}
</select>
</div>
{% endif %}
<!-- Filter actions -->
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary btn-sm">
<div class="space-y-2">
<button type="submit" class="w-full bg-blue-600 text-white px-3 py-2 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 text-sm">
Apply Filters
</button>
<a href="{{ path_for('adult.index') }}" class="btn btn-outline-secondary btn-sm">
<a href="{{ path_for('adult.index') }}" class="w-full bg-gray-100 text-gray-700 px-3 py-2 rounded-md hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 block text-center text-sm">
Clear All
</a>
</div>
</form>
</div>
</div>
</div>
<!-- Main content area -->
<div class="col-lg-9 col-xl-10">
<div class="px-4 py-3">
<!-- Header with search and view controls -->
<div class="d-flex flex-column flex-sm-row justify-content-between align-items-start align-items-sm-center mb-4 gap-3">
<div>
<h1 class="display-4 fw-bold text-dark">Adult Videos</h1>
<!-- Active Filters Summary -->
{% if filters.genres or filters.directors or filters.sources or search %}
<div class="bg-blue-50 rounded-lg p-4">
<h3 class="text-sm font-medium text-gray-900 mb-3">Active Filters</h3>
<div class="space-y-2">
{% if search %}
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600">Search: "{{ search }}"</span>
<a href="?{% for key, value in filters %}{% if key != 'search' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-blue-600 hover:text-blue-800">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</a>
</div>
{% endif %}
{% for genre in filters.genres %}
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600">Genre: {{ genre }}</span>
<a href="?{% for key, value in filters %}{% if key != 'genres' or (key == 'genres' and value != genre) %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-blue-600 hover:text-blue-800">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</a>
</div>
{% endfor %}
{% for director in filters.directors %}
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600">Director: {{ director }}</span>
<a href="?{% for key, value in filters %}{% if key != 'directors' or (key == 'directors' and value != director) %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-blue-600 hover:text-blue-800">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</a>
</div>
{% endfor %}
{% for source in filters.sources %}
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600">Source: {{ source }}</span>
<a href="?{% for key, value in filters %}{% if key != 'sources' or (key == 'sources' and value != source) %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-blue-600 hover:text-blue-800">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</a>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Quick Stats -->
<div class="bg-gray-50 rounded-lg p-4">
<h3 class="text-sm font-medium text-gray-900 mb-3">Quick Stats</h3>
<div class="space-y-2">
<div class="flex justify-between text-sm">
<span class="text-gray-600">Total Videos</span>
<span class="font-medium text-gray-900">{{ pagination.total_items }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-gray-600">This Page</span>
<span class="font-medium text-gray-900">{{ movies|length }}</span>
</div>
{% if pagination.total_pages > 1 %}
<div class="flex justify-between text-sm">
<span class="text-gray-600">Page</span>
<span class="font-medium text-gray-900">{{ pagination.current_page }} of {{ pagination.total_pages }}</span>
</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}
{% block content %}
<!-- Main content area -->
<div class="p-6">
<!-- Header -->
<div class="mb-6">
<h1 class="text-3xl font-bold text-gray-900">Adult Videos</h1>
{% if pagination.total_items > 0 %}
<div class="text-muted small mt-1">
<div class="text-gray-500 text-sm mt-1">
{{ pagination.total_items }} videos
{% if search %}
matching "{{ search }}"
{% endif %}
{% if filters.genres or filters.directors %}
{% if filters.genres or filters.directors or filters.sources %}
{% if filters.genres %}
<span class="badge bg-primary ms-1">{{ filters.genres|join(', ') }}</span>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 ml-2">{{ filters.genres|join(', ') }}</span>
{% endif %}
{% if filters.directors %}
<span class="badge bg-secondary ms-1">{{ filters.directors|join(', ') }}</span>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 ml-2">{{ filters.directors|join(', ') }}</span>
{% endif %}
{% if filters.sources %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 ml-2">{{ filters.sources|join(', ') }}</span>
{% endif %}
{% endif %}
</div>
{% endif %}
</div>
<div class="d-flex flex-column flex-sm-row gap-3 w-100 w-sm-auto">
<!-- Search form -->
<form method="GET" class="d-flex gap-2">
<input type="hidden" name="view" value="{{ view_mode }}">
<input type="hidden" name="per_page" value="{{ pagination.per_page }}">
<input type="hidden" name="sort" value="{{ sort }}">
{% for genre in filters.genres %}
<input type="hidden" name="genres[]" value="{{ genre }}">
{% endfor %}
{% for director in filters.directors %}
<input type="hidden" name="directors[]" value="{{ director }}">
{% endfor %}
<div class="position-relative">
<input
type="text"
name="search"
value="{{ search }}"
placeholder="Search adult videos..."
class="form-control ps-5"
>
<svg class="position-absolute top-50 start-0 translate-middle-y ms-3 text-muted" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
{% if error %}
<div class="alert alert-danger mb-4">
{{ error }}
</div>
<button type="submit" class="btn btn-primary">
Search
</button>
</form>
{% endif %}
<!-- Sort dropdown -->
<div class="dropdown">
<button class="btn btn-outline-secondary dropdown-toggle d-flex align-items-center" type="button" id="sortDropdown" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-sort-down me-1"></i>
<span class="d-none d-sm-inline">Sort</span>
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="sortDropdown">
{% for key, label in sort_options %}
<li>
{% set queryParams = {
'sort': key,
'view': view_mode,
'per_page': pagination.per_page != 24 ? pagination.per_page : null,
'search': search,
'genres': filters.genres,
'directors': filters.directors
} %}
<a class="dropdown-item d-flex justify-content-between align-items-center {{ sort == key ? 'active' : '' }}"
href="?{{ queryParams|filter((v, k) => v != '' and v is not empty)|url_encode }}">
{{ label }}
{% if sort == key %}
<i class="bi bi-check2 ms-2"></i>
{% endif %}
</a>
</li>
{% endfor %}
</ul>
</div>
<!-- View mode switcher -->
<div class="btn-group" role="group">
{% for mode in view_modes %}
<a
href="?{{ {
'view': mode,
'sort': sort,
'per_page': pagination.per_page != 24 ? pagination.per_page : null,
'search': search,
'genres': filters.genres,
'directors': filters.directors
}|filter((v, k) => v != '' and v is not empty)|url_encode }}"
class="btn btn-outline-secondary {{ view_mode == mode ? 'active' : '' }}"
>
{% if mode == 'grid' %}
<svg class="me-1" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"/>
</svg>
{% endif %}
{% if mode == 'list' %}
<svg class="me-1" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"/>
</svg>
{% endif %}
{{ mode|title }}
</a>
{% endfor %}
</div>
</div>
<div class="text-muted small">
Sorted by: {{ sort_options[sort] }}
</div>
{% if error %}
@@ -188,20 +277,20 @@
<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>
<h3 class="h5 fw-medium text-dark">
{% if search or filters.genres or filters.directors %}
{% if search or filters.genres or filters.directors or filters.sources %}
No adult videos found matching your criteria
{% else %}
No adult videos found
{% endif %}
</h3>
<p class="text-muted">
{% if search or filters.genres or filters.directors %}
{% if search or filters.genres or filters.directors or filters.sources %}
Try adjusting your search terms or filters.
{% else %}
Adult videos will appear here after syncing with XBVR or Stash sources.
{% endif %}
</p>
{% if search or filters.genres or filters.directors %}
{% if search or filters.genres or filters.directors or filters.sources %}
<a href="{{ path_for('adult.index') }}" class="btn btn-primary mt-3">
Clear filters
</a>
@@ -211,28 +300,28 @@
<!-- Adult videos content based on view mode -->
{% if view_mode == 'list' %}
<!-- List view -->
<div class="card">
<ul class="list-group list-group-flush">
<div class="bg-white rounded-lg shadow-md border border-gray-200">
<ul class="divide-y divide-gray-200">
{% for movie in movies %}
<li class="list-group-item">
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<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 me-3" style="width: 64px; height: 96px; object-fit: contain; background-color: #f8f9fa;" src="{{ movie.poster_url }}" alt="{{ movie.title }}">
<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-light rounded me-3 d-flex align-items-center justify-content-center" style="width: 64px; height: 96px;">
<svg class="text-muted" width="32" height="32" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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-grow-1">
<h3 class="h6 mb-1">
<a href="{{ path_for('adult.show', {'id': movie.id}) }}" class="text-decoration-none">
<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="d-flex align-items-center gap-3 small text-muted">
<div class="flex items-center gap-3 text-sm text-gray-600">
{% if movie.release_date %}
<span>{{ movie.release_date|date('Y') }}</span>
{% endif %}
@@ -246,14 +335,14 @@
</div>
</div>
</div>
<div class="d-flex gap-2">
<div class="flex gap-2">
{% if movie.watched %}
<span class="badge bg-success">
<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="badge bg-danger">
<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 %}
@@ -265,32 +354,58 @@
</div>
{% elseif view_mode == 'covers' %}
<!-- Cover grid view -->
<div class="row g-3">
<!-- Enhanced Cover grid view -->
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-4">
{% for movie in movies %}
<div class="col-6 col-sm-4 col-md-3 col-lg-2">
<div class="card h-100">
<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="position-relative" style="background-color: #f8f9fa; border-radius: 0.375rem; overflow: hidden;">
<img src="{{ movie.poster_url }}" alt="{{ movie.title }}" class="card-img-top w-100" style="max-height: 300px; object-fit: contain;">
<div class="relative 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 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 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 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="d-flex align-items-center justify-content-center bg-light" style="aspect-ratio: 2/3; min-height: 200px;">
<svg class="text-muted" width="48" height="48" 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"/>
<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="card-body">
<h6 class="card-title text-truncate" title="{{ movie.title }}">
<a href="{{ path_for('adult.show', {'id': movie.id}) }}" class="text-decoration-none">
<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="card-text small text-muted">{{ movie.release_date|date('Y') }}</p>
<p class="text-xs text-gray-600 font-medium">{{ movie.release_date|date('Y') }}</p>
{% endif %}
{% if 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>
{% endfor %}
@@ -298,30 +413,29 @@
{% else %}
<!-- Default grid view -->
<div class="row g-3">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-4">
{% for movie in movies %}
<div class="col-12 col-md-6 col-lg-3 col-xl-2">
<div class="card h-100">
<div class="card-body">
<div class="d-flex align-items-center">
<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-light rounded d-flex align-items-center justify-content-center" style="width: 64px; height: 96px;">
<svg class="text-muted" width="32" height="32" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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="ms-3 flex-grow-1">
<h5 class="card-title mb-1">
<a href="{{ path_for('adult.show', {'id': movie.id}) }}" class="text-decoration-none">
<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="d-flex align-items-center gap-2 small text-muted mb-2">
<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 %}
@@ -330,7 +444,7 @@
{% endif %}
</div>
{% if movie.source_name %}
<p class="card-text small text-muted mb-2">
<p class="text-sm text-gray-600 mb-2">
{{ movie.source_name }}
</p>
{% endif %}
@@ -338,23 +452,23 @@
</div>
{% if movie.overview %}
<div class="mt-3">
<p class="card-text small text-muted" style="display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;">
<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 d-flex justify-content-between align-items-center small text-muted">
<div class="mt-3 flex justify-between items-center text-sm text-gray-600">
{% if movie.runtime_minutes %}
<span>{{ (movie.runtime_minutes / 60)|round(1) }}h {{ movie.runtime_minutes % 60 }}m</span>
{% endif %}
<div class="d-flex gap-1">
<div class="flex gap-1">
{% if movie.watched %}
<span class="badge bg-success">
<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="badge bg-danger">
<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 %}
@@ -362,47 +476,93 @@
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
<!-- Pagination -->
<!-- Top Pagination & Controls -->
{% if pagination.total_pages > 1 %}
<div class="d-flex align-items-center justify-content-between mt-4">
<div class="d-flex align-items-center gap-2">
<label for="per_page" class="form-label mb-0">Show:</label>
<select id="per_page" class="form-select form-select-sm w-auto">
<div class="flex items-center justify-between mb-6 p-4 bg-gray-50 rounded-lg">
<div class="flex items-center gap-4">
<div class="text-sm text-gray-700">
Showing {{ (pagination.current_page - 1) * pagination.per_page + 1 }} to {{ min(pagination.current_page * pagination.per_page, pagination.total_items) }} of {{ pagination.total_items }} videos
</div>
<div class="flex items-center gap-2">
<label for="per_page_top" class="text-sm font-medium text-gray-700">Show:</label>
<select id="per_page_top" class="text-sm border border-gray-300 rounded px-2 py-1 focus:ring-blue-500 focus:border-blue-500">
<option value="12" {{ pagination.per_page == 12 ? 'selected' : '' }}>12</option>
<option value="24" {{ pagination.per_page == 24 ? 'selected' : '' }}>24</option>
<option value="48" {{ pagination.per_page == 48 ? 'selected' : '' }}>48</option>
<option value="100" {{ pagination.per_page == 100 ? 'selected' : '' }}>100</option>
</select>
<span class="text-muted small">per page</span>
<span class="text-sm text-gray-600">per page</span>
</div>
</div>
<div class="d-flex align-items-center gap-2">
<div class="flex items-center space-x-2">
<!-- Previous Button -->
{% if pagination.has_prev %}
<a href="?page={{ pagination.prev_page }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}&view={{ view_mode }}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for director in filters.directors %}&directors[]={{ director }}{% endfor %}"
class="btn btn-outline-secondary btn-sm">
<a href="?page={{ pagination.prev_page }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}&view={{ view_mode }}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for director in filters.directors %}&directors[]={{ director }}{% endfor %}{% for source in filters.sources %}&sources[]={{ source }}{% endfor %}"
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-700 transition-colors flex items-center">
<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 19l-7-7 7-7"/>
</svg>
Previous
</a>
{% else %}
<span class="px-3 py-2 text-sm font-medium text-gray-300 bg-gray-100 border border-gray-200 rounded-lg cursor-not-allowed flex items-center">
<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 19l-7-7 7-7"/>
</svg>
Previous
</span>
{% endif %}
<div class="btn-group" role="group">
{% for page_num in range(max(1, pagination.current_page - 2), min(pagination.total_pages, pagination.current_page + 2)) %}
<a href="?page={{ page_num }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}&view={{ view_mode }}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for director in filters.directors %}&directors[]={{ director }}{% endfor %}"
class="btn btn-sm {{ page_num == pagination.current_page ? 'btn-primary' : 'btn-outline-secondary' }}">
{{ page_num }}
</a>
{% endfor %}
</div>
<!-- Page Numbers -->
{% set start_page = max(1, pagination.current_page - 2) %}
{% set end_page = min(pagination.total_pages, pagination.current_page + 2) %}
{% if start_page > 1 %}
<a href="?page=1{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}&view={{ view_mode }}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for director in filters.directors %}&directors[]={{ director }}{% endfor %}{% for source in filters.sources %}&sources[]={{ source }}{% endfor %}"
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-700 transition-colors">1</a>
{% if start_page > 2 %}
<span class="px-2 py-2 text-sm font-medium text-gray-500">...</span>
{% endif %}
{% endif %}
{% for page_num in start_page..end_page %}
{% if page_num == pagination.current_page %}
<span class="px-3 py-2 text-sm font-medium text-white bg-blue-600 border border-blue-600 rounded-lg">{{ page_num }}</span>
{% else %}
<a href="?page={{ page_num }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}&view={{ view_mode }}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for director in filters.directors %}&directors[]={{ director }}{% endfor %}{% for source in filters.sources %}&sources[]={{ source }}{% endfor %}"
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-700 transition-colors">{{ page_num }}</a>
{% endif %}
{% endfor %}
{% if end_page < pagination.total_pages %}
{% if end_page < pagination.total_pages - 1 %}
<span class="px-2 py-2 text-sm font-medium text-gray-500">...</span>
{% endif %}
<a href="?page={{ pagination.total_pages }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}&view={{ view_mode }}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for director in filters.directors %}&directors[]={{ director }}{% endfor %}{% for source in filters.sources %}&sources[]={{ source }}{% endfor %}"
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-700 transition-colors">{{ pagination.total_pages }}</a>
{% endif %}
<!-- Next Button -->
{% if pagination.has_next %}
<a href="?page={{ pagination.next_page }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}&view={{ view_mode }}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for director in filters.directors %}&directors[]={{ director }}{% endfor %}"
class="btn btn-outline-secondary btn-sm">
<a href="?page={{ pagination.next_page }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}&view={{ view_mode }}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for director in filters.directors %}&directors[]={{ director }}{% endfor %}{% for source in filters.sources %}&sources[]={{ source }}{% endfor %}"
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-700 transition-colors flex items-center">
Next
<svg class="w-4 h-4 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</a>
{% else %}
<span class="px-3 py-2 text-sm font-medium text-gray-300 bg-gray-100 border border-gray-200 rounded-lg cursor-not-allowed flex items-center">
Next
<svg class="w-4 h-4 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</span>
{% endif %}
</div>
</div>
@@ -462,6 +622,13 @@ document.getElementById('per_page')?.addEventListener('change', function() {
url.searchParams.set('page', '1'); // Reset to first page
window.location = url.toString();
});
document.getElementById('per_page_top')?.addEventListener('change', function() {
const url = new URL(window.location);
url.searchParams.set('per_page', this.value);
url.searchParams.set('page', '1'); // Reset to first page
window.location = url.toString();
});
</script>
{% endblock %}

File diff suppressed because it is too large Load Diff

View File

@@ -1,230 +1,175 @@
{% extends 'layouts/app.twig' %}
{% block content %}
<div class="mb-4">
<h1 class="display-4 fw-bold text-dark">Dashboard</h1>
<p class="lead text-muted">Overview of your media collection</p>
<div class="p-6">
<div class="mb-6">
<h1 class="text-3xl font-bold text-gray-900">Dashboard</h1>
<p class="text-lg text-gray-600 mt-2">Overview of your media collection</p>
</div>
{% if error %}
<div class="alert alert-danger mb-4">
<div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-6">
{{ error }}
</div>
{% endif %}
<!-- Stats Grid -->
<div class="row g-3 mt-4">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<!-- Total Media -->
<div class="col-12 col-sm-6 col-lg-3">
<div class="card">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="text-primary" width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<svg class="text-blue-600 w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 4v16M17 4v16M3 8h4m10 0h4M3 12h18M3 16h4m10 0h4M4 20h16a1 1 0 001-1V5a1 1 0 00-1-1H4a1 1 0 00-1 1v14a1 1 0 001 1z"></path>
</svg>
</div>
<div class="ms-3 flex-grow-1">
<dl>
<dt class="text-muted small fw-medium">Total Media</dt>
<dd>
<div class="h5 mb-0">{{ stats.total_media|number_format }}</div>
</dd>
</dl>
</div>
</div>
<div class="ml-4 flex-1">
<dt class="text-sm font-medium text-gray-500 truncate">Total Media</dt>
<dd class="text-2xl font-semibold text-gray-900">{{ stats.total_media|number_format }}</dd>
</div>
</div>
</div>
<!-- Games -->
<div class="col-12 col-sm-6 col-lg-3">
<div class="card">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="text-success" width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<svg class="text-green-600 w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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"></path>
</svg>
</div>
<div class="ms-3 flex-grow-1">
<dl>
<dt class="text-muted small fw-medium">Games</dt>
<dd>
<div class="h5 mb-0">{{ stats.total_games|number_format }}</div>
<div class="text-muted small">{{ stats.favorite_games }} favorites</div>
</dd>
</dl>
</div>
</div>
<div class="ml-4 flex-1">
<dt class="text-sm font-medium text-gray-500 truncate">Games</dt>
<dd class="text-2xl font-semibold text-gray-900">{{ stats.total_games|number_format }}</dd>
<div class="text-sm text-gray-500">{{ stats.favorite_games }} favorites</div>
</div>
</div>
</div>
<!-- Movies & TV -->
<div class="col-12 col-sm-6 col-lg-3">
<div class="card">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="text-danger" width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<svg class="text-red-600 w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 4v16M17 4v16M3 8h4m10 0h4M3 12h18M3 16h4m10 0h4M4 20h16a1 1 0 001-1V5a1 1 0 00-1-1H4a1 1 0 00-1 1v14a1 1 0 001 1z"></path>
</svg>
</div>
<div class="ms-3 flex-grow-1">
<dl>
<dt class="text-muted small fw-medium">Movies & TV</dt>
<dd>
<div class="h5 mb-0">
{{ (stats.total_movies + stats.total_tv_shows)|number_format }}
</div>
<div class="text-muted small">{{ stats.watched_movies }} watched</div>
</dd>
</dl>
</div>
</div>
<div class="ml-4 flex-1">
<dt class="text-sm font-medium text-gray-500 truncate">Movies & TV</dt>
<dd class="text-2xl font-semibold text-gray-900">{{ (stats.total_movies + stats.total_tv_shows)|number_format }}</dd>
<div class="text-sm text-gray-500">{{ stats.watched_movies }} watched</div>
</div>
</div>
</div>
<!-- Music -->
<div class="col-12 col-sm-6 col-lg-3">
<div class="card">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="text-warning" width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<svg class="text-yellow-600 w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3"></path>
</svg>
</div>
<div class="ms-3 flex-grow-1">
<dl>
<dt class="text-muted small fw-medium">Music</dt>
<dd>
<div class="h5 mb-0">{{ stats.total_music|number_format }}</div>
<div class="text-muted small">{{ stats.favorite_music }} favorites</div>
</dd>
</dl>
</div>
</div>
<div class="ml-4 flex-1">
<dt class="text-sm font-medium text-gray-500 truncate">Music</dt>
<dd class="text-2xl font-semibold text-gray-900">{{ stats.total_music|number_format }}</dd>
<div class="text-sm text-gray-500">{{ stats.favorite_music }} favorites</div>
</div>
</div>
</div>
</div>
<!-- Additional Stats -->
<div class="row g-3 mt-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<!-- Total Playtime -->
<div class="col-12 col-md-4">
<div class="card">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="text-info" width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="text-cyan-600 w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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"></path>
</svg>
</div>
<div class="ms-3 flex-grow-1">
<dl>
<dt class="text-muted small fw-medium">Total Playtime</dt>
<dd>
<div class="h5 mb-0">
<div class="ml-4 flex-1">
<dt class="text-sm font-medium text-gray-500 truncate">Total Playtime</dt>
<dd class="text-2xl font-semibold text-gray-900">
{% if stats.total_playtime %}
{{ (stats.total_playtime / 60)|round }}h
{% else %}
0h
{% endif %}
</div>
</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
<!-- Total Episodes -->
<div class="col-12 col-md-4">
<div class="card">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="text-secondary" width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="text-gray-600 w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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"></path>
</svg>
</div>
<div class="ms-3 flex-grow-1">
<dl>
<dt class="text-muted small fw-medium">TV Episodes</dt>
<dd>
<div class="h5 mb-0">{{ stats.total_episodes|number_format }}</div>
</dd>
</dl>
</div>
</div>
<div class="ml-4 flex-1">
<dt class="text-sm font-medium text-gray-500 truncate">TV Episodes</dt>
<dd class="text-2xl font-semibold text-gray-900">{{ stats.total_episodes|number_format }}</dd>
</div>
</div>
</div>
<!-- Sync Status -->
<div class="col-12 col-md-4">
<div class="card">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="text-muted" width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="text-gray-600 w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
</div>
<div class="ms-3 flex-grow-1">
<dl>
<dt class="text-muted small fw-medium">Sync Status</dt>
<dd>
<div class="h5 mb-0">
<div class="ml-4 flex-1">
<dt class="text-sm font-medium text-gray-500 truncate">Sync Status</dt>
<dd class="text-2xl font-semibold text-gray-900">
{% if sync_stats.successful_syncs > 0 %}
{{ sync_stats.successful_syncs }}/{{ sync_stats.total_syncs }} Success
{% else %}
No syncs yet
{% endif %}
</div>
</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Recent Activity -->
<div class="mt-4">
<h2 class="h3 fw-medium text-dark">Recent Activity</h2>
<div class="space-y-8">
<div>
<h2 class="text-2xl font-semibold text-gray-900 mb-6">Recent Activity</h2>
</div>
<!-- Recent Games -->
{% if recent_games %}
<div class="mt-3">
<h3 class="h5 fw-medium text-dark mb-3">Recently Played Games</h3>
<div class="card">
<ul class="list-group list-group-flush">
<div>
<h3 class="text-lg font-medium text-gray-900 mb-4">Recently Played Games</h3>
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
<ul class="divide-y divide-gray-200">
{% for game in recent_games %}
<li class="list-group-item">
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<li class="px-6 py-4">
<div class="flex items-center justify-between">
<div class="flex items-center">
{% if game.image_url %}
<img class="rounded me-3" style="width: 40px; height: 40px; object-fit: cover;" src="{{ game.image_url }}" alt="">
<img class="w-10 h-10 rounded-lg object-cover mr-4" src="{{ game.image_url }}" alt="">
{% else %}
<div class="bg-light rounded me-3 d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;">
<svg class="text-muted" width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<div class="w-10 h-10 rounded-lg bg-gray-100 flex items-center justify-center mr-4">
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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"></path>
</svg>
</div>
{% endif %}
<div>
<p class="mb-0 fw-medium">{{ game.title }}</p>
<p class="mb-0 text-muted small">{{ game.source_name }}</p>
<p class="text-sm font-medium text-gray-900">{{ game.title }}</p>
<p class="text-sm text-gray-500">{{ game.source_name }}</p>
</div>
</div>
<div class="text-muted small">
<div class="text-sm text-gray-500">
{% if game.playtime_minutes %}
{{ (game.playtime_minutes / 60)|round }}h played
{% endif %}
@@ -239,29 +184,29 @@
<!-- Recent Movies -->
{% if recent_movies %}
<div class="mt-3">
<h3 class="h5 fw-medium text-dark mb-3">Recently Watched Movies</h3>
<div class="card">
<ul class="list-group list-group-flush">
<div>
<h3 class="text-lg font-medium text-gray-900 mb-4">Recently Watched Movies</h3>
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
<ul class="divide-y divide-gray-200">
{% for movie in recent_movies %}
<li class="list-group-item">
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<li class="px-6 py-4">
<div class="flex items-center justify-between">
<div class="flex items-center">
{% if movie.poster_url %}
<img class="rounded me-3" style="width: 40px; height: 40px; object-fit: cover;" src="{{ movie.poster_url }}" alt="">
<img class="w-10 h-10 rounded-lg object-cover mr-4" src="{{ movie.poster_url }}" alt="">
{% else %}
<div class="bg-light rounded me-3 d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;">
<svg class="text-muted" width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<div class="w-10 h-10 rounded-lg bg-gray-100 flex items-center justify-center mr-4">
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 4v16M17 4v16M3 8h4m10 0h4M3 12h18M3 16h4m10 0h4M4 20h16a1 1 0 001-1V5a1 1 0 00-1-1H4a1 1 0 00-1 1v14a1 1 0 001 1z"></path>
</svg>
</div>
{% endif %}
<div>
<p class="mb-0 fw-medium">{{ movie.title }}</p>
<p class="mb-0 text-muted small">{{ movie.source_name }}</p>
<p class="text-sm font-medium text-gray-900">{{ movie.title }}</p>
<p class="text-sm text-gray-500">{{ movie.source_name }}</p>
</div>
</div>
<div class="text-muted small">
<div class="text-sm text-gray-500">
{% if movie.watch_count %}
Watched {{ movie.watch_count }} times
{% endif %}
@@ -276,35 +221,35 @@
<!-- Recent Syncs -->
{% if recent_syncs %}
<div class="mt-3">
<h3 class="h5 fw-medium text-dark mb-3">Recent Sync Activities</h3>
<div class="card">
<ul class="list-group list-group-flush">
<div>
<h3 class="text-lg font-medium text-gray-900 mb-4">Recent Sync Activities</h3>
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
<ul class="divide-y divide-gray-200">
{% for sync in recent_syncs %}
<li class="list-group-item">
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<li class="px-6 py-4">
<div class="flex items-center justify-between">
<div class="flex items-center">
<div class="flex-shrink-0">
{% if sync.status == 'completed' %}
<svg class="text-success" width="20" height="20" fill="currentColor" viewBox="0 0 20 20">
<svg class="w-5 h-5 text-green-600" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
</svg>
{% elseif sync.status == 'failed' %}
<svg class="text-danger" width="20" height="20" fill="currentColor" viewBox="0 0 20 20">
<svg class="w-5 h-5 text-red-600" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path>
</svg>
{% else %}
<svg class="text-warning" width="20" height="20" fill="currentColor" viewBox="0 0 20 20">
<svg class="w-5 h-5 text-yellow-600" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4 4a2 2 0 00-2 2v8a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2H4zm2 6a2 2 0 114 0 2 2 0 01-4 0zm8 0a2 2 0 114 0 2 2 0 01-4 0z" clip-rule="evenodd"></path>
</svg>
{% endif %}
</div>
<div class="ms-3">
<p class="mb-0 fw-medium">{{ sync.source_name }}</p>
<p class="mb-0 text-muted small">{{ sync.sync_type|title }} sync</p>
<div class="ml-3">
<p class="text-sm font-medium text-gray-900">{{ sync.source_name }}</p>
<p class="text-sm text-gray-500">{{ sync.sync_type|title }} sync</p>
</div>
</div>
<div class="text-muted small">
<div class="text-sm text-gray-500">
{{ sync.processed_items }} items • {{ sync.created_at|date('M j, Y') }}
</div>
</div>
@@ -316,13 +261,13 @@
{% endif %}
{% if not recent_games and not recent_movies and not recent_syncs %}
<div class="card mt-3">
<div class="card-body text-center">
<svg class="mx-auto text-muted mb-3" width="48" height="48" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-12">
<div class="text-center">
<svg class="mx-auto h-12 w-12 text-gray-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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"></path>
</svg>
<h3 class="h5 fw-medium text-dark">No recent activity</h3>
<p class="text-muted">Start adding media to see your activity here.</p>
<h3 class="text-lg font-medium text-gray-900 mb-2">No recent activity</h3>
<p class="text-gray-500">Start adding media to see your activity here.</p>
</div>
</div>
{% endif %}

View File

@@ -1,15 +1,93 @@
{% extends "layouts/app.twig" %}
{% block content %}
<div class="container-fluid">
<div class="row">
<!-- Sidebar with filters -->
<div class="col-lg-3 col-xl-2">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Filters</h5>
{% block nav_controls %}
<!-- Search form -->
<form method="GET" class="flex gap-2">
<input type="hidden" name="view" value="{{ view_mode }}">
<input type="hidden" name="per_page" value="{{ pagination.per_page }}">
{% for genre in filters.genres %}
<input type="hidden" name="genres[]" value="{{ genre }}">
{% endfor %}
{% for platform in filters.platforms %}
<input type="hidden" name="platforms[]" value="{{ platform }}">
{% endfor %}
<div class="relative">
<input
type="text"
name="search"
value="{{ search }}"
placeholder="Search games..."
class="pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500 w-64 bg-gray-800 text-white placeholder-gray-400"
>
<svg class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
</div>
<div class="card-body">
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
Search
</button>
</form>
<!-- View mode switcher -->
<div class="flex gap-1" role="group">
{% for mode in view_modes %}
<a
href="?view={{ mode }}&sort={{ sort }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for platform in filters.platforms %}&platforms[]={{ platform }}{% endfor %}"
class="inline-flex items-center px-3 py-2 border border-gray-300 bg-gray-800 text-gray-300 hover:bg-gray-700 hover:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 {{ view_mode == mode ? 'bg-blue-600 border-blue-500 text-white' : '' }}"
title="{{ mode|title }} View"
>
{% if mode == 'grid' %}
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"/>
</svg>
{% endif %}
{% if mode == 'list' %}
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"/>
</svg>
{% endif %}
{% if mode == 'covers' %}
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
{% endif %}
<span class="hidden sm:inline ml-1">{{ mode|title }}</span>
</a>
{% endfor %}
</div>
<!-- Sort dropdown -->
<div class="relative" x-data="{ open: false }">
<button @click="open = !open" class="inline-flex items-center px-3 py-2 border border-gray-300 rounded-md bg-gray-800 text-gray-300 hover:bg-gray-700 hover:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
<svg class="mr-1 h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4h14M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12"/>
</svg>
<span class="hidden sm:inline">Sort</span>
</button>
<div x-show="open" @click.away="open = false" class="absolute right-0 z-50 mt-2 w-56 bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5">
<div class="py-1">
{% for key, label in sort_options %}
<a class="flex items-center justify-between px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 {{ sort == key ? 'bg-gray-50' : '' }}"
href="?sort={{ key }}&view={{ view_mode }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for platform in filters.platforms %}&platforms[]={{ platform }}{% endfor %}">
{{ label }}
{% if sort == key %}
<svg class="h-4 w-4 text-blue-600" fill="currentColor" viewBox="0 0 16 16">
<path d="M12.5 15a.5.5 0 0 1-.5-.5v-13a.5.5 0 0 1 1 0v13a.5.5 0 0 1-.5.5zM10 8a.5.5 0 0 1-.5.5H3.707l2.147 2.146a.5.5 0 0 1-.708.708l-3-3a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L3.707 7.5H9.5a.5.5 0 0 1 .5.5z"/>
</svg>
{% endif %}
</a>
{% endfor %}
</div>
</div>
</div>
{% endblock %}
{% block sidebar %}
<div class="space-y-4">
<!-- Filters -->
<div class="bg-gray-50 rounded-lg p-4">
<h3 class="text-sm font-medium text-gray-900 mb-4">Filters</h3>
<!-- Filter form -->
<form method="GET" id="filterForm">
<input type="hidden" name="view" value="{{ view_mode }}">
@@ -19,8 +97,8 @@
<!-- Genre filter -->
{% if available_filters.genres %}
<div class="mb-4">
<h6 class="fw-bold text-dark mb-2">Genres</h6>
<select class="form-select select2" name="genres[]" multiple data-placeholder="Select genres...">
<label class="block text-xs font-medium text-gray-700 mb-1">Genres</label>
<select class="select2 w-full text-sm border border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500" name="genres[]" multiple data-placeholder="Select genres...">
{% for genre in available_filters.genres %}
<option value="{{ genre }}" {{ genre in filters.genres ? 'selected' : '' }}>
{{ genre }}
@@ -33,8 +111,8 @@
<!-- Platform filter -->
{% if available_filters.platforms %}
<div class="mb-4">
<h6 class="fw-bold text-dark mb-2">Platforms</h6>
<select class="form-select select2" name="platforms[]" multiple data-placeholder="Select platforms...">
<label class="block text-xs font-medium text-gray-700 mb-1">Platforms</label>
<select class="select2 w-full text-sm border border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500" name="platforms[]" multiple data-placeholder="Select platforms...">
{% for platform in available_filters.platforms %}
<option value="{{ platform }}" {{ platform in filters.platforms ? 'selected' : '' }}>
{{ platform }}
@@ -45,129 +123,103 @@
{% endif %}
<!-- Filter actions -->
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary btn-sm">
<div class="space-y-2">
<button type="submit" class="w-full bg-blue-600 text-white px-3 py-2 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 text-sm">
Apply Filters
</button>
<a href="{{ path_for('games.index') }}" class="btn btn-outline-secondary btn-sm">
<a href="{{ path_for('games.index') }}" class="w-full bg-gray-100 text-gray-700 px-3 py-2 rounded-md hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 block text-center text-sm">
Clear All
</a>
</div>
</form>
</div>
</div>
</div>
<!-- Main content area -->
<div class="col-lg-9 col-xl-10">
<div class="px-4 py-3">
<!-- Header with search and view controls -->
<div class="d-flex flex-column flex-sm-row justify-content-between align-items-start align-items-sm-center mb-4 gap-3">
<div>
<h1 class="display-4 fw-bold text-dark">Games</h1>
<!-- Active Filters Summary -->
{% if filters.genres or filters.platforms or search %}
<div class="bg-blue-50 rounded-lg p-4">
<h3 class="text-sm font-medium text-gray-900 mb-3">Active Filters</h3>
<div class="space-y-2">
{% if search %}
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600">Search: "{{ search }}"</span>
<a href="?{% for key, value in filters %}{% if key != 'search' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-blue-600 hover:text-blue-800">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</a>
</div>
{% endif %}
{% for genre in filters.genres %}
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600">Genre: {{ genre }}</span>
<a href="?{% for key, value in filters %}{% if key != 'genres' or (key == 'genres' and value != genre) %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-blue-600 hover:text-blue-800">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</a>
</div>
{% endfor %}
{% for platform in filters.platforms %}
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600">Platform: {{ platform }}</span>
<a href="?{% for key, value in filters %}{% if key != 'platforms' or (key == 'platforms' and value != platform) %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-blue-600 hover:text-blue-800">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</a>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Quick Stats -->
<div class="bg-gray-50 rounded-lg p-4">
<h3 class="text-sm font-medium text-gray-900 mb-3">Quick Stats</h3>
<div class="space-y-2">
<div class="flex justify-between text-sm">
<span class="text-gray-600">Total Games</span>
<span class="font-medium text-gray-900">{{ pagination.total_items }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-gray-600">This Page</span>
<span class="font-medium text-gray-900">{{ games|length }}</span>
</div>
{% if pagination.total_pages > 1 %}
<div class="flex justify-between text-sm">
<span class="text-gray-600">Page</span>
<span class="font-medium text-gray-900">{{ pagination.current_page }} of {{ pagination.total_pages }}</span>
</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}
{% block content %}
<!-- Main content area -->
<div class="p-6">
<!-- Header -->
<div class="mb-6">
<h1 class="text-3xl font-bold text-gray-900">Games</h1>
{% if pagination.total_items > 0 %}
<div class="text-muted small mt-1">
<div class="text-gray-500 text-sm mt-1">
{{ pagination.total_items }} games from {{ games|reduce((carry, game) => carry + game.platform_count, 0) }} platforms
{% if search %}
matching "{{ search }}"
{% endif %}
{% if filters.genres or filters.platforms %}
{% if filters.genres %}
<span class="badge bg-primary ms-1">{{ filters.genres|join(', ') }}</span>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 ml-2">{{ filters.genres|join(', ') }}</span>
{% endif %}
{% if filters.platforms %}
<span class="badge bg-secondary ms-1">{{ filters.platforms|join(', ') }}</span>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 ml-2">{{ filters.platforms|join(', ') }}</span>
{% endif %}
{% endif %}
</div>
{% endif %}
</div>
<div class="d-flex flex-column flex-sm-row gap-3 w-100 w-sm-auto">
<!-- Search form -->
<form method="GET" class="d-flex gap-2">
<input type="hidden" name="view" value="{{ view_mode }}">
<input type="hidden" name="per_page" value="{{ pagination.per_page }}">
{% for genre in filters.genres %}
<input type="hidden" name="genres[]" value="{{ genre }}">
{% endfor %}
{% for platform in filters.platforms %}
<input type="hidden" name="platforms[]" value="{{ platform }}">
{% endfor %}
<div class="position-relative">
<input
type="text"
name="search"
value="{{ search }}"
placeholder="Search games..."
class="form-control ps-5"
>
<svg class="position-absolute top-50 start-0 translate-middle-y ms-3 text-muted" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
</div>
<button type="submit" class="btn btn-primary">
Search
</button>
</form>
<div class="d-flex gap-2">
<!-- Sort dropdown -->
<div class="dropdown">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" id="sortDropdown" data-bs-toggle="dropdown" aria-expanded="false">
<svg class="me-1" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12"/>
</svg>
<span class="d-none d-sm-inline">Sort</span>
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="sortDropdown">
{% for key, label in sort_options %}
<li>
<a class="dropdown-item d-flex justify-content-between align-items-center {{ sort == key ? 'active' : '' }}"
href="?sort={{ key }}&view={{ view_mode }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for platform in filters.platforms %}&platforms[]={{ platform }}{% endfor %}">
{{ label }}
{% if sort == key %}
<svg width="16" height="16" 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>
{% endif %}
</a>
</li>
{% endfor %}
</ul>
</div>
<!-- View mode switcher -->
<div class="btn-group" role="group">
{% for mode in view_modes %}
<a
href="?view={{ mode }}&sort={{ sort }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for platform in filters.platforms %}&platforms[]={{ platform }}{% endfor %}"
class="btn btn-outline-secondary {{ view_mode == mode ? 'active' : '' }}"
title="{{ mode|title }} View"
>
{% if mode == 'grid' %}
<svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"/>
</svg>
{% endif %}
{% if mode == 'list' %}
<svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"/>
</svg>
{% endif %}
{% if mode == 'covers' %}
<svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
{% endif %}
<span class="d-none d-sm-inline ms-1">{{ mode|title }}</span>
</a>
{% endfor %}
</div>
</div>
</div>
</div>
{% if games is empty %}
<div class="text-center py-5">
<svg class="mx-auto text-muted mb-3" width="48" height="48" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@@ -197,31 +249,31 @@
<!-- Games content based on view mode -->
{% if view_mode == 'list' %}
<!-- List view -->
<div class="card">
<ul class="list-group list-group-flush">
<div class="bg-white rounded-lg shadow-md border border-gray-200">
<ul class="divide-y divide-gray-200">
{% for game in games %}
<li class="list-group-item">
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<li class="px-4 py-3">
<div class="flex justify-between items-center">
<div class="flex items-center">
{% if game.image_url %}
<img class="rounded me-3" style="width: 64px; height: 64px; object-fit: cover;" src="/images/playnite/{{ game.image_url }}" alt="{{ game.title }}">
<img class="rounded mr-3" style="width: 64px; height: 64px; object-fit: cover;" src="/images/playnite/{{ game.image_url }}" alt="{{ game.title }}">
{% else %}
<div class="bg-light rounded me-3 d-flex align-items-center justify-content-center" style="width: 64px; height: 64px;">
<svg class="text-muted" width="32" height="32" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<div class="bg-gray-100 rounded mr-3 flex items-center justify-center" style="width: 64px; height: 64px;">
<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="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 %}
<div class="flex-grow-1">
<h3 class="h6 mb-1">
<a href="{{ path_for('games.show', {'game_key': game.game_key}) }}" class="text-decoration-none">
<div class="flex-1">
<h3 class="text-sm font-semibold mb-1">
<a href="{{ path_for('games.show', {'game_key': game.game_key}) }}" class="no-underline text-gray-900 hover:text-blue-600">
{{ game.title }}
</a>
</h3>
<div class="d-flex align-items-center gap-3 small text-muted">
<div class="flex items-center gap-3 text-sm text-gray-600">
<span>{{ game.platform_count }} platform{{ game.platform_count > 1 ? 's' : '' }}</span>
{% if game.platforms %}
<span class="badge bg-light text-dark">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
{{ game.platforms|join(', ') }}
</span>
{% endif %}
@@ -233,9 +285,9 @@
</div>
</div>
{% if game.genres %}
<div class="d-flex flex-wrap gap-1">
<div class="flex flex-wrap gap-1">
{% for genre in game.genres|slice(0, 3) %}
<span class="badge bg-primary">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{{ genre }}
</span>
{% endfor %}
@@ -249,27 +301,25 @@
{% elseif view_mode == 'covers' %}
<!-- Cover grid view -->
<div class="row g-3">
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4">
{% for game in games %}
<div class="col-6 col-sm-4 col-md-3 col-lg-1">
<div class="card h-100">
<div class="bg-white rounded-lg shadow-md border border-gray-200 overflow-hidden h-full">
{% if game.image_url %}
<div class="position-relative" style="aspect-ratio: 3/4; overflow: hidden;">
<img src="/images/playnite/{{ game.image_url }}" alt="{{ game.title }}" class="card-img-top" style="width: 100%; height: 100%; object-fit: cover;">
<div class="relative aspect-[3/4] overflow-hidden">
<img src="/images/playnite/{{ game.image_url }}" alt="{{ game.title }}" class="w-full h-full object-cover">
</div>
{% else %}
<div class="d-flex align-items-center justify-content-center bg-light" style="aspect-ratio: 3/4; min-height: 200px;">
<svg class="text-muted" width="48" height="48" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<div class="flex items-center justify-center bg-gray-100 aspect-[3/4] min-h-[200px]">
<svg class="text-gray-600" width="48" height="48" 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-7 8h12a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"/>
</svg>
</div>
{% endif %}
<div class="card-body">
<h6 class="card-title text-truncate" title="{{ game.title }}">
<div class="p-3">
<h6 class="text-sm font-semibold truncate" title="{{ game.title }}">
{{ game.title }}
</h6>
<p class="card-text small text-muted">{{ game.platform_count }} platform{{ game.platform_count > 1 ? 's' : '' }}</p>
</div>
<p class="text-xs text-gray-600 mt-1">{{ game.platform_count }} platform{{ game.platform_count > 1 ? 's' : '' }}</p>
</div>
</div>
{% endfor %}
@@ -277,33 +327,32 @@
{% else %}
<!-- Default grid view -->
<div class="row g-3">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{% for game in games %}
<div class="col-12 col-md-6 col-lg-4">
<div class="card h-100">
<div class="card-body">
<div class="d-flex align-items-center">
<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 game.image_url %}
<img class="rounded" style="width: 64px; height: 64px; object-fit: cover;" src="/images/playnite/{{ game.image_url }}" alt="{{ game.title }}">
{% else %}
<div class="bg-light rounded d-flex align-items-center justify-content-center" style="width: 64px; height: 64px;">
<svg class="text-muted" width="32" height="32" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<div class="bg-gray-100 rounded flex items-center justify-center" style="width: 64px; height: 64px;">
<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="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 %}
</div>
<div class="ms-3 flex-grow-1">
<h5 class="card-title mb-1">
<a href="{{ path_for('games.show', {'game_key': game.game_key}) }}" class="text-decoration-none">
<div class="ml-3 flex-1">
<h5 class="text-lg font-semibold mb-1">
<a href="{{ path_for('games.show', {'game_key': game.game_key}) }}" class="no-underline text-gray-900 hover:text-blue-600">
{{ game.title }}
</a>
</h5>
<p class="card-text small text-muted mb-2">
<p class="text-sm text-gray-600 mb-2">
{{ game.platform_count }} platform{{ game.platform_count > 1 ? 's' : '' }}
{% if game.platforms %}
<span class="badge bg-light text-dark ms-2">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 ml-2">
{{ game.platforms|join(', ') }}
</span>
{% endif %}
@@ -311,16 +360,16 @@
</div>
</div>
<div class="mt-3">
<div class="d-flex justify-content-between align-items-center small text-muted">
<div class="flex justify-between items-center text-sm text-gray-600">
<span>{{ game.total_playtime|format_duration }} played</span>
{% if game.max_completion > 0 %}
<span>{{ game.max_completion }}% complete</span>
{% endif %}
</div>
{% if game.genres %}
<div class="mt-2 d-flex flex-wrap gap-1">
<div class="mt-2 flex flex-wrap gap-1">
{% for genre in game.genres|slice(0, 3) %}
<span class="badge bg-primary">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{{ genre }}
</span>
{% endfor %}
@@ -329,47 +378,93 @@
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
<!-- Pagination -->
<!-- Top Pagination & Controls -->
{% if pagination.total_pages > 1 %}
<div class="d-flex align-items-center justify-content-between mt-4">
<div class="d-flex align-items-center gap-2">
<label for="per_page" class="form-label mb-0">Show:</label>
<select id="per_page" class="form-select form-select-sm w-auto">
<div class="flex items-center justify-between mb-6 p-4 bg-gray-50 rounded-lg">
<div class="flex items-center gap-4">
<div class="text-sm text-gray-700">
Showing {{ (pagination.current_page - 1) * pagination.per_page + 1 }} to {{ min(pagination.current_page * pagination.per_page, pagination.total_items) }} of {{ pagination.total_items }} games
</div>
<div class="flex items-center gap-2">
<label for="per_page_top" class="text-sm font-medium text-gray-700">Show:</label>
<select id="per_page_top" class="text-sm border border-gray-300 rounded px-2 py-1 focus:ring-blue-500 focus:border-blue-500">
<option value="12" {{ pagination.per_page == 12 ? 'selected' : '' }}>12</option>
<option value="24" {{ pagination.per_page == 24 ? 'selected' : '' }}>24</option>
<option value="48" {{ pagination.per_page == 48 ? 'selected' : '' }}>48</option>
<option value="100" {{ pagination.per_page == 100 ? 'selected' : '' }}>100</option>
</select>
<span class="text-muted small">per page</span>
<span class="text-sm text-gray-600">per page</span>
</div>
</div>
<div class="d-flex align-items-center gap-2">
<div class="flex items-center space-x-2">
<!-- Previous Button -->
{% if pagination.has_prev %}
<a href="?page={{ pagination.prev_page }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}&view={{ view_mode }}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for platform in filters.platforms %}&platforms[]={{ platform }}{% endfor %}"
class="btn btn-outline-secondary btn-sm">
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-700 transition-colors flex items-center">
<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 19l-7-7 7-7"/>
</svg>
Previous
</a>
{% else %}
<span class="px-3 py-2 text-sm font-medium text-gray-300 bg-gray-100 border border-gray-200 rounded-lg cursor-not-allowed flex items-center">
<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 19l-7-7 7-7"/>
</svg>
Previous
</span>
{% endif %}
<div class="btn-group" role="group">
{% for page_num in range(max(1, pagination.current_page - 2), min(pagination.total_pages, pagination.current_page + 2)) %}
<a href="?page={{ page_num }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}&view={{ view_mode }}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for platform in filters.platforms %}&platforms[]={{ platform }}{% endfor %}"
class="btn btn-sm {{ page_num == pagination.current_page ? 'btn-primary' : 'btn-outline-secondary' }}">
{{ page_num }}
</a>
{% endfor %}
</div>
<!-- Page Numbers -->
{% set start_page = max(1, pagination.current_page - 2) %}
{% set end_page = min(pagination.total_pages, pagination.current_page + 2) %}
{% if start_page > 1 %}
<a href="?page=1{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}&view={{ view_mode }}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for platform in filters.platforms %}&platforms[]={{ platform }}{% endfor %}"
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-700 transition-colors">1</a>
{% if start_page > 2 %}
<span class="px-2 py-2 text-sm font-medium text-gray-500">...</span>
{% endif %}
{% endif %}
{% for page_num in start_page..end_page %}
{% if page_num == pagination.current_page %}
<span class="px-3 py-2 text-sm font-medium text-white bg-blue-600 border border-blue-600 rounded-lg">{{ page_num }}</span>
{% else %}
<a href="?page={{ page_num }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}&view={{ view_mode }}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for platform in filters.platforms %}&platforms[]={{ platform }}{% endfor %}"
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-700 transition-colors">{{ page_num }}</a>
{% endif %}
{% endfor %}
{% if end_page < pagination.total_pages %}
{% if end_page < pagination.total_pages - 1 %}
<span class="px-2 py-2 text-sm font-medium text-gray-500">...</span>
{% endif %}
<a href="?page={{ pagination.total_pages }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}&view={{ view_mode }}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for platform in filters.platforms %}&platforms[]={{ platform }}{% endfor %}"
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-700 transition-colors">{{ pagination.total_pages }}</a>
{% endif %}
<!-- Next Button -->
{% if pagination.has_next %}
<a href="?page={{ pagination.next_page }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}&view={{ view_mode }}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for platform in filters.platforms %}&platforms[]={{ platform }}{% endfor %}"
class="btn btn-outline-secondary btn-sm">
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-700 transition-colors flex items-center">
Next
<svg class="w-4 h-4 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</a>
{% else %}
<span class="px-3 py-2 text-sm font-medium text-gray-300 bg-gray-100 border border-gray-200 rounded-lg cursor-not-allowed flex items-center">
Next
<svg class="w-4 h-4 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</span>
{% endif %}
</div>
</div>
@@ -429,6 +524,13 @@ document.getElementById('per_page')?.addEventListener('change', function() {
url.searchParams.set('page', '1'); // Reset to first page
window.location = url.toString();
});
document.getElementById('per_page_top')?.addEventListener('change', function() {
const url = new URL(window.location);
url.searchParams.set('per_page', this.value);
url.searchParams.set('page', '1'); // Reset to first page
window.location = url.toString();
});
</script>
{% endblock %}

File diff suppressed because it is too large Load Diff

View File

@@ -6,11 +6,9 @@
<title>{{ title }} - Media Collector</title>
<!-- Iconify Icons -->
<script src="https://code.iconify.design/iconify-icon/1.0.7/iconify-icon.min.js"></script>
<!-- Bootstrap CSS CDN -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Select2 CSS -->
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet">
<!-- In the head section of app.twig -->
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<!-- Tailwind CSS -->
{% if app_env == 'production' %}
{% if manifest['resources/css/app.css'] is defined %}
<link rel="stylesheet" href="{{ base_url() }}/build/assets/{{ manifest['resources/css/app.css'].file }}">
@@ -18,6 +16,7 @@
{% else %}
<link rel="stylesheet" href="{{ base_url() }}/css/app.css">
{% endif %}
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="{{ base_url() }}/favicon.svg">
@@ -30,107 +29,364 @@
{{ debugbarRenderer.renderHead()|raw }}
{% endif %}
<!-- In the head section -->
<!-- Alpine.js -->
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
<!-- Before closing </head> tag -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- jQuery (required for Select2) -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<!-- Select2 JS -->
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
{% if app_env == 'production' and manifest['resources/js/app.js'] is defined %}
<!-- jQuery (required for Select2) -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<!-- Select2 CSS -->
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet">
<!-- Select2 JS -->
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
{% if app_env == 'production' and manifest['resources/js/app.js'] is defined %}
<script type="module" src="{{ base_url() }}/build/assets/{{ manifest['resources/js/app.js'].file }}"></script>
{% else %}
{% else %}
<script type="module" src="{{ base_url() }}/resources/js/app.js"></script>
{% endif %}
{% endif %}
</head>
<body class="bg-light">
<body class="bg-gray-50 text-gray-900 min-h-screen" x-data="{ mobileMenuOpen: false }">
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-primary shadow-lg">
<div class="container-fluid">
<a class="navbar-brand fw-bold" href="{{ path_for('home') }}">Media Collector</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link {% if current_route == 'home' %}active{% endif %}" href="{{ path_for('home') }}">Dashboard</a>
</li>
{% if is_media_type_visible('games') %}
<li class="nav-item">
<a class="nav-link {% if current_route == 'games.index' %}active{% endif %}" href="{{ path_for('games.index') }}">Games</a>
</li>
{% endif %}
{% if is_media_type_visible('movies') %}
<li class="nav-item">
<a class="nav-link {% if current_route == 'movies.index' %}active{% endif %}" href="{{ path_for('movies.index') }}">Movies</a>
</li>
{% endif %}
{% if is_media_type_visible('tvshows') %}
<li class="nav-item">
<a class="nav-link {% if current_route == 'tvshows.index' %}active{% endif %}" href="{{ path_for('tvshows.index') }}">TV Shows</a>
</li>
{% endif %}
{% if is_media_type_visible('music') %}
<li class="nav-item">
<a class="nav-link {% if current_route == 'music.index' %}active{% endif %}" href="{{ path_for('music.index') }}">Music</a>
</li>
{% endif %}
{% if is_media_type_visible('adult') %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {% if current_route == 'adult.index' or current_route == 'actors.index' %}active{% endif %}" href="#" id="adultDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Adult Videos
</a>
<ul class="dropdown-menu" aria-labelledby="adultDropdown">
<li><a class="dropdown-item {% if current_route == 'adult.index' %}active{% endif %}" href="{{ path_for('adult.index') }}">Videos</a></li>
<li><a class="dropdown-item {% if current_route == 'actors.index' %}active{% endif %}" href="{{ path_for('actors.index') }}">Performers</a></li>
</ul>
</li>
{% endif %}
</ul>
<div class="d-flex align-items-center">
<!-- Search Link -->
<a href="{{ path_for('search.index') }}" class="text-white text-decoration-none me-3">
<svg class="me-1" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
<nav class="bg-slate-900 text-white shadow-lg">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16">
<!-- Left Section: App Title + Mobile Menu -->
<div class="flex items-center">
<!-- Mobile menu button -->
<div class="flex items-center lg:hidden mr-4">
<button @click="mobileMenuOpen = !mobileMenuOpen" class="text-white hover:text-gray-300 focus:outline-none focus:text-gray-300">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path x-show="!mobileMenuOpen" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
<path x-show="mobileMenuOpen" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
Search
</a>
<!-- Admin Link (only for admins) -->
{% if is_admin() %}
<a href="/admin" class="text-white text-decoration-none me-3">Admin</a>
{% endif %}
<!-- User Menu -->
<div class="dropdown">
<button class="btn btn-link text-white text-decoration-none dropdown-toggle" type="button" id="userDropdown" data-bs-toggle="dropdown" aria-expanded="false">
<span>{{ current_user().username }}</span>
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
<li><div class="dropdown-header">Signed in as<br><strong>{{ current_user().username }}</strong></div></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="/logout">Sign out</a></li>
</ul>
</div>
<!-- App Title -->
<div class="flex items-center">
<a href="{{ path_for('dashboard.index') }}" class="text-xl font-bold text-white hover:text-gray-300 transition-colors">
Media Collector
</a>
</div>
</div>
<!-- Center Section: Search Bar -->
<div class="flex-1 max-w-lg mx-4 lg:mx-8">
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<form action="{{ path_for('search.index') }}" method="GET" class="w-full">
<input
type="text"
name="q"
placeholder="Search media..."
class="block w-full pl-10 pr-3 py-2 border border-gray-600 rounded-md leading-5 bg-slate-800 text-white placeholder-gray-400 focus:outline-none focus:bg-slate-700 focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
autocomplete="off"
>
</form>
</div>
</div>
<!-- Right Section: Page-specific controls -->
<div class="flex items-center space-x-4">
{% block nav_controls %}{% endblock %}
</div>
</div>
</div>
</nav>
<!-- Mobile Sidebar Overlay -->
<div x-show="mobileMenuOpen" @click="mobileMenuOpen = false" class="fixed inset-0 z-40 lg:hidden" x-transition:enter="transition-opacity ease-linear duration-300" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition-opacity ease-linear duration-300" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0">
<div class="absolute inset-0 bg-gray-600 opacity-75"></div>
</div>
<!-- Mobile Sidebar -->
<div x-show="mobileMenuOpen" class="fixed inset-y-0 left-0 z-50 w-80 bg-slate-800 text-white shadow-xl lg:hidden transform transition-transform duration-300 ease-in-out" x-transition:enter="transform -translate-x-full" x-transition:enter-start="-translate-x-full" x-transition:enter-end="translate-x-0" x-transition:leave="transform -translate-x-full" x-transition:leave-start="translate-x-0" x-transition:leave-end="-translate-x-full">
<div class="flex flex-col h-full">
<!-- Mobile Sidebar Header -->
<div class="flex items-center justify-between p-4 border-b border-slate-700">
<h2 class="text-lg font-semibold">Library</h2>
<button @click="mobileMenuOpen = false" class="p-1 rounded-md hover:bg-slate-700">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<!-- Mobile Navigation Items -->
<nav class="flex-1 px-2 py-4 space-y-1 overflow-y-auto">
<a href="{{ path_for('dashboard.index') }}" class="flex items-center px-3 py-2 text-sm font-medium rounded-md {% if current_route == 'dashboard.index' %}bg-slate-900 text-white{% else %}text-slate-300 hover:bg-slate-700 hover:text-white{% endif %}">
<svg class="mr-3 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5a2 2 0 012-2h4a2 2 0 012 2v2H8V5z" />
</svg>
Dashboard
</a>
{% if is_media_type_visible('games') %}
<a href="{{ path_for('games.index') }}" class="flex items-center px-3 py-2 text-sm font-medium rounded-md {% if current_route == 'games.index' %}bg-slate-900 text-white{% else %}text-slate-300 hover:bg-slate-700 hover:text-white{% endif %}">
<svg class="mr-3 h-5 w-5" 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>
Games
</a>
{% endif %}
{% if is_media_type_visible('movies') %}
<a href="{{ path_for('movies.index') }}" class="flex items-center px-3 py-2 text-sm font-medium rounded-md {% if current_route == 'movies.index' %}bg-slate-900 text-white{% else %}text-slate-300 hover:bg-slate-700 hover:text-white{% endif %}">
<svg class="mr-3 h-5 w-5" 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>
Movies
</a>
{% endif %}
{% if is_media_type_visible('tvshows') %}
<a href="{{ path_for('tvshows.index') }}" class="flex items-center px-3 py-2 text-sm font-medium rounded-md {% if current_route == 'tvshows.index' %}bg-slate-900 text-white{% else %}text-slate-300 hover:bg-slate-700 hover:text-white{% endif %}">
<svg class="mr-3 h-5 w-5" 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>
TV Shows
</a>
{% endif %}
{% if is_media_type_visible('music') %}
<a href="{{ path_for('music.index') }}" class="flex items-center px-3 py-2 text-sm font-medium rounded-md {% if current_route == 'music.index' %}bg-slate-900 text-white{% else %}text-slate-300 hover:bg-slate-700 hover:text-white{% endif %}">
<svg class="mr-3 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3" />
</svg>
Music
</a>
{% endif %}
{% if is_media_type_visible('adult') %}
<div class="space-y-1">
<div class="flex items-center px-3 py-2 text-sm font-medium text-slate-300">
<svg class="mr-3 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
Adult
</div>
<a href="{{ path_for('adult.index') }}" class="flex items-center ml-6 px-3 py-2 text-sm font-medium rounded-md {% if current_route == 'adult.index' %}bg-slate-900 text-white{% else %}text-slate-400 hover:bg-slate-700 hover:text-white{% endif %}">
Videos
</a>
<a href="{{ path_for('actors.index') }}" class="flex items-center ml-6 px-3 py-2 text-sm font-medium rounded-md {% if current_route == 'actors.index' %}bg-slate-900 text-white{% else %}text-slate-400 hover:bg-slate-700 hover:text-white{% endif %}">
Performers
</a>
</div>
{% endif %}
</nav>
</div>
</div>
<!-- DebugBar -->
{% if debugbarRenderer is defined %}
{{ debugbarRenderer.render()|raw }}
{% endif %}
<!-- Page Content -->
<main class="container-fluid py-4">
<main class="flex min-h-screen">
<!-- Left Sidebar -->
<aside class="w-64 bg-slate-800 text-white shadow-lg hidden lg:block" x-data="{ collapsed: false }">
<div class="flex flex-col h-full">
<!-- Sidebar Header -->
<div class="flex items-center justify-between p-4 border-b border-slate-700">
<h2 class="text-lg font-semibold" x-show="!collapsed">Library</h2>
<button @click="collapsed = !collapsed" class="p-1 rounded-md hover:bg-slate-700">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path x-show="!collapsed" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
<path x-show="collapsed" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</button>
</div>
<!-- Navigation Items -->
<nav class="flex-1 px-2 py-4 space-y-1" x-show="!collapsed">
<a href="{{ path_for('dashboard.index') }}" class="flex items-center px-3 py-2 text-sm font-medium rounded-md {% if current_route == 'dashboard.index' %}bg-slate-900 text-white{% else %}text-slate-300 hover:bg-slate-700 hover:text-white{% endif %}">
<svg class="mr-3 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5a2 2 0 012-2h4a2 2 0 012 2v2H8V5z" />
</svg>
Dashboard
</a>
{% if is_media_type_visible('games') %}
<a href="{{ path_for('games.index') }}" class="flex items-center px-3 py-2 text-sm font-medium rounded-md {% if current_route == 'games.index' %}bg-slate-900 text-white{% else %}text-slate-300 hover:bg-slate-700 hover:text-white{% endif %}">
<svg class="mr-3 h-5 w-5" 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>
Games
</a>
{% endif %}
{% if is_media_type_visible('movies') %}
<a href="{{ path_for('movies.index') }}" class="flex items-center px-3 py-2 text-sm font-medium rounded-md {% if current_route == 'movies.index' %}bg-slate-900 text-white{% else %}text-slate-300 hover:bg-slate-700 hover:text-white{% endif %}">
<svg class="mr-3 h-5 w-5" 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>
Movies
</a>
{% endif %}
{% if is_media_type_visible('tvshows') %}
<a href="{{ path_for('tvshows.index') }}" class="flex items-center px-3 py-2 text-sm font-medium rounded-md {% if current_route == 'tvshows.index' %}bg-slate-900 text-white{% else %}text-slate-300 hover:bg-slate-700 hover:text-white{% endif %}">
<svg class="mr-3 h-5 w-5" 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>
TV Shows
</a>
{% endif %}
{% if is_media_type_visible('music') %}
<a href="{{ path_for('music.index') }}" class="flex items-center px-3 py-2 text-sm font-medium rounded-md {% if current_route == 'music.index' %}bg-slate-900 text-white{% else %}text-slate-300 hover:bg-slate-700 hover:text-white{% endif %}">
<svg class="mr-3 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3" />
</svg>
Music
</a>
{% endif %}
{% if is_media_type_visible('adult') %}
<div class="space-y-1">
<div class="flex items-center px-3 py-2 text-sm font-medium text-slate-300">
<svg class="mr-3 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
Adult
</div>
<a href="{{ path_for('adult.index') }}" class="flex items-center ml-6 px-3 py-2 text-sm font-medium rounded-md {% if current_route == 'adult.index' %}bg-slate-900 text-white{% else %}text-slate-400 hover:bg-slate-700 hover:text-white{% endif %}">
Videos
</a>
<a href="{{ path_for('actors.index') }}" class="flex items-center ml-6 px-3 py-2 text-sm font-medium rounded-md {% if current_route == 'actors.index' %}bg-slate-900 text-white{% else %}text-slate-400 hover:bg-slate-700 hover:text-white{% endif %}">
Performers
</a>
</div>
{% endif %}
</nav>
<!-- Collapsed State Icon -->
<div class="px-2 py-4" x-show="collapsed">
<div class="space-y-2">
<a href="{{ path_for('dashboard.index') }}" class="flex justify-center p-2 text-slate-300 hover:bg-slate-700 hover:text-white rounded-md" title="Dashboard">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5a2 2 0 012-2h4a2 2 0 012 2v2H8V5z" />
</svg>
</a>
{% if is_media_type_visible('games') %}
<a href="{{ path_for('games.index') }}" class="flex justify-center p-2 text-slate-300 hover:bg-slate-700 hover:text-white rounded-md" title="Games">
<svg class="h-5 w-5" 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>
</a>
{% endif %}
{% if is_media_type_visible('movies') %}
<a href="{{ path_for('movies.index') }}" class="flex justify-center p-2 text-slate-300 hover:bg-slate-700 hover:text-white rounded-md" title="Movies">
<svg class="h-5 w-5" 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>
</a>
{% endif %}
</div>
</div>
</div>
</aside>
<!-- Main Content Area with Second Sidebar -->
<div class="flex-1 flex">
<!-- Second Left Sidebar (Grouped List - only in list mode) -->
<aside class="w-80 bg-white shadow-lg border-r border-gray-200 {% if view_mode != 'list' %}hidden{% endif %}" x-data="{ collapsed: false }">
<div class="flex flex-col h-full">
<!-- Sidebar Header -->
<div class="flex items-center justify-between p-4 border-b border-gray-200">
<h2 class="text-lg font-semibold text-gray-900" x-show="!collapsed">Items</h2>
<button @click="collapsed = !collapsed" class="p-1 rounded-md hover:bg-gray-100">
<svg class="h-5 w-5 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path x-show="!collapsed" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
<path x-show="collapsed" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</button>
</div>
<!-- Grouped Items List -->
<div class="flex-1 p-4 overflow-y-auto" x-show="!collapsed">
{% block item_list %}
<div class="space-y-4">
<!-- Grouped items will be populated by individual pages -->
<div class="text-sm text-gray-500 text-center py-8">
No items to display
</div>
</div>
{% endblock %}
</div>
</div>
</aside>
<!-- Content -->
<div class="flex-1 min-w-0">
{% block content %}{% endblock %}
</div>
<!-- Right Sidebar -->
<aside class="w-80 bg-white shadow-lg border-l border-gray-200 hidden xl:block" x-data="{ collapsed: false }">
<div class="flex flex-col h-full">
<!-- Sidebar Header -->
<div class="flex items-center justify-between p-4 border-b border-gray-200">
<h2 class="text-lg font-semibold text-gray-900" x-show="!collapsed">Details</h2>
<button @click="collapsed = !collapsed" class="p-1 rounded-md hover:bg-gray-100">
<svg class="h-5 w-5 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path x-show="!collapsed" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
<path x-show="collapsed" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</button>
</div>
<!-- Details Content -->
<div class="flex-1 p-4" x-show="!collapsed">
{% block sidebar %}
<div class="space-y-4">
<!-- Quick Stats -->
<div class="bg-gray-50 rounded-lg p-4">
<h3 class="text-sm font-medium text-gray-900 mb-3">Quick Stats</h3>
<div class="space-y-2">
<div class="flex justify-between text-sm">
<span class="text-gray-600">Total Items</span>
<span class="font-medium text-gray-900">1,234</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-gray-600">Recently Added</span>
<span class="font-medium text-gray-900">23</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-gray-600">Favorites</span>
<span class="font-medium text-gray-900">45</span>
</div>
</div>
</div>
<!-- Recent Activity -->
<div class="bg-gray-50 rounded-lg p-4">
<h3 class="text-sm font-medium text-gray-900 mb-3">Recent Activity</h3>
<div class="space-y-2">
<div class="text-xs text-gray-600">
<div class="font-medium text-gray-900">Added 3 new movies</div>
<div>2 hours ago</div>
</div>
<div class="text-xs text-gray-600">
<div class="font-medium text-gray-900">Updated game metadata</div>
<div>5 hours ago</div>
</div>
</div>
</div>
</div>
{% endblock %}
</div>
</div>
</aside>
</div>
</main>
</body>
</html>

View File

@@ -1,15 +1,149 @@
{% extends "layouts/app.twig" %}
{% block content %}
<div class="container-fluid">
<div class="row">
<!-- Sidebar with filters -->
<div class="col-lg-3 col-xl-2">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Filters</h5>
{% block nav_controls %}
<!-- Search form -->
<form method="GET" class="flex gap-2">
<input type="hidden" name="view" value="{{ view_mode }}">
<input type="hidden" name="per_page" value="{{ pagination.per_page }}">
{% for genre in filters.genres %}
<input type="hidden" name="genres[]" value="{{ genre }}">
{% endfor %}
{% for director in filters.directors %}
<input type="hidden" name="directors[]" value="{{ director }}">
{% endfor %}
<div class="relative">
<input
type="text"
name="search"
value="{{ search }}"
placeholder="Search movies..."
class="pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500 w-64 bg-gray-800 text-white placeholder-gray-400"
>
<svg class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
</div>
<div class="card-body">
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
Search
</button>
</form>
<!-- View mode switcher -->
<div class="flex gap-1" role="group">
{% for mode in view_modes %}
<a
href="?view={{ mode }}&sort={{ sort }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for director in filters.directors %}&directors[]={{ director }}{% endfor %}"
class="inline-flex items-center px-3 py-2 border border-gray-300 bg-gray-800 text-gray-300 hover:bg-gray-700 hover:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 {{ view_mode == mode ? 'bg-blue-600 border-blue-500 text-white' : '' }}"
title="{{ mode|title }} View"
>
{% if mode == 'grid' %}
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"/>
</svg>
{% endif %}
{% if mode == 'list' %}
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"/>
</svg>
{% endif %}
{% if mode == 'covers' %}
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
{% endif %}
<span class="hidden sm:inline ml-1">{{ mode|title }}</span>
</a>
{% endfor %}
</div>
<!-- Sort dropdown -->
<div class="relative" x-data="{ open: false }">
<button @click="open = !open" class="inline-flex items-center px-3 py-2 border border-gray-300 rounded-md bg-gray-800 text-gray-300 hover:bg-gray-700 hover:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
<svg class="mr-1 h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4h14M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12"/>
</svg>
<span class="hidden sm:inline">Sort</span>
</button>
<div x-show="open" @click.away="open = false" class="absolute right-0 z-50 mt-2 w-56 bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5">
<div class="py-1">
{% for key, label in sort_options %}
<a class="flex items-center justify-between px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 {{ sort == key ? 'bg-gray-50' : '' }}"
href="?sort={{ key }}&view={{ view_mode }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for director in filters.directors %}&directors[]={{ director }}{% endfor %}">
{{ label }}
{% if sort == key %}
<svg class="h-4 w-4 text-blue-600" fill="currentColor" viewBox="0 0 16 16">
<path d="M12.5 15a.5.5 0 0 1-.5-.5v-13a.5.5 0 0 1 1 0v13a.5.5 0 0 1-.5.5zM10 8a.5.5 0 0 1-.5.5H3.707l2.147 2.146a.5.5 0 0 1-.708.708l-3-3a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L3.707 7.5H9.5a.5.5 0 0 1 .5.5z"/>
</svg>
{% endif %}
</a>
{% endfor %}
</div>
</div>
</div>
{% endblock %}
{% block item_list %}
{% if view_mode == 'list' and movies %}
<div class="space-y-4">
<!-- Group movies by first letter -->
{% set grouped_movies = {} %}
{% for movie in movies %}
{% set first_letter = movie.title|first|upper %}
{% if grouped_movies[first_letter] is not defined %}
{% set grouped_movies = grouped_movies|merge({(first_letter): []}) %}
{% endif %}
{% set grouped_movies = grouped_movies|merge({(first_letter): grouped_movies[first_letter]|merge([movie])}) %}
{% endfor %}
{% for letter, letter_movies in grouped_movies|sort %}
<div class="space-y-2">
<h4 class="text-sm font-semibold text-gray-900 border-b border-gray-200 pb-1">{{ letter }}</h4>
<div class="space-y-1">
{% for movie in letter_movies %}
<a href="{{ path_for('movies.show', {'id': movie.id}) }}" class="flex items-center p-2 rounded-md hover:bg-gray-100 transition-colors">
{% if movie.poster_url %}
<img class="w-8 h-12 object-cover rounded mr-3 flex-shrink-0" src="/images/{{ movie.poster_url }}" alt="{{ movie.title }}">
{% else %}
<div class="w-8 h-12 bg-gray-200 rounded mr-3 flex-shrink-0 flex items-center justify-center">
<svg class="w-4 h-4 text-gray-400" 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 min-w-0">
<div class="text-sm font-medium text-gray-900 truncate">{{ movie.title }}</div>
{% if movie.release_date %}
<div class="text-xs text-gray-500">{{ movie.release_date|date('Y') }}</div>
{% endif %}
</div>
{% if movie.watched %}
<div class="flex-shrink-0 ml-2">
<svg class="w-4 h-4 text-green-600" fill="currentColor" viewBox="0 0 16 16">
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z"/>
</svg>
</div>
{% endif %}
</a>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="space-y-4">
<div class="text-sm text-gray-500 text-center py-8">
No items to display
</div>
</div>
{% endif %}
{% endblock %}
{% block sidebar %}
<div class="space-y-4">
<!-- Filters -->
<div class="bg-gray-50 rounded-lg p-4">
<h3 class="text-sm font-medium text-gray-900 mb-4">Filters</h3>
<!-- Filter form -->
<form method="GET" id="filterForm">
<input type="hidden" name="view" value="{{ view_mode }}">
@@ -19,8 +153,8 @@
<!-- Genre filter -->
{% if available_filters.genres %}
<div class="mb-4">
<h6 class="fw-bold text-dark mb-2">Genres</h6>
<select class="form-select select2" name="genres[]" multiple data-placeholder="Select genres...">
<label class="block text-xs font-medium text-gray-700 mb-1">Genres</label>
<select class="select2 w-full text-sm border border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500" name="genres[]" multiple data-placeholder="Select genres...">
{% for genre in available_filters.genres %}
<option value="{{ genre }}" {{ genre in filters.genres ? 'selected' : '' }}>
{{ genre }}
@@ -33,8 +167,8 @@
<!-- Director filter -->
{% if available_filters.directors %}
<div class="mb-4">
<h6 class="fw-bold text-dark mb-2">Directors</h6>
<select class="form-select select2" name="directors[]" multiple data-placeholder="Select directors...">
<label class="block text-xs font-medium text-gray-700 mb-1">Directors</label>
<select class="select2 w-full text-sm border border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500" name="directors[]" multiple data-placeholder="Select directors...">
{% for director in available_filters.directors %}
<option value="{{ director }}" {{ director in filters.directors ? 'selected' : '' }}>
{{ director }}
@@ -45,140 +179,116 @@
{% endif %}
<!-- Filter actions -->
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary btn-sm">
<div class="space-y-2">
<button type="submit" class="w-full bg-blue-600 text-white px-3 py-2 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 text-sm">
Apply Filters
</button>
<a href="{{ path_for('movies.index') }}" class="btn btn-outline-secondary btn-sm">
<a href="{{ path_for('movies.index') }}" class="w-full bg-gray-100 text-gray-700 px-3 py-2 rounded-md hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 block text-center text-sm">
Clear All
</a>
</div>
</form>
</div>
</div>
</div>
<!-- Main content area -->
<div class="col-lg-9 col-xl-10">
<div class="px-4 py-3">
<!-- Header with search and view controls -->
<div class="d-flex flex-column flex-sm-row justify-content-between align-items-start align-items-sm-center mb-4 gap-3">
<div>
<h1 class="display-4 fw-bold text-dark">Movies</h1>
<!-- Active Filters Summary -->
{% if filters.genres or filters.directors or search %}
<div class="bg-blue-50 rounded-lg p-4">
<h3 class="text-sm font-medium text-gray-900 mb-3">Active Filters</h3>
<div class="space-y-2">
{% if search %}
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600">Search: "{{ search }}"</span>
<a href="?{% for key, value in filters %}{% if key != 'search' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-blue-600 hover:text-blue-800">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</a>
</div>
{% endif %}
{% for genre in filters.genres %}
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600">Genre: {{ genre }}</span>
<a href="?{% for key, value in filters %}{% if key != 'genres' or (key == 'genres' and value != genre) %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-blue-600 hover:text-blue-800">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</a>
</div>
{% endfor %}
{% for director in filters.directors %}
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600">Director: {{ director }}</span>
<a href="?{% for key, value in filters %}{% if key != 'directors' or (key == 'directors' and value != director) %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-blue-600 hover:text-blue-800">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</a>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Quick Stats -->
<div class="bg-gray-50 rounded-lg p-4">
<h3 class="text-sm font-medium text-gray-900 mb-3">Quick Stats</h3>
<div class="space-y-2">
<div class="flex justify-between text-sm">
<span class="text-gray-600">Total Movies</span>
<span class="font-medium text-gray-900">{{ pagination.total_items }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-gray-600">This Page</span>
<span class="font-medium text-gray-900">{{ movies|length }}</span>
</div>
{% if pagination.total_pages > 1 %}
<div class="flex justify-between text-sm">
<span class="text-gray-600">Page</span>
<span class="font-medium text-gray-900">{{ pagination.current_page }} of {{ pagination.total_pages }}</span>
</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}
{% block content %}
<!-- Main content area -->
<div class="p-6">
<!-- Header -->
<div class="mb-6">
<h1 class="text-3xl font-bold text-gray-900">Movies</h1>
{% if pagination.total_items > 0 %}
<div class="text-muted small mt-1">
<div class="text-gray-500 text-sm mt-1">
{{ pagination.total_items }} movies
{% if search %}
matching "{{ search }}"
{% endif %}
{% if filters.genres or filters.directors %}
{% if filters.genres %}
<span class="badge bg-primary ms-1">{{ filters.genres|join(', ') }}</span>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 ml-2">{{ filters.genres|join(', ') }}</span>
{% endif %}
{% if filters.directors %}
<span class="badge bg-secondary ms-1">{{ filters.directors|join(', ') }}</span>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 ml-2">{{ filters.directors|join(', ') }}</span>
{% endif %}
{% endif %}
</div>
{% endif %}
</div>
<div class="d-flex flex-column flex-sm-row gap-3 w-100 w-sm-auto">
<!-- Search form -->
<form method="GET" class="d-flex gap-2">
<input type="hidden" name="view" value="{{ view_mode }}">
<input type="hidden" name="per_page" value="{{ pagination.per_page }}">
{% for genre in filters.genres %}
<input type="hidden" name="genres[]" value="{{ genre }}">
{% endfor %}
{% for director in filters.directors %}
<input type="hidden" name="directors[]" value="{{ director }}">
{% endfor %}
<div class="position-relative">
<input
type="text"
name="search"
value="{{ search }}"
placeholder="Search movies..."
class="form-control ps-5"
>
<svg class="position-absolute top-50 start-0 translate-middle-y ms-3 text-muted" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
</div>
<button type="submit" class="btn btn-primary">
Search
</button>
</form>
<!-- View mode switcher -->
<div class="btn-group me-2" role="group">
{% for mode in view_modes %}
<a
href="?view={{ mode }}&sort={{ sort }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for director in filters.directors %}&directors[]={{ director }}{% endfor %}"
class="btn btn-outline-secondary {{ view_mode == mode ? 'active' : '' }}"
title="{{ mode|title }} View"
>
{% if mode == 'grid' %}
<svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"/>
</svg>
{% endif %}
{% if mode == 'list' %}
<svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"/>
</svg>
{% endif %}
{% if mode == 'covers' %}
<svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
{% endif %}
<span class="d-none d-sm-inline ms-1">{{ mode|title }}</span>
</a>
{% endfor %}
</div>
<!-- Sort dropdown -->
<div class="dropdown">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" id="sortDropdown" data-bs-toggle="dropdown" aria-expanded="false">
<svg class="me-1" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12"/>
</svg>
<span class="d-none d-sm-inline">Sort</span>
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="sortDropdown">
{% for key, label in sort_options %}
<li>
<a class="dropdown-item {% if sort == key %}active{% endif %}"
href="?sort={{ key }}&view={{ view_mode }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for director in filters.directors %}&directors[]={{ director }}{% endfor %}">
{{ label }}
{% if sort == key %}
<svg class="float-end mt-1" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M12.5 15a.5.5 0 0 1-.5-.5v-13a.5.5 0 0 1 1 0v13a.5.5 0 0 1-.5.5zM10 8a.5.5 0 0 1-.5.5H3.707l2.147 2.146a.5.5 0 0 1-.708.708l-3-3a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L3.707 7.5H9.5a.5.5 0 0 1 .5.5z"/>
</svg>
{% endif %}
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
{% if movies is empty %}
<div class="text-center py-5">
<svg class="mx-auto text-muted mb-3" width="48" height="48" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<div class="text-center py-12">
<svg class="mx-auto text-gray-400 mb-4 h-12 w-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>
<h3 class="h5 fw-medium text-dark">
<h3 class="text-lg font-medium text-gray-900 mb-2">
{% if search or filters.genres or filters.directors %}
No movies found matching your criteria
{% else %}
No movies found
{% endif %}
</h3>
<p class="text-muted">
<p class="text-gray-500 mb-4">
{% if search or filters.genres or filters.directors %}
Try adjusting your search terms or filters.
{% else %}
@@ -186,7 +296,7 @@
{% endif %}
</p>
{% if search or filters.genres or filters.directors %}
<a href="{{ path_for('movies.index') }}" class="btn btn-primary mt-3">
<a href="{{ path_for('movies.index') }}" class="inline-flex items-center px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
Clear filters
</a>
{% endif %}
@@ -195,28 +305,28 @@
<!-- Movies content based on view mode -->
{% if view_mode == 'list' %}
<!-- List view -->
<div class="card">
<ul class="list-group list-group-flush">
<div class="bg-white rounded-lg shadow-md border border-gray-200">
<ul class="divide-y divide-gray-200">
{% for movie in movies %}
<li class="list-group-item">
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<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 me-3" style="width: 64px; height: 96px; object-fit: cover;" src="/images/{{ movie.poster_url }}" alt="{{ movie.title }}">
<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-light rounded me-3 d-flex align-items-center justify-content-center" style="width: 64px; height: 96px;">
<svg class="text-muted" width="32" height="32" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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-grow-1">
<h3 class="h6 mb-1">
<a href="{{ path_for('movies.show', {'id': movie.id}) }}" class="text-decoration-none">
<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="d-flex align-items-center gap-3 small text-muted">
<div class="flex items-center gap-3 text-sm text-gray-600">
{% if movie.release_date %}
<span>{{ movie.release_date|date('Y') }}</span>
{% endif %}
@@ -230,14 +340,14 @@
</div>
</div>
</div>
<div class="d-flex gap-2">
<div class="flex gap-2">
{% if movie.watched %}
<span class="badge bg-success">
<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="badge bg-danger">
<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 %}
@@ -249,77 +359,88 @@
</div>
{% elseif view_mode == 'covers' %}
<!-- Cover grid view -->
<div class="items">
<div class="row g-3">
<!-- Enhanced Cover grid view -->
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-4">
{% for movie in movies %}
<figure class="item" style="padding:0px">
<a href="{{ path_for('movies.show', {'id': movie.id}) }}" class="text-decoration-none">
<img src="/images/{{ movie.poster_url }}" />
<figcaption>{{ movie.title }}</figcaption>
</a>
</figure>
<!--
<div class="col-6 col-sm-4 col-md-3 col-lg-2">
<div class="card h-100">
<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="position-relative" style="aspect-ratio: 2/3; overflow: hidden;">
<img src="/images/{{ movie.poster_url }}" alt="{{ movie.title }}" class="card-img-top" style="width: 100%; height: 100%; object-fit: cover;">
<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 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 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="d-flex align-items-center justify-content-center bg-light" style="aspect-ratio: 2/3; min-height: 200px;">
<svg class="text-muted" width="48" height="48" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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="card-body">
<h6 class="card-title text-truncate" title="{{ movie.title }}">
<a href="{{ path_for('movies.show', {'id': movie.id}) }}" class="text-decoration-none">
<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="card-text small text-muted">{{ movie.release_date|date('Y') }}</p>
<p class="text-xs text-gray-600 font-medium">{{ movie.release_date|date('Y') }}</p>
{% endif %}
{% if 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>
</div>-->
{% endfor %}
</div>
{% else %}
<!-- Default grid view -->
<div class="row g-3">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-4">
{% for movie in movies %}
<div class="col-12 col-md-6 col-lg-3 col-xl-2">
<div class="card h-100">
<div class="card-body">
<div class="d-flex align-items-center">
<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-light rounded d-flex align-items-center justify-content-center" style="width: 64px; height: 96px;">
<svg class="text-muted" width="32" height="32" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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="ms-3 flex-grow-1">
<h5 class="card-title mb-1">
<a href="{{ path_for('movies.show', {'id': movie.id}) }}" class="text-decoration-none">
<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="d-flex align-items-center gap-2 small text-muted mb-2">
<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 %}
@@ -328,7 +449,7 @@
{% endif %}
</div>
{% if movie.source_name %}
<p class="card-text small text-muted mb-2">
<p class="text-sm text-gray-600 mb-2">
{{ movie.source_name }}
</p>
{% endif %}
@@ -336,23 +457,23 @@
</div>
{% if movie.overview %}
<div class="mt-3">
<p class="card-text small text-muted" style="display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;">
<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 d-flex justify-content-between align-items-center small text-muted">
<div class="mt-3 flex justify-between items-center text-sm text-gray-600">
{% if movie.runtime_minutes %}
<span>{{ (movie.runtime_minutes / 60)|round(1) }}h {{ movie.runtime_minutes % 60 }}m</span>
{% endif %}
<div class="d-flex gap-1">
<div class="flex gap-1">
{% if movie.watched %}
<span class="badge bg-success">
<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="badge bg-danger">
<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 %}
@@ -360,47 +481,93 @@
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
<!-- Pagination -->
<!-- Top Pagination & Controls -->
{% if pagination.total_pages > 1 %}
<div class="d-flex align-items-center justify-content-between mt-4">
<div class="d-flex align-items-center gap-2">
<label for="per_page" class="form-label mb-0">Show:</label>
<select id="per_page" class="form-select form-select-sm w-auto">
<div class="flex items-center justify-between mb-6 p-4 bg-gray-50 rounded-lg">
<div class="flex items-center gap-4">
<div class="text-sm text-gray-700">
Showing {{ (pagination.current_page - 1) * pagination.per_page + 1 }} to {{ min(pagination.current_page * pagination.per_page, pagination.total_items) }} of {{ pagination.total_items }} movies
</div>
<div class="flex items-center gap-2">
<label for="per_page_top" class="text-sm font-medium text-gray-700">Show:</label>
<select id="per_page_top" class="text-sm border border-gray-300 rounded px-2 py-1 focus:ring-blue-500 focus:border-blue-500">
<option value="12" {{ pagination.per_page == 12 ? 'selected' : '' }}>12</option>
<option value="24" {{ pagination.per_page == 24 ? 'selected' : '' }}>24</option>
<option value="48" {{ pagination.per_page == 48 ? 'selected' : '' }}>48</option>
<option value="100" {{ pagination.per_page == 100 ? 'selected' : '' }}>100</option>
</select>
<span class="text-muted small">per page</span>
<span class="text-sm text-gray-600">per page</span>
</div>
</div>
<div class="d-flex align-items-center gap-2">
<div class="flex items-center space-x-2">
<!-- Previous Button -->
{% if pagination.has_prev %}
<a href="?page={{ pagination.prev_page }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}&view={{ view_mode }}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for director in filters.directors %}&directors[]={{ director }}{% endfor %}"
class="btn btn-outline-secondary btn-sm">
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-700 transition-colors flex items-center">
<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 19l-7-7 7-7"/>
</svg>
Previous
</a>
{% else %}
<span class="px-3 py-2 text-sm font-medium text-gray-300 bg-gray-100 border border-gray-200 rounded-lg cursor-not-allowed flex items-center">
<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 19l-7-7 7-7"/>
</svg>
Previous
</span>
{% endif %}
<div class="btn-group" role="group">
{% for page_num in range(max(1, pagination.current_page - 2), min(pagination.total_pages, pagination.current_page + 2)) %}
<a href="?page={{ page_num }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}&view={{ view_mode }}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for director in filters.directors %}&directors[]={{ director }}{% endfor %}"
class="btn btn-sm {{ page_num == pagination.current_page ? 'btn-primary' : 'btn-outline-secondary' }}">
{{ page_num }}
</a>
{% endfor %}
</div>
<!-- Page Numbers -->
{% set start_page = max(1, pagination.current_page - 2) %}
{% set end_page = min(pagination.total_pages, pagination.current_page + 2) %}
{% if start_page > 1 %}
<a href="?page=1{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}&view={{ view_mode }}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for director in filters.directors %}&directors[]={{ director }}{% endfor %}"
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-700 transition-colors">1</a>
{% if start_page > 2 %}
<span class="px-2 py-2 text-sm font-medium text-gray-500">...</span>
{% endif %}
{% endif %}
{% for page_num in start_page..end_page %}
{% if page_num == pagination.current_page %}
<span class="px-3 py-2 text-sm font-medium text-white bg-blue-600 border border-blue-600 rounded-lg">{{ page_num }}</span>
{% else %}
<a href="?page={{ page_num }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}&view={{ view_mode }}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for director in filters.directors %}&directors[]={{ director }}{% endfor %}"
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-700 transition-colors">{{ page_num }}</a>
{% endif %}
{% endfor %}
{% if end_page < pagination.total_pages %}
{% if end_page < pagination.total_pages - 1 %}
<span class="px-2 py-2 text-sm font-medium text-gray-500">...</span>
{% endif %}
<a href="?page={{ pagination.total_pages }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}&view={{ view_mode }}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for director in filters.directors %}&directors[]={{ director }}{% endfor %}"
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-700 transition-colors">{{ pagination.total_pages }}</a>
{% endif %}
<!-- Next Button -->
{% if pagination.has_next %}
<a href="?page={{ pagination.next_page }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}&view={{ view_mode }}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for director in filters.directors %}&directors[]={{ director }}{% endfor %}"
class="btn btn-outline-secondary btn-sm">
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-700 transition-colors flex items-center">
Next
<svg class="w-4 h-4 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</a>
{% else %}
<span class="px-3 py-2 text-sm font-medium text-gray-300 bg-gray-100 border border-gray-200 rounded-lg cursor-not-allowed flex items-center">
Next
<svg class="w-4 h-4 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</span>
{% endif %}
</div>
</div>
@@ -504,6 +671,13 @@ document.getElementById('per_page')?.addEventListener('change', function() {
url.searchParams.set('page', '1'); // Reset to first page
window.location = url.toString();
});
document.getElementById('per_page_top')?.addEventListener('change', function() {
const url = new URL(window.location);
url.searchParams.set('per_page', this.value);
url.searchParams.set('page', '1'); // Reset to first page
window.location = url.toString();
});
</script>
{% endblock %}

View File

@@ -1,66 +1,37 @@
{% extends "layouts/app.twig" %}
{% block content %}
<div class="container-fluid px-4 py-3">
<!-- Hero Section with Backdrop -->
<div class="relative">
{% if movie.backdrop_url %}
<div class="h-96 md:h-[500px] relative overflow-hidden">
<img src="/images/{{ movie.backdrop_url }}" alt="{{ movie.title }} backdrop" class="w-full h-full object-cover">
<div class="absolute inset-0 bg-gradient-to-t from-black via-black/50 to-transparent"></div>
<div class="absolute inset-0 bg-gradient-to-r from-black/80 via-black/40 to-transparent"></div>
<!-- Back button -->
<div class="mb-4">
<a href="{{ path_for('movies.index') }}" class="btn btn-link text-decoration-none p-0">
<svg class="me-2" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<div class="absolute top-4 left-4 z-10">
<a href="{{ path_for('movies.index') }}" class="inline-flex items-center text-white hover:text-gray-300 transition-colors bg-black/20 backdrop-blur-sm rounded-full px-4 py-2">
<svg class="mr-2" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
</svg>
Back to Movies
</a>
</div>
<div class="card shadow-sm">
<div class="row g-0">
<!-- Movie poster -->
<div class="col-md-4">
<div class="card-body p-4">
<div class="ratio ratio-2x3">
{% if movie.poster_url %}
<img src="/images/{{ movie.poster_url }}" alt="{{ movie.title }}" class="img-fluid rounded">
{% else %}
<div class="w-100 h-100 d-flex align-items-center justify-content-center bg-light rounded">
<svg class="text-muted" width="96" height="96" 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>
<!-- Action buttons -->
<div class="mt-4 d-grid gap-2">
<button class="btn btn-primary">
<svg class="me-2" width="16" height="16" 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>
Mark as Watched
</button>
<button class="btn btn-outline-danger">
<svg class="me-2" width="16" height="16" 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>
Add to Favorites
</button>
</div>
</div>
</div>
<!-- Movie details -->
<div class="col-md-8">
<div class="card-body p-4">
<div class="mb-4">
<h1 class="display-4 fw-bold text-dark mb-2">{{ movie.title }}</h1>
<!-- Hero Content -->
<div class="absolute bottom-0 left-0 right-0 p-6 md:p-8">
<div class="max-w-4xl">
<h1 class="text-4xl md:text-6xl font-bold text-white mb-2 drop-shadow-lg">{{ movie.title }}</h1>
{% if movie.tagline %}
<p class="lead text-muted mb-3">{{ movie.tagline }}</p>
<p class="text-xl md:text-2xl text-gray-200 mb-4 drop-shadow-md">{{ movie.tagline }}</p>
{% endif %}
<!-- Movie metadata -->
<div class="d-flex flex-wrap gap-3 small text-muted mb-3">
<!-- Quick Info -->
<div class="flex flex-wrap gap-4 text-sm text-white mb-4">
{% if movie.release_date %}
<span class="d-flex align-items-center">
<svg class="me-1" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<span class="flex items-center bg-black/30 backdrop-blur-sm rounded-full px-3 py-1">
<svg class="mr-2" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
{{ movie.release_date|date('Y') }}
@@ -68,8 +39,8 @@
{% endif %}
{% if movie.rating %}
<span class="d-flex align-items-center">
<svg class="me-1" width="16" height="16" fill="currentColor" viewBox="0 0 20 20">
<span class="flex items-center bg-black/30 backdrop-blur-sm rounded-full px-3 py-1">
<svg class="mr-2" width="16" height="16" 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>
{{ movie.rating }}/10
@@ -77,123 +48,195 @@
{% endif %}
{% if movie.runtime_minutes %}
<span class="d-flex align-items-center">
<svg class="me-1" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<span class="flex items-center bg-black/30 backdrop-blur-sm rounded-full px-3 py-1">
<svg class="mr-2" width="16" height="16" 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>
{{ (movie.runtime_minutes / 60)|round(1) }}h {{ movie.runtime_minutes % 60 }}m
</span>
{% endif %}
{% if movie.vote_count %}
<span class="d-flex align-items-center">
<svg class="me-1" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/>
</svg>
{{ movie.vote_count }} votes
</span>
{% endif %}
<span class="d-flex align-items-center">
<svg class="me-1" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/>
</svg>
{{ movie.source_name }}
</span>
</div>
<!-- Status badges -->
<div class="d-flex flex-wrap gap-2">
<div class="flex flex-wrap gap-2 mb-4">
{% if movie.watched %}
<span class="badge bg-success d-flex align-items-center">
<svg class="me-1" width="12" height="12" fill="currentColor" viewBox="0 0 20 20">
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-500 text-white">
<svg class="mr-2" width="14" height="14" 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>
Watched
</span>
{% endif %}
{% if movie.watch_count > 0 %}
<span class="badge bg-primary">{{ movie.watch_count }} watch{{ movie.watch_count > 1 ? 'es' : '' }}</span>
{% endif %}
{% if movie.is_favorite %}
<span class="badge bg-danger d-flex align-items-center">
<svg class="me-1" width="12" height="12" fill="currentColor" viewBox="0 0 20 20">
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-red-500 text-white">
<svg class="mr-2" width="14" height="14" fill="currentColor" viewBox="0 0 20 20">
<path d="M3.172 5.172a4 4 0 015.656 0L10 6.343l1.172-1.171a4 4 0 115.656 5.656L10 17.657l-6.828-6.829a4 4 0 010-5.656z"/>
</svg>
Favorite
</span>
{% endif %}
</div>
{% if movie.status %}
<span class="badge bg-secondary">{{ movie.status|title }}</span>
<!-- Action buttons -->
<div class="flex flex-wrap gap-3">
<button class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg font-medium transition-colors flex items-center">
<svg class="mr-2" width="18" height="18" 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>
Mark as Watched
</button>
<button class="bg-white/10 hover:bg-white/20 backdrop-blur-sm text-white border border-white/30 px-6 py-3 rounded-lg font-medium transition-colors flex items-center">
<svg class="mr-2" width="18" height="18" 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>
Add to Favorites
</button>
</div>
</div>
</div>
</div>
{% else %}
<!-- Fallback hero without backdrop -->
<div class="bg-gradient-to-r from-blue-900 to-purple-900 h-64 relative">
<div class="absolute top-4 left-4 z-10">
<a href="{{ path_for('movies.index') }}" class="inline-flex items-center text-white hover:text-gray-300 transition-colors">
<svg class="mr-2" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
</svg>
Back to Movies
</a>
</div>
<div class="absolute bottom-0 left-0 right-0 p-6">
<h1 class="text-4xl font-bold text-white mb-2">{{ movie.title }}</h1>
{% if movie.tagline %}
<p class="text-xl text-gray-200">{{ movie.tagline }}</p>
{% endif %}
</div>
</div>
{% endif %}
</div>
<!-- Main Content -->
<div class="max-w-7xl mx-auto px-4 py-8">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Poster and Quick Actions -->
<div class="lg:col-span-1">
<div class="sticky top-4">
<!-- Poster -->
<div class="bg-white rounded-xl shadow-lg overflow-hidden mb-6">
<div class="aspect-[2/3]">
{% if movie.poster_url %}
<img src="/images/{{ movie.poster_url }}" alt="{{ movie.title }}" class="w-full h-full object-cover">
{% else %}
<div class="w-full h-full flex items-center justify-center bg-gradient-to-br from-gray-100 to-gray-200">
<svg class="text-gray-400 w-16 h-16" 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>
<!-- Quick Stats -->
<div class="bg-white rounded-xl shadow-lg p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Quick Stats</h3>
<div class="space-y-3">
{% if movie.vote_count %}
<div class="flex justify-between items-center">
<span class="text-gray-600">Votes</span>
<span class="font-medium">{{ movie.vote_count|number_format }}</span>
</div>
{% endif %}
{% if movie.watch_count %}
<div class="flex justify-between items-center">
<span class="text-gray-600">Watch Count</span>
<span class="font-medium">{{ movie.watch_count }}</span>
</div>
{% endif %}
<div class="flex justify-between items-center">
<span class="text-gray-600">Source</span>
<span class="font-medium">{{ movie.source_name }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- Main Details -->
<div class="lg:col-span-2 space-y-8">
<!-- Overview -->
{% if movie.overview %}
<div class="mb-4">
<h2 class="h5 fw-semibold text-dark mb-2">Overview</h2>
<p class="text-muted">{{ movie.overview }}</p>
<div class="bg-white rounded-xl shadow-lg p-6">
<h2 class="text-2xl font-bold text-gray-900 mb-4">Overview</h2>
<p class="text-gray-700 leading-relaxed">{{ movie.overview }}</p>
</div>
{% endif %}
<!-- Cast & Crew Section -->
<div class="mb-4">
<h2 class="h5 fw-semibold text-dark mb-3">Cast & Crew</h2>
<div class="row g-3">
<!-- Cast & Crew -->
<div class="bg-white rounded-xl shadow-lg p-6">
<h2 class="text-2xl font-bold text-gray-900 mb-6">Cast & Crew</h2>
<!-- Director and Writer -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
{% if movie.director %}
<div class="col-md-6">
<div class="d-flex align-items-center">
<svg class="me-2 text-muted" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<div class="flex items-start">
<div class="bg-blue-100 rounded-full p-3 mr-4">
<svg class="text-blue-600" width="24" height="24" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
<div>
<small class="text-muted d-block">Director</small>
<span class="fw-medium">{{ movie.director }}</span>
</div>
<div>
<h3 class="font-semibold text-gray-900">Director</h3>
<p class="text-gray-600">{{ movie.director }}</p>
</div>
</div>
{% endif %}
{% if movie.writer %}
<div class="col-md-6">
<div class="d-flex align-items-center">
<svg class="me-2 text-muted" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<div class="flex items-start">
<div class="bg-green-100 rounded-full p-3 mr-4">
<svg class="text-green-600" width="24" height="24" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
</svg>
<div>
<small class="text-muted d-block">Writer</small>
<span class="fw-medium">{{ movie.writer }}</span>
</div>
<div>
<h3 class="font-semibold text-gray-900">Writer</h3>
<p class="text-gray-600">{{ movie.writer }}</p>
</div>
</div>
{% endif %}
</div>
<!-- Cast -->
{% if actors %}
<div class="mt-3">
<small class="text-muted d-block mb-2">Cast</small>
<div class="d-flex flex-wrap gap-2">
<div>
<h3 class="text-lg font-semibold text-gray-900 mb-4">Cast</h3>
<div class="grid grid-cols-2 md:grid-cols-3 gap-4">
{% for actor in actors %}
<span class="badge bg-light text-dark">{{ actor.name }}</span>
<div class="flex items-center p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
<div class="w-10 h-10 bg-gradient-to-br from-blue-400 to-purple-500 rounded-full flex items-center justify-center mr-3">
<span class="text-white font-medium text-sm">{{ actor.name|first|upper }}</span>
</div>
<div>
<p class="font-medium text-gray-900 text-sm">{{ actor.name }}</p>
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
<!-- Movie Details Grid -->
<div class="row g-4 mb-4">
<!-- Details Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Genres -->
{% if movie.genre %}
<div class="col-md-4">
<h3 class="h6 fw-semibold text-dark mb-3">Genres</h3>
<div class="d-flex flex-wrap gap-2">
<div class="bg-white rounded-xl shadow-lg p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Genres</h3>
<div class="flex flex-wrap gap-2">
{% for genre in movie.genre|split(',') %}
<span class="badge bg-primary">{{ genre|trim }}</span>
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800">{{ genre|trim }}</span>
{% endfor %}
</div>
</div>
@@ -201,18 +244,18 @@
<!-- Production -->
{% if movie.production_companies or movie.production_countries %}
<div class="col-md-4">
<h3 class="h6 fw-semibold text-dark mb-3">Production</h3>
<div class="mb-2">
<div class="bg-white rounded-xl shadow-lg p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Production</h3>
{% if movie.production_companies %}
<small class="text-muted d-block">Companies</small>
<span class="fw-medium">{{ movie.production_companies }}</span>
{% endif %}
<div class="mb-3">
<p class="text-sm text-gray-600 mb-1">Companies</p>
<p class="font-medium">{{ movie.production_companies }}</p>
</div>
{% endif %}
{% if movie.production_countries %}
<div class="mb-2">
<small class="text-muted d-block">Countries</small>
<span class="fw-medium">{{ movie.production_countries }}</span>
<div>
<p class="text-sm text-gray-600 mb-1">Countries</p>
<p class="font-medium">{{ movie.production_countries }}</p>
</div>
{% endif %}
</div>
@@ -220,131 +263,152 @@
<!-- Technical Details -->
{% if movie.budget or movie.revenue or movie.original_language %}
<div class="col-md-4">
<h3 class="h6 fw-semibold text-dark mb-3">Technical</h3>
<div class="mb-2">
<div class="bg-white rounded-xl shadow-lg p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Technical</h3>
{% if movie.budget %}
<small class="text-muted d-block">Budget</small>
<span class="fw-medium">${{ movie.budget|number_format(0, '.', ',') }}</span>
{% endif %}
<div class="mb-3">
<p class="text-sm text-gray-600 mb-1">Budget</p>
<p class="font-medium">${{ movie.budget|number_format(0, '.', ',') }}</p>
</div>
{% endif %}
{% if movie.revenue %}
<div class="mb-2">
<small class="text-muted d-block">Revenue</small>
<span class="fw-medium">${{ movie.revenue|number_format(0, '.', ',') }}</span>
<div class="mb-3">
<p class="text-sm text-gray-600 mb-1">Revenue</p>
<p class="font-medium">${{ movie.revenue|number_format(0, '.', ',') }}</p>
</div>
{% endif %}
{% if movie.original_language %}
<div class="mb-2">
<small class="text-muted d-block">Language</small>
<span class="fw-medium">{{ movie.original_language|upper }}</span>
<div>
<p class="text-sm text-gray-600 mb-1">Language</p>
<p class="font-medium">{{ movie.original_language|upper }}</p>
</div>
{% endif %}
</div>
{% endif %}
</div>
<!-- Collection/Series Info -->
<!-- Collection Info -->
{% if movie.belongs_to_collection %}
<div class="mb-4 p-3 bg-light rounded">
<h3 class="h6 fw-semibold text-dark mb-2">Collection</h3>
<span class="fw-medium">{{ movie.belongs_to_collection }}</span>
</div>
{% endif %}
<!-- Streaming & Availability -->
{% if movie.streaming_providers or movie.availability %}
<div class="mb-4">
<h3 class="h5 fw-semibold text-dark mb-3">Where to Watch</h3>
<div class="row g-3">
{% if movie.streaming_providers %}
<div class="col-md-6">
<small class="text-muted d-block mb-2">Streaming</small>
<div class="d-flex flex-wrap gap-2">
{% for provider in movie.streaming_providers %}
<span class="badge bg-success">{{ provider }}</span>
{% endfor %}
</div>
</div>
{% endif %}
{% if movie.availability %}
<div class="col-md-6">
<small class="text-muted d-block mb-2">Availability</small>
<span class="fw-medium">{{ movie.availability }}</span>
</div>
{% endif %}
</div>
</div>
{% endif %}
<!-- User Reviews & Ratings -->
{% if movie.user_reviews or movie.critic_reviews %}
<div class="mb-4">
<h3 class="h5 fw-semibold text-dark mb-3">Reviews & Ratings</h3>
<div class="row g-4">
{% if movie.user_reviews %}
<div class="col-md-6">
<h4 class="h6 fw-semibold text-dark mb-2">User Reviews</h4>
<div class="vstack gap-3">
{% for review in movie.user_reviews[:3] %}
<div class="p-3 bg-light rounded">
<div class="d-flex align-items-center mb-2">
<div class="d-flex text-warning">
{% for i in 1..5 %}
<svg class="me-1" width="16" height="16" fill="{{ i <= review.rating ? 'currentColor' : 'none' }}" 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"/>
<div class="bg-white rounded-xl shadow-lg p-6">
<h2 class="text-2xl font-bold text-gray-900 mb-4">Collection</h2>
<div class="flex items-center">
<div class="bg-purple-100 rounded-full p-3 mr-4">
<svg class="text-purple-600" width="24" height="24" 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>
{% endfor %}
</div>
<span class="ms-2 small text-muted">{{ review.author }}</span>
<div>
<h3 class="font-semibold text-gray-900">{{ movie.belongs_to_collection }}</h3>
<p class="text-gray-600">Part of a collection</p>
</div>
<p class="small text-muted">{{ review.content|slice(0, 150) }}{% if review.content|length > 150 %}...{% endif %}</p>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% if movie.critic_reviews %}
<div class="col-md-6">
<h4 class="h6 fw-semibold text-dark mb-2">Critic Reviews</h4>
<div class="vstack gap-3">
{% for review in movie.critic_reviews[:3] %}
<div class="p-3 bg-light rounded">
<div class="d-flex align-items-center justify-content-between mb-2">
<span class="fw-medium">{{ review.publication }}</span>
<span class="badge bg-info">{{ review.rating }}/100</span>
</div>
<p class="small text-muted">{{ review.excerpt|slice(0, 150) }}{% if review.excerpt|length > 150 %}...{% endif %}</p>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
</div>
{% endif %}
<!-- Metadata (for debugging/advanced users) -->
<!-- Technical Details & Metadata -->
{% if metadata %}
<div class="mt-4 pt-4 border-top">
<div class="space-y-6">
<h2 class="text-2xl font-bold text-gray-900">Technical Details & Metadata</h2>
<!-- Jellyfin ID -->
{% if metadata.jellyfin_id %}
<div class="bg-white rounded-xl shadow-lg p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-3 flex items-center">
<svg class="mr-2 text-blue-600" width="20" height="20" 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>
Jellyfin ID
</h3>
<div class="bg-gray-50 rounded-lg p-3">
<code class="text-sm text-gray-800 font-mono">{{ metadata.jellyfin_id }}</code>
</div>
</div>
{% endif %}
<!-- Genres -->
{% if metadata.genres %}
<div class="bg-white rounded-xl shadow-lg p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-3 flex items-center">
<svg class="mr-2 text-green-600" width="20" height="20" 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>
Genres
</h3>
<div class="flex flex-wrap gap-2">
{% for genre in metadata.genres %}
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-100 text-green-800">{{ genre }}</span>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Studios -->
{% if metadata.studios %}
<div class="bg-white rounded-xl shadow-lg p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-3 flex items-center">
<svg class="mr-2 text-purple-600" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/>
</svg>
Studios
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
{% for studio in metadata.studios %}
<div class="bg-gray-50 rounded-lg p-3">
<p class="font-medium text-gray-900">{{ studio.Name }}</p>
<p class="text-xs text-gray-600 font-mono">{{ studio.Id }}</p>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Image URLs -->
{% if metadata.poster_url or metadata.backdrop_url %}
<div class="bg-white rounded-xl shadow-lg p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-3 flex items-center">
<svg class="mr-2 text-orange-600" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
Image URLs
</h3>
<div class="space-y-3">
{% if metadata.poster_url %}
<div>
<p class="text-sm font-medium text-gray-700 mb-1">Poster URL</p>
<div class="bg-gray-50 rounded-lg p-3 overflow-x-auto">
<code class="text-xs text-gray-800 font-mono break-all">{{ metadata.poster_url }}</code>
</div>
</div>
{% endif %}
{% if metadata.backdrop_url %}
<div>
<p class="text-sm font-medium text-gray-700 mb-1">Backdrop URL</p>
<div class="bg-gray-50 rounded-lg p-3 overflow-x-auto">
<code class="text-xs text-gray-800 font-mono break-all">{{ metadata.backdrop_url }}</code>
</div>
</div>
{% endif %}
</div>
</div>
{% endif %}
<!-- Raw Metadata (Collapsible) -->
<div class="bg-white rounded-xl shadow-lg p-6">
<details class="group">
<summary class="cursor-pointer small fw-medium text-muted hover:text-dark d-flex align-items-center">
<svg class="me-2 group-open:rotate-90 transition-transform" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<summary class="cursor-pointer flex items-center text-gray-700 hover:text-gray-900 transition-colors">
<svg class="mr-2 group-open:rotate-90 transition-transform" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
Technical Details & Metadata
<span class="font-medium">Full Raw Metadata</span>
</summary>
<div class="mt-3 small">
<pre class="bg-light p-3 rounded"><code>{{ metadata|json_encode(constant('JSON_PRETTY_PRINT')) }}</code></pre>
<div class="mt-4 bg-gray-50 rounded-lg p-4 overflow-x-auto">
<pre class="text-xs text-gray-800 whitespace-pre-wrap"><code>{{ metadata|json_encode(constant('JSON_PRETTY_PRINT')) }}</code></pre>
</div>
</details>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,15 +1,63 @@
{% extends "layouts/app.twig" %}
{% block content %}
<div class="container-fluid">
<div class="row">
<!-- Sidebar with filters -->
<div class="col-lg-3 col-xl-2">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Filters</h5>
{% block nav_controls %}
<!-- Search form -->
<form method="GET" class="flex gap-2">
<input type="hidden" name="view" value="{{ view_mode }}">
<input type="hidden" name="per_page" value="{{ pagination.per_page }}">
{% for genre in filters.genres %}
<input type="hidden" name="genres[]" value="{{ genre }}">
{% endfor %}
{% for year in filters.years %}
<input type="hidden" name="years[]" value="{{ year }}">
{% endfor %}
<div class="relative">
<input
type="text"
name="search"
value="{{ search }}"
placeholder="Search TV shows..."
class="pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500 w-64 bg-gray-800 text-white placeholder-gray-400"
>
<svg class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
</div>
<div class="card-body">
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
Search
</button>
</form>
<!-- View mode switcher -->
<div class="flex gap-1" role="group">
{% for mode in view_modes %}
<a
href="?view={{ mode }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for year in filters.years %}&years[]={{ year }}{% endfor %}"
class="inline-flex items-center px-3 py-2 border border-gray-300 bg-gray-800 text-gray-300 hover:bg-gray-700 hover:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 {{ view_mode == mode ? 'bg-blue-600 border-blue-500 text-white' : '' }}"
title="{{ mode|title }} View"
>
{% if mode == 'grid' %}
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"/>
</svg>
{% endif %}
{% if mode == 'list' %}
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"/>
</svg>
{% endif %}
<span class="hidden sm:inline ml-1">{{ mode|title }}</span>
</a>
{% endfor %}
</div>
{% endblock %}
{% block sidebar %}
<div class="space-y-4">
<!-- Filters -->
<div class="bg-gray-50 rounded-lg p-4">
<h3 class="text-sm font-medium text-gray-900 mb-4">Filters</h3>
<!-- Filter form -->
<form method="GET" id="filterForm">
<input type="hidden" name="view" value="{{ view_mode }}">
@@ -19,8 +67,8 @@
<!-- Genre filter -->
{% if available_filters.genres %}
<div class="mb-4">
<h6 class="fw-bold text-dark mb-2">Genres</h6>
<select class="form-select select2" name="genres[]" multiple data-placeholder="Select genres...">
<label class="block text-xs font-medium text-gray-700 mb-1">Genres</label>
<select class="select2 w-full text-sm border border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500" name="genres[]" multiple data-placeholder="Select genres...">
{% for genre in available_filters.genres %}
<option value="{{ genre }}" {{ genre in filters.genres ? 'selected' : '' }}>
{{ genre }}
@@ -33,8 +81,8 @@
<!-- Year filter -->
{% if available_filters.years %}
<div class="mb-4">
<h6 class="fw-bold text-dark mb-2">Years</h6>
<select class="form-select select2" name="years[]" multiple data-placeholder="Select years...">
<label class="block text-xs font-medium text-gray-700 mb-1">Years</label>
<select class="select2 w-full text-sm border border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500" name="years[]" multiple data-placeholder="Select years...">
{% for year in available_filters.years %}
<option value="{{ year }}" {{ year in filters.years ? 'selected' : '' }}>
{{ year }}
@@ -45,96 +93,103 @@
{% endif %}
<!-- Filter actions -->
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary btn-sm">
<div class="space-y-2">
<button type="submit" class="w-full bg-blue-600 text-white px-3 py-2 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 text-sm">
Apply Filters
</button>
<a href="{{ path_for('tvshows.index') }}" class="btn btn-outline-secondary btn-sm">
<a href="{{ path_for('tvshows.index') }}" class="w-full bg-gray-100 text-gray-700 px-3 py-2 rounded-md hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 block text-center text-sm">
Clear All
</a>
</div>
</form>
</div>
</div>
</div>
<!-- Main content area -->
<div class="col-lg-9 col-xl-10">
<div class="px-4 py-3">
<!-- Header with search and view controls -->
<div class="d-flex flex-column flex-sm-row justify-content-between align-items-start align-items-sm-center mb-4 gap-3">
<div>
<h1 class="display-4 fw-bold text-dark">TV Shows</h1>
<!-- Active Filters Summary -->
{% if filters.genres or filters.years or search %}
<div class="bg-blue-50 rounded-lg p-4">
<h3 class="text-sm font-medium text-gray-900 mb-3">Active Filters</h3>
<div class="space-y-2">
{% if search %}
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600">Search: "{{ search }}"</span>
<a href="?{% for key, value in filters %}{% if key != 'search' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-blue-600 hover:text-blue-800">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</a>
</div>
{% endif %}
{% for genre in filters.genres %}
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600">Genre: {{ genre }}</span>
<a href="?{% for key, value in filters %}{% if key != 'genres' or (key == 'genres' and value != genre) %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-blue-600 hover:text-blue-800">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</a>
</div>
{% endfor %}
{% for year in filters.years %}
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600">Year: {{ year }}</span>
<a href="?{% for key, value in filters %}{% if key != 'years' or (key == 'years' and value != year) %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-blue-600 hover:text-blue-800">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</a>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Quick Stats -->
<div class="bg-gray-50 rounded-lg p-4">
<h3 class="text-sm font-medium text-gray-900 mb-3">Quick Stats</h3>
<div class="space-y-2">
<div class="flex justify-between text-sm">
<span class="text-gray-600">Total TV Shows</span>
<span class="font-medium text-gray-900">{{ pagination.total_items }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-gray-600">This Page</span>
<span class="font-medium text-gray-900">{{ tvshows|length }}</span>
</div>
{% if pagination.total_pages > 1 %}
<div class="flex justify-between text-sm">
<span class="text-gray-600">Page</span>
<span class="font-medium text-gray-900">{{ pagination.current_page }} of {{ pagination.total_pages }}</span>
</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}
{% block content %}
<!-- Main content area -->
<div class="p-6">
<!-- Header -->
<div class="mb-6">
<h1 class="text-3xl font-bold text-gray-900">TV Shows</h1>
{% if pagination.total_items > 0 %}
<div class="text-muted small mt-1">
<div class="text-gray-500 text-sm mt-1">
{{ pagination.total_items }} TV shows
{% if search %}
matching "{{ search }}"
{% endif %}
{% if filters.genres or filters.years %}
{% if filters.genres %}
<span class="badge bg-primary ms-1">{{ filters.genres|join(', ') }}</span>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 ml-2">{{ filters.genres|join(', ') }}</span>
{% endif %}
{% if filters.years %}
<span class="badge bg-secondary ms-1">{{ filters.years|join(', ') }}</span>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 ml-2">{{ filters.years|join(', ') }}</span>
{% endif %}
{% endif %}
</div>
{% endif %}
</div>
<div class="d-flex flex-column flex-sm-row gap-3 w-100 w-sm-auto">
<!-- Search form -->
<form method="GET" class="d-flex gap-2">
<input type="hidden" name="view" value="{{ view_mode }}">
<input type="hidden" name="per_page" value="{{ pagination.per_page }}">
{% for genre in filters.genres %}
<input type="hidden" name="genres[]" value="{{ genre }}">
{% endfor %}
{% for year in filters.years %}
<input type="hidden" name="years[]" value="{{ year }}">
{% endfor %}
<div class="position-relative">
<input
type="text"
name="search"
value="{{ search }}"
placeholder="Search TV shows..."
class="form-control ps-5"
>
<svg class="position-absolute top-50 start-0 translate-middle-y ms-3 text-muted" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
</div>
<button type="submit" class="btn btn-primary">
Search
</button>
</form>
<!-- View mode switcher -->
<div class="btn-group" role="group">
{% for mode in view_modes %}
<a
href="?view={{ mode }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for year in filters.years %}&years[]={{ year }}{% endfor %}"
class="btn btn-outline-secondary {{ view_mode == mode ? 'active' : '' }}"
>
{% if mode == 'grid' %}
<svg class="me-1" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"/>
</svg>
{% endif %}
{% if mode == 'list' %}
<svg class="me-1" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"/>
</svg>
{% endif %}
{{ mode|title }}
</a>
{% endfor %}
</div>
</div>
</div>
{% if tvshows is empty %}
<div class="text-center py-5">
<svg class="mx-auto text-muted mb-3" width="48" height="48" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@@ -164,28 +219,28 @@
<!-- TV Shows content based on view mode -->
{% if view_mode == 'list' %}
<!-- List view -->
<div class="card">
<ul class="list-group list-group-flush">
<div class="bg-white rounded-lg shadow-md border border-gray-200">
<ul class="divide-y divide-gray-200">
{% for tvshow in tvshows %}
<li class="list-group-item">
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<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 me-3" style="width: 64px; height: 96px; object-fit: cover;" src="/images/{{ tvshow.poster_url }}" alt="{{ tvshow.title }}">
<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-light rounded me-3 d-flex align-items-center justify-content-center" style="width: 64px; height: 96px;">
<svg class="text-muted" width="32" height="32" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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-grow-1">
<h3 class="h6 mb-1">
<a href="{{ path_for('tvshows.show', {'id': tvshow.id}) }}" class="text-decoration-none">
<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="d-flex align-items-center gap-3 small text-muted">
<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 %}
@@ -202,9 +257,9 @@
</div>
</div>
</div>
<div class="d-flex gap-2">
<div class="flex gap-2">
{% if tvshow.is_favorite %}
<span class="badge bg-danger">
<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 %}
@@ -217,62 +272,59 @@
{% elseif view_mode == 'covers' %}
<!-- Cover grid view -->
<div class="row g-3">
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4">
{% for tvshow in tvshows %}
<div class="col-6 col-sm-4 col-md-3 col-lg-2">
<div class="card h-100">
<div class="bg-white rounded-lg shadow-md border border-gray-200 overflow-hidden h-full">
{% if tvshow.poster_url %}
<div class="position-relative" style="aspect-ratio: 2/3; overflow: hidden;">
<img src="/images/{{ tvshow.poster_url }}" alt="{{ tvshow.title }}" class="card-img-top" style="width: 100%; height: 100%; object-fit: cover;">
<div class="relative aspect-[2/3] overflow-hidden">
<img src="/images/{{ tvshow.poster_url }}" alt="{{ tvshow.title }}" class="w-full h-full object-cover">
</div>
{% else %}
<div class="d-flex align-items-center justify-content-center bg-light" style="aspect-ratio: 2/3; min-height: 200px;">
<svg class="text-muted" width="48" height="48" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<div class="flex items-center justify-center bg-gray-100 aspect-[2/3] min-h-[200px]">
<svg class="text-gray-600" width="48" height="48" 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="card-body">
<h6 class="card-title text-truncate" title="{{ tvshow.title }}">
<a href="{{ path_for('tvshows.show', {'id': tvshow.id}) }}" class="text-decoration-none">
<div class="p-3">
<h6 class="text-sm font-semibold truncate" title="{{ tvshow.title }}">
<a href="{{ path_for('tvshows.show', {'id': tvshow.id}) }}" class="no-underline text-gray-900 hover:text-blue-600">
{{ tvshow.title }}
</a>
</h6>
{% if tvshow.first_air_date %}
<p class="card-text small text-muted">{{ tvshow.first_air_date|date('Y') }}</p>
<p class="text-xs text-gray-600 mt-1">{{ tvshow.first_air_date|date('Y') }}</p>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<!-- Default grid view -->
<div class="row g-3">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-4">
{% for tvshow in tvshows %}
<div class="col-12 col-md-6 col-lg-3 col-xl-2">
<div class="card h-100">
<div class="card-body">
<div class="d-flex align-items-center">
<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-light rounded d-flex align-items-center justify-content-center" style="width: 64px; height: 96px;">
<svg class="text-muted" width="32" height="32" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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="ms-3 flex-grow-1">
<h5 class="card-title mb-1">
<a href="{{ path_for('tvshows.show', {'id': tvshow.id}) }}" class="text-decoration-none">
<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="d-flex align-items-center gap-2 small text-muted mb-2">
<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 %}
@@ -281,7 +333,7 @@
{% endif %}
</div>
{% if tvshow.source_name %}
<p class="card-text small text-muted mb-2">
<p class="text-sm text-gray-600 mb-2">
{{ tvshow.source_name }}
</p>
{% endif %}
@@ -289,18 +341,18 @@
</div>
{% if tvshow.overview %}
<div class="mt-3">
<p class="card-text small text-muted" style="display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;">
<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 d-flex justify-content-between align-items-center small text-muted">
<div class="mt-3 flex justify-between items-center text-sm text-gray-600">
{% if tvshow.number_of_seasons %}
<span>{{ tvshow.number_of_seasons }} season{{ tvshow.number_of_seasons > 1 ? 's' : '' }}</span>
{% endif %}
<div class="d-flex gap-1">
<div class="flex gap-1">
{% if tvshow.is_favorite %}
<span class="badge bg-danger">
<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 %}
@@ -308,47 +360,93 @@
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
<!-- Pagination -->
<!-- Top Pagination & Controls -->
{% if pagination.total_pages > 1 %}
<div class="d-flex align-items-center justify-content-between mt-4">
<div class="d-flex align-items-center gap-2">
<label for="per_page" class="form-label mb-0">Show:</label>
<select id="per_page" class="form-select form-select-sm w-auto">
<div class="flex items-center justify-between mb-6 p-4 bg-gray-50 rounded-lg">
<div class="flex items-center gap-4">
<div class="text-sm text-gray-700">
Showing {{ (pagination.current_page - 1) * pagination.per_page + 1 }} to {{ min(pagination.current_page * pagination.per_page, pagination.total_items) }} of {{ pagination.total_items }} TV shows
</div>
<div class="flex items-center gap-2">
<label for="per_page_top" class="text-sm font-medium text-gray-700">Show:</label>
<select id="per_page_top" class="text-sm border border-gray-300 rounded px-2 py-1 focus:ring-blue-500 focus:border-blue-500">
<option value="12" {{ pagination.per_page == 12 ? 'selected' : '' }}>12</option>
<option value="24" {{ pagination.per_page == 24 ? 'selected' : '' }}>24</option>
<option value="48" {{ pagination.per_page == 48 ? 'selected' : '' }}>48</option>
<option value="100" {{ pagination.per_page == 100 ? 'selected' : '' }}>100</option>
</select>
<span class="text-muted small">per page</span>
<span class="text-sm text-gray-600">per page</span>
</div>
</div>
<div class="d-flex align-items-center gap-2">
<div class="flex items-center space-x-2">
<!-- Previous Button -->
{% if pagination.has_prev %}
<a href="?page={{ pagination.prev_page }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}&view={{ view_mode }}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for year in filters.years %}&years[]={{ year }}{% endfor %}"
class="btn btn-outline-secondary btn-sm">
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-700 transition-colors flex items-center">
<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 19l-7-7 7-7"/>
</svg>
Previous
</a>
{% else %}
<span class="px-3 py-2 text-sm font-medium text-gray-300 bg-gray-100 border border-gray-200 rounded-lg cursor-not-allowed flex items-center">
<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 19l-7-7 7-7"/>
</svg>
Previous
</span>
{% endif %}
<div class="btn-group" role="group">
{% for page_num in range(max(1, pagination.current_page - 2), min(pagination.total_pages, pagination.current_page + 2)) %}
<a href="?page={{ page_num }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}&view={{ view_mode }}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for year in filters.years %}&years[]={{ year }}{% endfor %}"
class="btn btn-sm {{ page_num == pagination.current_page ? 'btn-primary' : 'btn-outline-secondary' }}">
{{ page_num }}
</a>
{% endfor %}
</div>
<!-- Page Numbers -->
{% set start_page = max(1, pagination.current_page - 2) %}
{% set end_page = min(pagination.total_pages, pagination.current_page + 2) %}
{% if start_page > 1 %}
<a href="?page=1{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}&view={{ view_mode }}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for year in filters.years %}&years[]={{ year }}{% endfor %}"
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-700 transition-colors">1</a>
{% if start_page > 2 %}
<span class="px-2 py-2 text-sm font-medium text-gray-500">...</span>
{% endif %}
{% endif %}
{% for page_num in start_page..end_page %}
{% if page_num == pagination.current_page %}
<span class="px-3 py-2 text-sm font-medium text-white bg-blue-600 border border-blue-600 rounded-lg">{{ page_num }}</span>
{% else %}
<a href="?page={{ page_num }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}&view={{ view_mode }}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for year in filters.years %}&years[]={{ year }}{% endfor %}"
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-700 transition-colors">{{ page_num }}</a>
{% endif %}
{% endfor %}
{% if end_page < pagination.total_pages %}
{% if end_page < pagination.total_pages - 1 %}
<span class="px-2 py-2 text-sm font-medium text-gray-500">...</span>
{% endif %}
<a href="?page={{ pagination.total_pages }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}&view={{ view_mode }}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for year in filters.years %}&years[]={{ year }}{% endfor %}"
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-700 transition-colors">{{ pagination.total_pages }}</a>
{% endif %}
<!-- Next Button -->
{% if pagination.has_next %}
<a href="?page={{ pagination.next_page }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}&view={{ view_mode }}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for year in filters.years %}&years[]={{ year }}{% endfor %}"
class="btn btn-outline-secondary btn-sm">
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-700 transition-colors flex items-center">
Next
<svg class="w-4 h-4 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</a>
{% else %}
<span class="px-3 py-2 text-sm font-medium text-gray-300 bg-gray-100 border border-gray-200 rounded-lg cursor-not-allowed flex items-center">
Next
<svg class="w-4 h-4 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</span>
{% endif %}
</div>
</div>
@@ -408,6 +506,13 @@ document.getElementById('per_page')?.addEventListener('change', function() {
url.searchParams.set('page', '1'); // Reset to first page
window.location = url.toString();
});
document.getElementById('per_page_top')?.addEventListener('change', function() {
const url = new URL(window.location);
url.searchParams.set('per_page', this.value);
url.searchParams.set('page', '1'); // Reset to first page
window.location = url.toString();
});
</script>
{% endblock %}

View File

@@ -1,66 +1,37 @@
{% extends "layouts/app.twig" %}
{% block content %}
<div class="container-fluid px-4 py-3">
<!-- Hero Section with Backdrop -->
<div class="relative">
{% if tvshow.backdrop_url %}
<div class="h-96 md:h-[500px] relative overflow-hidden">
<img src="/images/{{ tvshow.backdrop_url }}" alt="{{ tvshow.title }} backdrop" class="w-full h-full object-cover">
<div class="absolute inset-0 bg-gradient-to-t from-black via-black/50 to-transparent"></div>
<div class="absolute inset-0 bg-gradient-to-r from-black/80 via-black/40 to-transparent"></div>
<!-- Back button -->
<div class="mb-4">
<a href="{{ path_for('tvshows.index') }}" class="btn btn-link text-decoration-none p-0">
<svg class="me-2" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<div class="absolute top-4 left-4 z-10">
<a href="{{ path_for('tvshows.index') }}" class="inline-flex items-center text-white hover:text-gray-300 transition-colors bg-black/20 backdrop-blur-sm rounded-full px-4 py-2">
<svg class="mr-2" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
</svg>
Back to TV Shows
</a>
</div>
<div class="card shadow-sm">
<div class="row g-0">
<!-- TV Show poster -->
<div class="col-md-4">
<div class="card-body p-4">
<div class="ratio ratio-2x3">
{% if tvshow.poster_url %}
<img src="/images/{{ tvshow.poster_url }}" alt="{{ tvshow.title }}" class="img-fluid rounded">
{% else %}
<div class="w-100 h-100 d-flex align-items-center justify-content-center bg-light rounded">
<svg class="text-muted" width="96" height="96" 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>
<!-- Action buttons -->
<div class="mt-4 d-grid gap-2">
<button class="btn btn-primary">
<svg class="me-2" width="16" height="16" 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>
Mark as Watched
</button>
<button class="btn btn-outline-danger">
<svg class="me-2" width="16" height="16" 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>
Add to Favorites
</button>
</div>
</div>
</div>
<!-- TV Show details -->
<div class="col-md-8">
<div class="card-body p-4">
<div class="mb-4">
<h1 class="display-4 fw-bold text-dark mb-2">{{ tvshow.title }}</h1>
<!-- Hero Content -->
<div class="absolute bottom-0 left-0 right-0 p-6 md:p-8">
<div class="max-w-4xl">
<h1 class="text-4xl md:text-6xl font-bold text-white mb-2 drop-shadow-lg">{{ tvshow.title }}</h1>
{% if tvshow.tagline %}
<p class="lead text-muted mb-3">{{ tvshow.tagline }}</p>
<p class="text-xl md:text-2xl text-gray-200 mb-4 drop-shadow-md">{{ tvshow.tagline }}</p>
{% endif %}
<!-- TV Show metadata -->
<div class="d-flex flex-wrap gap-3 small text-muted mb-3">
<!-- Quick Info -->
<div class="flex flex-wrap gap-4 text-sm text-white mb-4">
{% if tvshow.first_air_date %}
<span class="d-flex align-items-center">
<svg class="me-1" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<span class="flex items-center bg-black/30 backdrop-blur-sm rounded-full px-3 py-1">
<svg class="mr-2" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
{{ tvshow.first_air_date|date('Y') }}
@@ -68,8 +39,8 @@
{% endif %}
{% if tvshow.last_air_date %}
<span class="d-flex align-items-center">
<svg class="me-1" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<span class="flex items-center bg-black/30 backdrop-blur-sm rounded-full px-3 py-1">
<svg class="mr-2" width="16" height="16" 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>
Ended {{ tvshow.last_air_date|date('Y') }}
@@ -77,8 +48,8 @@
{% endif %}
{% if tvshow.vote_average %}
<span class="d-flex align-items-center">
<svg class="me-1" width="16" height="16" fill="currentColor" viewBox="0 0 20 20">
<span class="flex items-center bg-black/30 backdrop-blur-sm rounded-full px-3 py-1">
<svg class="mr-2" width="16" height="16" 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>
{{ tvshow.vote_average }}/10
@@ -86,16 +57,16 @@
{% endif %}
{% if tvshow.number_of_seasons and tvshow.number_of_episodes %}
<span class="d-flex align-items-center">
<svg class="me-1" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<span class="flex items-center bg-black/30 backdrop-blur-sm rounded-full px-3 py-1">
<svg class="mr-2" width="16" height="16" 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>
{{ tvshow.number_of_seasons }} season{{ tvshow.number_of_seasons > 1 ? 's' : '' }}, {{ tvshow.number_of_episodes }} episode{{ tvshow.number_of_episodes > 1 ? 's' : '' }}
</span>
{% endif %}
<span class="d-flex align-items-center">
<svg class="me-1" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<span class="flex items-center bg-black/30 backdrop-blur-sm rounded-full px-3 py-1">
<svg class="mr-2" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/>
</svg>
{{ tvshow.source_name }}
@@ -103,17 +74,17 @@
</div>
<!-- Status badges -->
<div class="d-flex flex-wrap gap-2">
<div class="flex flex-wrap gap-2 mb-4">
{% if tvshow.status == 'Ended' %}
<span class="badge bg-danger d-flex align-items-center">
<svg class="me-1" width="12" height="12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-red-500 text-white">
<svg class="mr-2" width="14" height="14" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"/>
</svg>
Ended
</span>
{% elseif tvshow.status == 'Returning Series' %}
<span class="badge bg-success d-flex align-items-center">
<svg class="me-1" width="12" height="12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-500 text-white">
<svg class="mr-2" width="14" height="14" 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>
Ongoing
@@ -121,170 +92,287 @@
{% endif %}
{% if tvshow.is_favorite %}
<span class="badge bg-warning text-dark d-flex align-items-center">
<svg class="me-1" width="12" height="12" fill="currentColor" viewBox="0 0 20 20">
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-red-500 text-white">
<svg class="mr-2" width="14" height="14" fill="currentColor" viewBox="0 0 20 20">
<path d="M3.172 5.172a4 4 0 015.656 0L10 6.343l1.172-1.171a4 4 0 115.656 5.656L10 17.657l-6.828-6.829a4 4 0 010-5.656z"/>
</svg>
Favorite
</span>
{% endif %}
</div>
</div>
<!-- Overview -->
{% if tvshow.overview %}
<div class="mb-4">
<h2 class="h5 fw-semibold text-dark mb-2">Overview</h2>
<p class="text-muted">{{ tvshow.overview }}</p>
</div>
{% endif %}
<!-- Cast & Crew Section -->
<div class="mb-4">
<h2 class="h5 fw-semibold text-dark mb-3">Cast & Crew</h2>
<div class="row g-3">
{% if tvshow.created_by %}
<div class="col-md-6">
<div class="d-flex align-items-center">
<svg class="me-2 text-muted" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
<!-- Action buttons -->
<div class="flex flex-wrap gap-3">
<button class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg font-medium transition-colors flex items-center">
<svg class="mr-2" width="18" height="18" 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>
<div>
<small class="text-muted d-block">Created by</small>
<span class="fw-medium">{{ tvshow.created_by }}</span>
</div>
</div>
</div>
{% endif %}
</div>
{% if actors %}
<div class="mt-3">
<small class="text-muted d-block mb-2">Main Cast</small>
<div class="d-flex flex-wrap gap-2">
{% for actor in actors %}
<span class="badge bg-light text-dark">{{ actor.name }}</span>
{% endfor %}
</div>
</div>
{% endif %}
</div>
<!-- TV Show Details Grid -->
<div class="row g-4 mb-4">
<!-- Genres -->
{% if tvshow.genre %}
<div class="col-md-4">
<h3 class="h6 fw-semibold text-dark mb-3">Genres</h3>
<div class="d-flex flex-wrap gap-2">
{% for genre in tvshow.genre|split(',') %}
<span class="badge bg-primary">{{ genre|trim }}</span>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Networks & Production -->
{% if tvshow.networks or tvshow.production_companies %}
<div class="col-md-4">
<h3 class="h6 fw-semibold text-dark mb-3">Production</h3>
<div class="mb-2">
{% if tvshow.networks %}
<small class="text-muted d-block">Networks</small>
<span class="fw-medium">{{ tvshow.networks }}</span>
{% endif %}
</div>
{% if tvshow.production_companies %}
<div class="mb-2">
<small class="text-muted d-block">Companies</small>
<span class="fw-medium">{{ tvshow.production_companies }}</span>
</div>
{% endif %}
</div>
{% endif %}
<!-- Episode Details -->
{% if tvshow.episode_run_time or tvshow.origin_country %}
<div class="col-md-4">
<h3 class="h6 fw-semibold text-dark mb-3">Details</h3>
<div class="mb-2">
{% if tvshow.episode_run_time %}
<small class="text-muted d-block">Episode Runtime</small>
<span class="fw-medium">{{ tvshow.episode_run_time }} minutes</span>
{% endif %}
</div>
{% if tvshow.origin_country %}
<div class="mb-2">
<small class="text-muted d-block">Origin Country</small>
<span class="fw-medium">{{ tvshow.origin_country }}</span>
</div>
{% endif %}
{% if tvshow.original_language %}
<div class="mb-2">
<small class="text-muted d-block">Language</small>
<span class="fw-medium">{{ tvshow.original_language|upper }}</span>
</div>
{% endif %}
</div>
{% endif %}
</div>
<!-- Seasons Section -->
{% if seasons %}
<div class="mb-4">
<h3 class="h5 fw-semibold text-dark mb-3">Seasons</h3>
<div class="accordion" id="seasonsAccordion">
{% for season in seasons %}
<div class="accordion-item">
<h2 class="accordion-header" id="heading{{ season.season_number }}">
<button class="accordion-button {% if loop.first %}collapsed{% else %}collapsed{% endif %}" type="button" data-bs-toggle="collapse" data-bs-target="#collapse{{ season.season_number }}" aria-expanded="{% if loop.first %}true{% else %}false{% endif %}" aria-controls="collapse{{ season.season_number }}">
<div class="d-flex justify-content-between w-100">
<span>Season {{ season.season_number }}</span>
<span class="text-muted">{{ season.episode_count }} episodes</span>
</div>
Mark as Watched
</button>
</h2>
<div id="collapse{{ season.season_number }}" class="accordion-collapse collapse {% if loop.first %}show{% endif %}" aria-labelledby="heading{{ season.season_number }}" data-bs-parent="#seasonsAccordion">
<div class="accordion-body">
{% if season.episodes %}
<div class="list-group list-group-flush">
{% for episode in season.episodes %}
<div class="list-group-item d-flex align-items-center py-3">
<div class="flex-shrink-0 me-3">
{% if episode.poster_url %}
<img src="/images/{{ episode.poster_url }}" alt="{{ episode.title }}" class="rounded" style="width: 60px; height: 34px; object-fit: cover;">
<button class="bg-white/10 hover:bg-white/20 backdrop-blur-sm text-white border border-white/30 px-6 py-3 rounded-lg font-medium transition-colors flex items-center">
<svg class="mr-2" width="18" height="18" 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>
Add to Favorites
</button>
</div>
</div>
</div>
</div>
{% else %}
<div class="rounded d-flex align-items-center justify-content-center bg-light" style="width: 60px; height: 34px;">
<svg class="text-muted" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<!-- Fallback hero without backdrop -->
<div class="bg-gradient-to-r from-blue-900 to-purple-900 h-64 relative">
<div class="absolute top-4 left-4 z-10">
<a href="{{ path_for('tvshows.index') }}" class="inline-flex items-center text-white hover:text-gray-300 transition-colors">
<svg class="mr-2" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
</svg>
Back to TV Shows
</a>
</div>
<div class="absolute bottom-0 left-0 right-0 p-6">
<h1 class="text-4xl font-bold text-white mb-2">{{ tvshow.title }}</h1>
{% if tvshow.tagline %}
<p class="text-xl text-gray-200">{{ tvshow.tagline }}</p>
{% endif %}
</div>
</div>
{% endif %}
</div>
<!-- Main Content -->
<div class="max-w-7xl mx-auto px-4 py-8">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Poster and Quick Actions -->
<div class="lg:col-span-1">
<div class="sticky top-4">
<!-- Poster -->
<div class="bg-white rounded-xl shadow-lg overflow-hidden mb-6">
<div class="aspect-[2/3]">
{% if tvshow.poster_url %}
<img src="/images/{{ tvshow.poster_url }}" alt="{{ tvshow.title }}" class="w-full h-full object-cover">
{% else %}
<div class="w-full h-full flex items-center justify-center bg-gradient-to-br from-gray-100 to-gray-200">
<svg class="text-gray-400 w-16 h-16" 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="flex-grow-1">
<div class="d-flex align-items-center gap-2 mb-1">
<span class="badge bg-info">E{{ episode.episode_number }}</span>
{% if episode.air_date %}
<span class="small text-muted">{{ episode.air_date|date('M j, Y') }}</span>
</div>
<!-- Quick Stats -->
<div class="bg-white rounded-xl shadow-lg p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Quick Stats</h3>
<div class="space-y-3">
{% if tvshow.vote_count %}
<div class="flex justify-between items-center">
<span class="text-gray-600">Votes</span>
<span class="font-medium">{{ tvshow.vote_count|number_format }}</span>
</div>
{% endif %}
{% if episode.is_watched %}
<span class="badge bg-success">Watched</span>
{% if tvshow.number_of_seasons %}
<div class="flex justify-between items-center">
<span class="text-gray-600">Seasons</span>
<span class="font-medium">{{ tvshow.number_of_seasons }}</span>
</div>
{% endif %}
{% if tvshow.number_of_episodes %}
<div class="flex justify-between items-center">
<span class="text-gray-600">Episodes</span>
<span class="font-medium">{{ tvshow.number_of_episodes }}</span>
</div>
{% endif %}
<div class="flex justify-between items-center">
<span class="text-gray-600">Source</span>
<span class="font-medium">{{ tvshow.source_name }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- Main Details -->
<div class="lg:col-span-2 space-y-8">
<!-- Overview -->
{% if tvshow.overview %}
<div class="bg-white rounded-xl shadow-lg p-6">
<h2 class="text-2xl font-bold text-gray-900 mb-4">Overview</h2>
<p class="text-gray-700 leading-relaxed">{{ tvshow.overview }}</p>
</div>
{% endif %}
<!-- Cast & Crew -->
<div class="bg-white rounded-xl shadow-lg p-6">
<h2 class="text-2xl font-bold text-gray-900 mb-6">Cast & Crew</h2>
<!-- Creator -->
{% if tvshow.created_by %}
<div class="flex items-start mb-6">
<div class="bg-blue-100 rounded-full p-3 mr-4">
<svg class="text-blue-600" width="24" height="24" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
</div>
<div>
<h3 class="font-semibold text-gray-900">Created by</h3>
<p class="text-gray-600">{{ tvshow.created_by }}</p>
</div>
</div>
{% endif %}
<!-- Cast -->
{% if actors %}
<div>
<h3 class="text-lg font-semibold text-gray-900 mb-4">Cast</h3>
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
{% for actor in actors %}
<a href="{{ path_for('actors.show', {'id': actor.id}) }}" class="flex flex-col items-center text-center group hover:scale-105 transition-transform">
<div class="w-16 h-16 md:w-20 md:h-20 bg-gray-200 rounded-full overflow-hidden mb-2 group-hover:ring-2 group-hover:ring-blue-300 transition-all">
{% if actor.thumbnail_path %}
<img src="/images/{{ actor.thumbnail_path }}" alt="{{ actor.name }}" class="w-full h-full object-cover">
{% else %}
<div class="w-full h-full bg-gradient-to-br from-blue-400 to-purple-500 flex items-center justify-center">
<span class="text-white font-bold text-lg">{{ actor.name|first|upper }}</span>
</div>
{% endif %}
</div>
<h6 class="mb-1">{{ episode.title }}</h6>
<p class="font-medium text-gray-900 text-sm leading-tight group-hover:text-blue-600 transition-colors">{{ actor.name }}</p>
</a>
{% endfor %}
</div>
</div>
{% endif %}
</div>
<!-- Details Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Genres -->
{% if tvshow.genre %}
<div class="bg-white rounded-xl shadow-lg p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Genres</h3>
<div class="flex flex-wrap gap-2">
{% for genre in tvshow.genre|split(',') %}
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800">{{ genre|trim }}</span>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Production -->
{% if tvshow.networks or tvshow.production_companies %}
<div class="bg-white rounded-xl shadow-lg p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Production</h3>
{% if tvshow.networks %}
<div class="mb-3">
<p class="text-sm text-gray-600 mb-1">Networks</p>
<p class="font-medium">{{ tvshow.networks }}</p>
</div>
{% endif %}
{% if tvshow.production_companies %}
<div>
<p class="text-sm text-gray-600 mb-1">Companies</p>
<p class="font-medium">{{ tvshow.production_companies }}</p>
</div>
{% endif %}
</div>
{% endif %}
<!-- Technical Details -->
{% if tvshow.episode_run_time or tvshow.origin_country or tvshow.original_language %}
<div class="bg-white rounded-xl shadow-lg p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Technical</h3>
{% if tvshow.episode_run_time %}
<div class="mb-3">
<p class="text-sm text-gray-600 mb-1">Episode Runtime</p>
<p class="font-medium">{{ tvshow.episode_run_time }} minutes</p>
</div>
{% endif %}
{% if tvshow.origin_country %}
<div class="mb-3">
<p class="text-sm text-gray-600 mb-1">Origin Country</p>
<p class="font-medium">{{ tvshow.origin_country }}</p>
</div>
{% endif %}
{% if tvshow.original_language %}
<div>
<p class="text-sm text-gray-600 mb-1">Language</p>
<p class="font-medium">{{ tvshow.original_language|upper }}</p>
</div>
{% endif %}
</div>
{% endif %}
</div>
<!-- Seasons & Episodes -->
{% if seasons %}
<div class="bg-white rounded-xl shadow-lg p-6">
<h2 class="text-2xl font-bold text-gray-900 mb-6">Seasons & Episodes</h2>
<div class="space-y-4">
{% for season in seasons %}
<div class="border border-gray-200 rounded-lg overflow-hidden">
<button class="w-full px-6 py-4 bg-gray-50 hover:bg-gray-100 transition-colors text-left flex items-center justify-between focus:outline-none focus:ring-2 focus:ring-blue-500">
<div class="flex items-center">
<svg class="w-5 h-5 text-gray-600 mr-3" 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>
<div>
<h3 class="text-lg font-semibold text-gray-900">Season {{ season.season_number }}</h3>
<p class="text-sm text-gray-600">{{ season.episode_count }} episodes</p>
</div>
</div>
<svg class="w-5 h-5 text-gray-400 transform transition-transform" 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>
</button>
<div class="hidden px-6 py-4 bg-white border-t border-gray-100">
{% if season.episodes %}
<div class="space-y-3">
{% for episode in season.episodes %}
<div class="flex items-start p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
<div class="flex-shrink-0 mr-4">
{% if episode.still_url %}
<img src="/images/{{ episode.still_url }}" alt="{{ episode.title }}" class="w-20 h-12 object-cover rounded">
{% else %}
<div class="w-20 h-12 bg-gray-200 rounded flex items-center justify-center">
<svg class="w-6 h-6 text-gray-400" 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="flex-grow">
<div class="flex items-center gap-2 mb-1">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">E{{ episode.episode_number }}</span>
{% if episode.air_date %}
<span class="text-sm text-gray-600">{{ episode.air_date|date('M j, Y') }}</span>
{% endif %}
{% if episode.is_watched %}
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">Watched</span>
{% endif %}
</div>
<h4 class="text-sm font-semibold text-gray-900 mb-1">{{ episode.title }}</h4>
{% if episode.overview %}
<p class="small text-muted mb-0">{{ episode.overview|slice(0, 100) }}{% if episode.overview|length > 100 %}...{% endif %}</p>
<p class="text-sm text-gray-700 mb-2">{{ episode.overview|slice(0, 120) }}{% if episode.overview|length > 120 %}...{% endif %}</p>
{% endif %}
{% if episode.actors %}
<div class="flex flex-wrap gap-1">
{% for actor in episode.actors %}
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
{{ actor.name }}
</span>
{% endfor %}
</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% else %}
<p class="text-muted">No episodes available.</p>
<p class="text-gray-600">No episodes available.</p>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
@@ -292,33 +380,40 @@
<!-- Recent Episodes -->
{% if recent_episodes %}
<div class="mb-4">
<h3 class="h5 fw-semibold text-dark mb-3">Recent Episodes</h3>
<div class="vstack gap-3">
<div class="bg-white rounded-xl shadow-lg p-6">
<h2 class="text-2xl font-bold text-gray-900 mb-6">Recent Episodes</h2>
<div class="grid grid-cols-1 gap-4">
{% for episode in recent_episodes[:5] %}
<div class="card border-0 bg-light">
<div class="card-body p-3">
<div class="d-flex">
<div class="flex-shrink-0 me-3">
<div class="flex items-start p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
<div class="flex-shrink-0 mr-4">
{% if episode.still_url %}
<img src="/images/{{ episode.still_url }}" alt="{{ episode.title }}" class="rounded" style="width: 120px; height: 68px; object-fit: cover;">
<img src="/images/{{ episode.still_url }}" alt="{{ episode.title }}" class="w-24 h-14 object-cover rounded">
{% else %}
<div class="rounded d-flex align-items-center justify-content-center bg-secondary" style="width: 120px; height: 68px;">
<svg class="text-white" width="32" height="32" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<div class="w-24 h-14 bg-gray-200 rounded flex items-center justify-center">
<svg class="w-8 h-8 text-gray-400" 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="flex-grow-1">
<div class="d-flex align-items-center gap-2 mb-1">
<span class="badge bg-info">S{{ episode.season_number }}E{{ episode.episode_number }}</span>
<span class="small text-muted">{{ episode.air_date|date('M j, Y') }}</span>
<div class="flex-grow">
<div class="flex items-center gap-2 mb-1">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">S{{ episode.season_number }}E{{ episode.episode_number }}</span>
<span class="text-sm text-gray-600">{{ episode.air_date|date('M j, Y') }}</span>
</div>
<h6 class="fw-medium mb-1">{{ episode.title }}</h6>
<h4 class="text-sm font-semibold text-gray-900 mb-1">{{ episode.title }}</h4>
{% if episode.actors %}
<div class="flex flex-wrap gap-1 mb-2">
{% for actor in episode.actors %}
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
{{ actor.name }}
</span>
{% endfor %}
</div>
{% endif %}
{% if episode.vote_average %}
<div class="d-flex align-items-center small text-muted">
<svg class="me-1 text-warning" width="14" height="14" fill="currentColor" viewBox="0 0 20 20">
<div class="flex items-center text-sm text-gray-600">
<svg class="w-4 h-4 text-yellow-500 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>
{{ episode.vote_average }}/10
@@ -326,58 +421,113 @@
{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Streaming & Availability -->
<!-- Where to Watch -->
{% if tvshow.streaming_providers or tvshow.availability %}
<div class="mb-4">
<h3 class="h5 fw-semibold text-dark mb-3">Where to Watch</h3>
<div class="row g-3">
<div class="bg-white rounded-xl shadow-lg p-6">
<h2 class="text-2xl font-bold text-gray-900 mb-6">Where to Watch</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
{% if tvshow.streaming_providers %}
<div class="col-md-6">
<small class="text-muted d-block mb-2">Streaming</small>
<div class="d-flex flex-wrap gap-2">
<div>
<h3 class="text-lg font-semibold text-gray-900 mb-4">Streaming</h3>
<div class="flex flex-wrap gap-2">
{% for provider in tvshow.streaming_providers %}
<span class="badge bg-success">{{ provider }}</span>
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-100 text-green-800">{{ provider }}</span>
{% endfor %}
</div>
</div>
{% endif %}
{% if tvshow.availability %}
<div class="col-md-6">
<small class="text-muted d-block mb-2">Availability</small>
<span class="fw-medium">{{ tvshow.availability }}</span>
<div>
<h3 class="text-lg font-semibold text-gray-900 mb-4">Availability</h3>
<p class="text-gray-700">{{ tvshow.availability }}</p>
</div>
{% endif %}
</div>
</div>
{% endif %}
<!-- Metadata (for debugging/advanced users) -->
<!-- Technical Details & Metadata -->
{% if metadata %}
<div class="mt-4 pt-4 border-top">
<div class="space-y-6">
<h2 class="text-2xl font-bold text-gray-900">Technical Details & Metadata</h2>
<!-- Jellyfin ID -->
{% if metadata.jellyfin_id %}
<div class="bg-white rounded-xl shadow-lg p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-3 flex items-center">
<svg class="mr-2 text-blue-600" width="20" height="20" 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>
Jellyfin ID
</h3>
<div class="bg-gray-50 rounded-lg p-3">
<code class="text-sm text-gray-800 font-mono">{{ metadata.jellyfin_id }}</code>
</div>
</div>
{% endif %}
<!-- Genres -->
{% if metadata.genres %}
<div class="bg-white rounded-xl shadow-lg p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-3 flex items-center">
<svg class="mr-2 text-green-600" width="20" height="20" 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>
Genres
</h3>
<div class="flex flex-wrap gap-2">
{% for genre in metadata.genres %}
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-100 text-green-800">{{ genre }}</span>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Raw Metadata (Collapsible) -->
<div class="bg-white rounded-xl shadow-lg p-6">
<details class="group">
<summary class="cursor-pointer small fw-medium text-muted hover:text-dark d-flex align-items-center">
<svg class="me-2 group-open:rotate-90 transition-transform" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<summary class="cursor-pointer flex items-center text-gray-700 hover:text-gray-900 transition-colors">
<svg class="mr-2 group-open:rotate-90 transition-transform" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
Technical Details & Metadata
<span class="font-medium">Full Raw Metadata</span>
</summary>
<div class="mt-3 small">
<pre class="bg-light p-3 rounded"><code>{{ metadata|json_encode(constant('JSON_PRETTY_PRINT')) }}</code></pre>
<div class="mt-4 bg-gray-50 rounded-lg p-4 overflow-x-auto">
<pre class="text-xs text-gray-800 whitespace-pre-wrap"><code>{{ metadata|json_encode(constant('JSON_PRETTY_PRINT')) }}</code></pre>
</div>
</details>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Season accordion functionality
const seasonButtons = document.querySelectorAll('.border-gray-200.rounded-lg.overflow-hidden button');
seasonButtons.forEach(button => {
button.addEventListener('click', function() {
const content = this.nextElementSibling;
const icon = this.querySelector('svg:last-child');
if (content.classList.contains('hidden')) {
content.classList.remove('hidden');
icon.style.transform = 'rotate(180deg)';
} else {
content.classList.add('hidden');
icon.style.transform = 'rotate(0deg)';
}
});
});
});
</script>
{% endblock %}

View File

@@ -57,6 +57,7 @@ $app->group('', function (RouteCollectorProxy $group) {
// Adult Performers (Actors)
$mediaGroup->get('/actors', 'App\Controllers\ActorController:index')->setName('actors.index');
$mediaGroup->get('/actors/{id:\d+}', 'App\Controllers\ActorController:show')->setName('actors.show');
$mediaGroup->map(['GET', 'POST'], '/actors/{id:\d+}/edit', 'App\Controllers\ActorController:edit')->setName('actors.edit');
});

13
tailwind.config.js Normal file
View File

@@ -0,0 +1,13 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./resources/views/**/*.twig",
"./public/**/*.js",
"./resources/**/*.js",
"./resources/**/*.css",
],
theme: {
extend: {},
},
plugins: [],
}