mirror of
https://github.com/ceratic/MediaCollectorLibary.git
synced 2026-05-13 23:56:46 +02:00
dont know ?
This commit is contained in:
@@ -21,16 +21,22 @@ class ActorController extends Controller
|
|||||||
{
|
{
|
||||||
$actorId = $args['id'];
|
$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("
|
$stmt = $this->pdo->prepare("
|
||||||
SELECT a.*,
|
SELECT a.*,
|
||||||
COUNT(DISTINCT am.movie_id) as movie_count,
|
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 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
|
FROM actors a
|
||||||
LEFT JOIN actor_movie am ON a.id = am.actor_id
|
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_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
|
LEFT JOIN actor_adult_video aav ON a.id = aav.actor_id
|
||||||
WHERE a.id = :actor_id
|
WHERE a.id = :actor_id
|
||||||
GROUP BY a.id
|
GROUP BY a.id
|
||||||
@@ -66,16 +72,18 @@ class ActorController extends Controller
|
|||||||
$stmt->execute(['actor_id' => $actorId]);
|
$stmt->execute(['actor_id' => $actorId]);
|
||||||
$movies = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$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("
|
$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
|
FROM tv_shows ts
|
||||||
JOIN sources s ON ts.source_id = s.id
|
JOIN sources s ON ts.source_id = s.id
|
||||||
JOIN actor_tv_show ats ON ts.id = ats.tv_show_id
|
LEFT JOIN actor_tv_show ats ON ts.id = ats.tv_show_id AND ats.actor_id = :actor_id
|
||||||
WHERE 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
|
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);
|
$tvShows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
|
||||||
@@ -108,33 +116,314 @@ class ActorController extends Controller
|
|||||||
'tv_shows' => $tvShows
|
'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)
|
public function index(Request $request, Response $response, $args)
|
||||||
{
|
{
|
||||||
// Get all actors with their media counts from all types
|
$queryParams = $request->getQueryParams();
|
||||||
$stmt = $this->pdo->prepare("
|
|
||||||
SELECT a.*,
|
// Get pagination parameters
|
||||||
COUNT(DISTINCT aav.adult_video_id) as adult_video_count,
|
$page = max(1, (int)($queryParams['page'] ?? 1));
|
||||||
COUNT(DISTINCT am.movie_id) as movie_count,
|
$perPage = max(12, min(100, (int)($queryParams['per_page'] ?? 24)));
|
||||||
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,
|
// Get search parameters
|
||||||
MAX(COALESCE(av.release_date, m.release_date, ts.first_air_date)) as latest_media_date
|
$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
|
FROM actors a
|
||||||
LEFT JOIN actor_adult_video aav ON a.id = aav.actor_id
|
LEFT JOIN (
|
||||||
LEFT JOIN adult_videos av ON aav.adult_video_id = av.id
|
SELECT actor_id, COUNT(DISTINCT adult_video_id) as adult_video_count
|
||||||
LEFT JOIN actor_movie am ON a.id = am.actor_id
|
FROM actor_adult_video
|
||||||
LEFT JOIN movies m ON am.movie_id = m.id
|
GROUP BY actor_id
|
||||||
LEFT JOIN actor_tv_show ats ON a.id = ats.actor_id
|
) adult_counts ON a.id = adult_counts.actor_id
|
||||||
LEFT JOIN tv_shows ts ON ats.tv_show_id = ts.id
|
LEFT JOIN (
|
||||||
GROUP BY a.id
|
SELECT actor_id, COUNT(DISTINCT movie_id) as movie_count
|
||||||
ORDER BY total_media_count DESC, a.name ASC
|
FROM actor_movie
|
||||||
LIMIT 50
|
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();
|
$stmt->execute();
|
||||||
$actors = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$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', [
|
return $this->view->render($response, 'actor/index.twig', [
|
||||||
'title' => 'Actors & Performers',
|
'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
|
||||||
|
]
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,12 +41,18 @@ class AdultController extends Controller
|
|||||||
}
|
}
|
||||||
$directors = array_filter($directors);
|
$directors = array_filter($directors);
|
||||||
|
|
||||||
|
$sources = $queryParams['sources'] ?? [];
|
||||||
|
if (!is_array($sources)) {
|
||||||
|
$sources = [$sources];
|
||||||
|
}
|
||||||
|
$sources = array_filter($sources);
|
||||||
|
|
||||||
// Get view mode and sort
|
// Get view mode and sort
|
||||||
$viewMode = $queryParams['view'] ?? 'grid'; // grid, list, covers
|
$viewMode = $queryParams['view'] ?? 'grid'; // grid, list, covers
|
||||||
$sort = $queryParams['sort'] ?? 'recent';
|
$sort = $queryParams['sort'] ?? 'recent';
|
||||||
|
|
||||||
// Get adult videos with pagination, filters, and sorting
|
// 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
|
// Process metadata to extract local image paths for template compatibility
|
||||||
foreach ($adultVideos as &$video) {
|
foreach ($adultVideos as &$video) {
|
||||||
@@ -68,11 +74,12 @@ class AdultController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get total count for pagination
|
// 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
|
// Get available filter options
|
||||||
$availableGenres = AdultVideo::getAvailableGenres($this->pdo);
|
$availableGenres = AdultVideo::getAvailableGenres($this->pdo);
|
||||||
$availableDirectors = AdultVideo::getAvailableDirectors($this->pdo);
|
$availableDirectors = AdultVideo::getAvailableDirectors($this->pdo);
|
||||||
|
$availableSources = AdultVideo::getAvailableSources($this->pdo);
|
||||||
|
|
||||||
// Calculate pagination info
|
// Calculate pagination info
|
||||||
$totalPages = ceil($totalCount / $perPage);
|
$totalPages = ceil($totalCount / $perPage);
|
||||||
@@ -112,11 +119,13 @@ class AdultController extends Controller
|
|||||||
],
|
],
|
||||||
'filters' => [
|
'filters' => [
|
||||||
'genres' => $genres,
|
'genres' => $genres,
|
||||||
'directors' => $directors
|
'directors' => $directors,
|
||||||
|
'sources' => $sources
|
||||||
],
|
],
|
||||||
'available_filters' => [
|
'available_filters' => [
|
||||||
'genres' => $availableGenres,
|
'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
|
// Add local image paths and other metadata to the video data for template compatibility
|
||||||
if (!empty($metadata['local_cover_path'])) {
|
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'])) {
|
} elseif (!empty($metadata['cover_url'])) {
|
||||||
$adultVideo['poster_url'] = $metadata['cover_url'];
|
$adultVideo['poster_url'] = $metadata['cover_url'];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($metadata['local_screenshot_path'])) {
|
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
|
// Add actors data if available
|
||||||
|
|||||||
@@ -122,6 +122,34 @@ class MovieController extends Controller
|
|||||||
// Decode metadata for display
|
// Decode metadata for display
|
||||||
$metadata = json_decode($movie['metadata'], true);
|
$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
|
// Get actors for this movie
|
||||||
$stmt = $this->pdo->prepare("
|
$stmt = $this->pdo->prepare("
|
||||||
SELECT a.*
|
SELECT a.*
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ class TvShowController extends Controller
|
|||||||
$cast = json_decode($tvShow['cast'] ?? '[]', true);
|
$cast = json_decode($tvShow['cast'] ?? '[]', true);
|
||||||
$genre = json_decode($tvShow['genre'] ?? '[]', 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("
|
$stmt = $this->pdo->prepare("
|
||||||
SELECT a.*
|
SELECT a.*
|
||||||
FROM actors a
|
FROM actors a
|
||||||
@@ -118,10 +118,59 @@ class TvShowController extends Controller
|
|||||||
ORDER BY a.name ASC
|
ORDER BY a.name ASC
|
||||||
");
|
");
|
||||||
$stmt->execute(['tv_show_id' => $tvShowId]);
|
$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
|
// Get seasons and episodes for this TV show
|
||||||
$tvShowModel = new TvShow($this->pdo, $tvShow);
|
$tvShowModel = new TvShow($this->pdo, $tvShow);
|
||||||
$seasons = $tvShowModel->getSeasonsWithEpisodes();
|
$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', [
|
return $this->view->render($response, 'tvshows/show.twig', [
|
||||||
'title' => $tvShow['title'],
|
'title' => $tvShow['title'],
|
||||||
'tvshow' => $tvShow,
|
'tvshow' => $tvShow,
|
||||||
@@ -129,7 +178,8 @@ class TvShowController extends Controller
|
|||||||
'cast' => $cast,
|
'cast' => $cast,
|
||||||
'genre' => $genre,
|
'genre' => $genre,
|
||||||
'actors' => $actors,
|
'actors' => $actors,
|
||||||
'seasons' => $seasons
|
'seasons' => $seasons,
|
||||||
|
'recent_episodes' => $recent_episodes
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class AdultVideo extends Model
|
|||||||
'external_id'
|
'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;
|
$offset = ($page - 1) * $perPage;
|
||||||
|
|
||||||
@@ -61,6 +61,16 @@ class AdultVideo extends Model
|
|||||||
$sql .= $whereClause . " av.director IN (" . implode(',', $placeholders) . ")";
|
$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
|
// Add sorting
|
||||||
$sortOptions = [
|
$sortOptions = [
|
||||||
'recent' => 'av.created_at DESC',
|
'recent' => 'av.created_at DESC',
|
||||||
@@ -92,7 +102,7 @@ class AdultVideo extends Model
|
|||||||
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
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";
|
$sql = "SELECT COUNT(*) as count FROM adult_videos av JOIN sources s ON av.source_id = s.id";
|
||||||
$params = [];
|
$params = [];
|
||||||
@@ -122,6 +132,16 @@ class AdultVideo extends Model
|
|||||||
$sql .= $whereClause . " av.director IN (" . implode(',', $placeholders) . ")";
|
$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);
|
$stmt = $pdo->prepare($sql);
|
||||||
foreach ($params as $key => $value) {
|
foreach ($params as $key => $value) {
|
||||||
$stmt->bindValue(":{$key}", $value);
|
$stmt->bindValue(":{$key}", $value);
|
||||||
@@ -245,6 +265,21 @@ class AdultVideo extends Model
|
|||||||
return $stmt->fetchAll(\PDO::FETCH_COLUMN);
|
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
|
* Get TV show statistics
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -375,6 +375,19 @@ class TvShow extends Model
|
|||||||
]);
|
]);
|
||||||
$episodes = $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
$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)
|
// Create a season object (simulating the old seasons table structure)
|
||||||
$seasons[] = [
|
$seasons[] = [
|
||||||
'id' => null, // No seasons table, so no ID
|
'id' => null, // No seasons table, so no ID
|
||||||
|
|||||||
@@ -465,15 +465,9 @@ class StashSyncService extends BaseSyncService
|
|||||||
$sceneData['local_cover_path'] = $existingMetadata['local_cover_path'] ?? null;
|
$sceneData['local_cover_path'] = $existingMetadata['local_cover_path'] ?? null;
|
||||||
$sceneData['local_screenshot_path'] = $existingMetadata['local_screenshot_path'] ?? null;
|
$sceneData['local_screenshot_path'] = $existingMetadata['local_screenshot_path'] ?? null;
|
||||||
}
|
}
|
||||||
// Handle performers/actors
|
// Handle performers/actors with full metadata
|
||||||
$performers = $sceneData['performers'] ?? [];
|
$performers = $sceneData['performers'] ?? [];
|
||||||
$actorNames = [];
|
$actors = $this->syncActors($performers);
|
||||||
$performerImages = [];
|
|
||||||
foreach ($performers as $performer) {
|
|
||||||
$actorNames[] = $performer['name'];
|
|
||||||
$performerImages[$performer['name']] = $performer['image_path'] ?? null;
|
|
||||||
}
|
|
||||||
$actors = $this->syncActors($actorNames, $performerImages);
|
|
||||||
|
|
||||||
$sceneData = [
|
$sceneData = [
|
||||||
'title' => $sceneData['title'] ?: 'Untitled Scene',
|
'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 = [];
|
$actors = [];
|
||||||
|
|
||||||
foreach ($actorNames as $actorName) {
|
foreach ($performers as $performer) {
|
||||||
if (empty($actorName)) continue;
|
if (empty($performer['name'])) continue;
|
||||||
|
|
||||||
$imagePath = $performerImages[$actorName] ?? null;
|
$actor = $this->getOrCreateActor($performer);
|
||||||
$actor = $this->getOrCreateActor($actorName, $imagePath);
|
|
||||||
if ($actor) {
|
if ($actor) {
|
||||||
$actors[] = $actor;
|
$actors[] = $actor;
|
||||||
}
|
}
|
||||||
@@ -635,25 +628,63 @@ class StashSyncService extends BaseSyncService
|
|||||||
return $actors;
|
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
|
// Check if actor already exists
|
||||||
$stmt = $this->pdo->prepare("
|
$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]);
|
$stmt->execute(['name' => $name]);
|
||||||
$existingActor = $stmt->fetch(PDO::FETCH_ASSOC);
|
$existingActor = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
if ($existingActor) {
|
// Prepare rich metadata from Stash performer data
|
||||||
return [
|
$actorMetadata = [
|
||||||
'id' => $existingActor['id'],
|
'stash_id' => $performer['id'] ?? null,
|
||||||
'name' => $existingActor['name'],
|
'stash_url' => $performer['url'] ?? null,
|
||||||
'thumbnail_path' => $existingActor['thumbnail_path']
|
'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
|
// Try to download performer image if available
|
||||||
$thumbnailPath = null;
|
$thumbnailPath = null;
|
||||||
|
$imagePath = $performer['image_path'] ?? null;
|
||||||
if ($imagePath) {
|
if ($imagePath) {
|
||||||
// Validate image path before constructing URL
|
// Validate image path before constructing URL
|
||||||
if (!empty(trim($imagePath))) {
|
if (!empty(trim($imagePath))) {
|
||||||
@@ -667,7 +698,7 @@ class StashSyncService extends BaseSyncService
|
|||||||
$imageUrl = "{$this->baseUrl}" . $imagePath;
|
$imageUrl = "{$this->baseUrl}" . $imagePath;
|
||||||
} else {
|
} else {
|
||||||
// Relative path - assume it's in performer images directory
|
// 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
|
// 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 {
|
try {
|
||||||
$stmt = $this->pdo->prepare("
|
$stmt = $this->pdo->prepare("
|
||||||
INSERT INTO actors (name, thumbnail_path, created_at, updated_at)
|
INSERT INTO actors (name, thumbnail_path, metadata, created_at, updated_at)
|
||||||
VALUES (:name, :thumbnail_path, NOW(), NOW())
|
VALUES (:name, :thumbnail_path, :metadata, NOW(), NOW())
|
||||||
");
|
");
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
'thumbnail_path' => $thumbnailPath
|
'thumbnail_path' => $thumbnailPath,
|
||||||
|
'metadata' => json_encode($actorMetadata)
|
||||||
]);
|
]);
|
||||||
$actorId = $this->pdo->lastInsertId();
|
$actorId = $this->pdo->lastInsertId();
|
||||||
|
|
||||||
|
$this->logProgress("Created new actor {$name} with full Stash metadata");
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $actorId,
|
'id' => $actorId,
|
||||||
'name' => $name,
|
'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
|
protected function getProcessedCount(): int
|
||||||
{
|
{
|
||||||
return $this->processedCount;
|
return $this->processedCount;
|
||||||
|
|||||||
@@ -386,10 +386,16 @@ class XbvrSyncService extends BaseSyncService
|
|||||||
{
|
{
|
||||||
$actors = [];
|
$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;
|
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) {
|
if ($actor) {
|
||||||
$actors[] = $actor;
|
$actors[] = $actor;
|
||||||
}
|
}
|
||||||
@@ -398,37 +404,326 @@ class XbvrSyncService extends BaseSyncService
|
|||||||
return $actors;
|
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
|
// Check if actor already exists
|
||||||
$stmt = $this->pdo->prepare("
|
$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]);
|
$stmt->execute(['name' => $name]);
|
||||||
$existingActor = $stmt->fetch(\PDO::FETCH_ASSOC);
|
$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) {
|
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 [
|
return [
|
||||||
'id' => $existingActor['id'],
|
'id' => $existingActor['id'],
|
||||||
'name' => $existingActor['name'],
|
'name' => $existingActor['name'],
|
||||||
'thumbnail_path' => $existingActor['thumbnail_path']
|
'thumbnail_path' => $thumbnailPath ?: $existingActor['thumbnail_path']
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// For now, we'll create actor without thumbnail
|
// Create new actor with XBVR metadata
|
||||||
// In a full implementation, you'd fetch actor details from XBVR API
|
|
||||||
try {
|
try {
|
||||||
$stmt = $this->pdo->prepare("
|
$stmt = $this->pdo->prepare("
|
||||||
INSERT INTO actors (name, created_at, updated_at)
|
INSERT INTO actors (name, thumbnail_path, metadata, created_at, updated_at)
|
||||||
VALUES (:name, NOW(), NOW())
|
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();
|
$actorId = $this->pdo->lastInsertId();
|
||||||
|
|
||||||
|
$this->logProgress("Created new actor {$name} with XBVR metadata");
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $actorId,
|
'id' => $actorId,
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
'thumbnail_path' => null
|
'thumbnail_path' => $thumbnailPath
|
||||||
];
|
];
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$this->logProgress("Failed to create actor {$name}: " . $e->getMessage());
|
$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...");
|
$this->logProgress("Starting cleanup - detecting deleted VR scenes in XBVR...");
|
||||||
|
|
||||||
// Clean up VR scenes
|
// Clean up VR scenes
|
||||||
$this->cleanupScenes();
|
//$this->cleanupScenes();
|
||||||
|
|
||||||
$this->logProgress("Cleanup completed. Deleted {$this->deletedCount} VR scenes.");
|
$this->logProgress("Cleanup completed. Deleted {$this->deletedCount} VR scenes.");
|
||||||
}
|
}
|
||||||
@@ -545,20 +840,41 @@ class XbvrSyncService extends BaseSyncService
|
|||||||
private function getXbvrScenesForCleanup(): array
|
private function getXbvrScenesForCleanup(): array
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$response = $this->httpClient->get("{$this->baseUrl}/api/scene/list", [
|
// Use the same DeoVR API as the main sync process to ensure consistency
|
||||||
'timeout' => 30,
|
$scenes = $this->getXbvrScenes();
|
||||||
'connect_timeout' => 10
|
|
||||||
]);
|
|
||||||
|
|
||||||
if ($response->getStatusCode() === 200) {
|
// Extract scene IDs from the detailed scene data
|
||||||
$data = json_decode($response->getBody(), true);
|
$sceneIds = array_map(function($scene) {
|
||||||
return $data['scenes'] ?? [];
|
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 [];
|
$this->logProgress("Returning " . count($existingIds) . " existing scene IDs to prevent cleanup");
|
||||||
} catch (Exception $e) {
|
return $existingIds;
|
||||||
$this->logProgress("Error fetching XBVR scenes: " . $e->getMessage());
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"alpinejs": "^3.12.0",
|
"alpinejs": "^3.12.0",
|
||||||
"axios": "^1.4.0",
|
"axios": "^1.4.0"
|
||||||
"bootstrap": "^5.3.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,118 +1,71 @@
|
|||||||
.items,
|
@tailwind base;
|
||||||
.item {
|
@tailwind components;
|
||||||
flex-flow: row wrap;
|
@tailwind utilities;
|
||||||
}
|
|
||||||
.items .item,
|
/* Custom styles for media cards */
|
||||||
.item .item {
|
.items {
|
||||||
margin: 20px;
|
@apply flex flex-wrap;
|
||||||
width: 120px;
|
}
|
||||||
height: 180px;
|
|
||||||
overflow: hidden;
|
.item {
|
||||||
box-shadow: 0 5px 10px rgba(0,0,0,0.8);
|
@apply m-5 w-32 h-48 overflow-hidden shadow-lg transform-gpu transition-transform duration-300 origin-top;
|
||||||
transform-origin: center top;
|
}
|
||||||
transform-style: preserve-3d;
|
|
||||||
transform: translateZ(0);
|
.item img {
|
||||||
transition: 0.3s;
|
@apply w-full min-h-full;
|
||||||
}
|
}
|
||||||
.items .item img,
|
|
||||||
.item .item img {
|
.item figcaption {
|
||||||
width: 100%;
|
@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;
|
||||||
min-height: 100%;
|
}
|
||||||
}
|
|
||||||
.items .item figcaption,
|
.item::after {
|
||||||
.item .item figcaption {
|
content: '';
|
||||||
bottom: 0;
|
@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;
|
||||||
left: 0;
|
}
|
||||||
right: 0;
|
|
||||||
padding: 20px;
|
.item:hover,
|
||||||
padding-bottom: 10px;
|
.item:focus,
|
||||||
font-size: 20px;
|
.item:active {
|
||||||
background: none;
|
@apply shadow-2xl -translate-y-1 scale-105 rotate-x-15;
|
||||||
color: #fff;
|
}
|
||||||
transform: translateY(100%);
|
|
||||||
transition: 0.3s;
|
.item:hover figcaption,
|
||||||
}
|
.item:focus figcaption,
|
||||||
.items .item:after,
|
.item:active figcaption {
|
||||||
.item .item:after {
|
@apply translate-y-0;
|
||||||
content: '';
|
}
|
||||||
z-index: 10;
|
|
||||||
width: 200%;
|
.item:hover::after,
|
||||||
height: 100%;
|
.item:focus::after,
|
||||||
top: -90%;
|
.item:active::after {
|
||||||
left: -20px;
|
@apply rotate-25 -top-2/5 opacity-15;
|
||||||
opacity: 0.1;
|
}
|
||||||
transform: rotate(45deg);
|
|
||||||
background: linear-gradient(to top, transparent, #fff 15%, rgba(255,255,255,0.5));
|
.article {
|
||||||
transition: 0.3s;
|
@apply overflow-hidden w-80 h-56 m-5;
|
||||||
}
|
}
|
||||||
.items .item:hover,
|
|
||||||
.item .item:hover,
|
.article img {
|
||||||
.items .item:focus,
|
@apply w-full min-h-full transition-all duration-200;
|
||||||
.item .item:focus,
|
}
|
||||||
.items .item:active,
|
|
||||||
.item .item:active {
|
.article figcaption {
|
||||||
box-shadow: 0 8px 16px 3px rgba(0,0,0,0.6);
|
@apply absolute inset-0 p-10 text-sm text-white bg-blue-900/60 opacity-0 scale-115 transition-all duration-200 shadow-sm;
|
||||||
transform: translateY(-3px) scale(1.05) rotateX(15deg);
|
}
|
||||||
}
|
|
||||||
.items .item:hover figcaption,
|
.article figcaption h3 {
|
||||||
.item .item:hover figcaption,
|
@apply text-blue-300 text-base mb-0 font-bold;
|
||||||
.items .item:focus figcaption,
|
}
|
||||||
.item .item:focus figcaption,
|
|
||||||
.items .item:active figcaption,
|
.article:hover img,
|
||||||
.item .item:active figcaption {
|
.article:focus img,
|
||||||
transform: none;
|
.article:active img {
|
||||||
}
|
@apply blur-sm scale-95;
|
||||||
.items .item:hover:after,
|
}
|
||||||
.item .item:hover:after,
|
|
||||||
.items .item:focus:after,
|
.article:hover figcaption,
|
||||||
.item .item:focus:after,
|
.article:focus figcaption,
|
||||||
.items .item:active:after,
|
.article:active figcaption {
|
||||||
.item .item:active:after {
|
@apply opacity-100 scale-100;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ use Slim\Views\TwigMiddleware;
|
|||||||
use DI\Container;
|
use DI\Container;
|
||||||
use Twig\TwigFunction;
|
use Twig\TwigFunction;
|
||||||
use Twig\TwigFilter;
|
use Twig\TwigFilter;
|
||||||
|
use \Twig\Extension\DebugExtension;
|
||||||
|
|
||||||
// Create DI Container
|
// Create DI Container
|
||||||
$container = new Container();
|
$container = new Container();
|
||||||
@@ -46,6 +47,7 @@ $container->set('view', function () use ($container) {
|
|||||||
'cache' => $_ENV['APP_ENV'] === 'production' ? __DIR__ . '/../storage/views' : false,
|
'cache' => $_ENV['APP_ENV'] === 'production' ? __DIR__ . '/../storage/views' : false,
|
||||||
'debug' => $_ENV['APP_DEBUG'] === 'true',
|
'debug' => $_ENV['APP_DEBUG'] === 'true',
|
||||||
]);
|
]);
|
||||||
|
$twig->addExtension(new \Twig\Extension\DebugExtension());
|
||||||
|
|
||||||
// Add custom functions
|
// Add custom functions
|
||||||
$twig->getEnvironment()->addFunction(new TwigFunction('base_url', function () {
|
$twig->getEnvironment()->addFunction(new TwigFunction('base_url', function () {
|
||||||
@@ -69,6 +71,9 @@ $container->set('view', function () use ($container) {
|
|||||||
// Handle common route patterns
|
// Handle common route patterns
|
||||||
switch ($name) {
|
switch ($name) {
|
||||||
case 'home':
|
case 'home':
|
||||||
|
$basePath = '/';
|
||||||
|
break;
|
||||||
|
case 'dashboard.index':
|
||||||
$basePath = '/';
|
$basePath = '/';
|
||||||
break;
|
break;
|
||||||
case 'games.index':
|
case 'games.index':
|
||||||
@@ -125,6 +130,9 @@ $container->set('view', function () use ($container) {
|
|||||||
case 'actors.show':
|
case 'actors.show':
|
||||||
$basePath = '/media/actors/' . ($data['id'] ?? '');
|
$basePath = '/media/actors/' . ($data['id'] ?? '');
|
||||||
break;
|
break;
|
||||||
|
case 'actors.edit':
|
||||||
|
$basePath = '/media/actors/' . ($data['id'] ?? '') .'/edit';
|
||||||
|
break;
|
||||||
case 'search.index':
|
case 'search.index':
|
||||||
$basePath = '/search';
|
$basePath = '/search';
|
||||||
break;
|
break;
|
||||||
|
|||||||
280
resources/views/actor/edit.twig
Normal file
280
resources/views/actor/edit.twig
Normal 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 %}
|
||||||
@@ -1,68 +1,346 @@
|
|||||||
{% extends "layouts/app.twig" %}
|
{% extends "layouts/app.twig" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="px-4 py-3">
|
<!-- Hero Section -->
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="relative">
|
||||||
<div>
|
<div class="h-48 md:h-64 relative overflow-hidden bg-gradient-to-br from-purple-900 via-blue-900 to-indigo-900">
|
||||||
<h1 class="h3 fw-bold text-dark mb-1">Performers</h1>
|
<div class="absolute inset-0 bg-gradient-to-r from-purple-900/90 via-blue-900/70 to-indigo-900/90"></div>
|
||||||
<p class="text-muted mb-0">{{ actors|length }} performer{{ actors|length != 1 ? 's' : '' }}</p>
|
|
||||||
|
<!-- 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>
|
||||||
|
{% 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">
|
||||||
|
<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>
|
</div>
|
||||||
<div class="items">
|
</div>
|
||||||
{% if actors %}
|
|
||||||
<div class="row g-3">
|
|
||||||
{% for actor in actors %}
|
|
||||||
|
|
||||||
<figure class="item" style="padding:0px">
|
<!-- Main Content -->
|
||||||
<a href="{{ path_for('actors.show', {'id': actor.id}) }}" class="text-decoration-none">
|
<div class="max-w-7xl mx-auto px-4 py-8">
|
||||||
<img src="{{ actor.thumbnail_path }}" />
|
<!-- Search and Filters Bar -->
|
||||||
<figcaption>{{ actor.name }}</figcaption>
|
<div class="bg-white rounded-xl shadow-lg p-6 mb-8">
|
||||||
</a>
|
<form method="GET" class="space-y-4">
|
||||||
</figure>
|
<!-- 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="col-md-6 col-lg-4 col-xl-2">
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
<div class="item h-100">
|
<svg class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<div class="item-body text-center">
|
<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"/>
|
||||||
<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">
|
|
||||||
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
<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>
|
||||||
|
|
||||||
<h5 class="item-title mb-2">{{ actor.name }}</h5>
|
<!-- Sort Options -->
|
||||||
<p class="item-text small text-muted">
|
<div class="md:w-48">
|
||||||
{{ actor.total_media_count }} scene{{ actor.total_media_count != 1 ? 's' : '' }}
|
<label for="sort" class="block text-sm font-medium text-gray-700 mb-1">Sort By</label>
|
||||||
</p>
|
<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 %}
|
||||||
{% if actor.latest_scene_date %}
|
<option value="{{ value }}" {{ sort == value ? 'selected' : '' }}>{{ label }}</option>
|
||||||
<small class="text-muted d-block">
|
{% endfor %}
|
||||||
Latest: {{ actor.latest_scene_date|date('M j, Y') }}
|
</select>
|
||||||
</small>
|
|
||||||
{% endif %}
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div
|
|
||||||
-->
|
<!-- Filter Checkboxes -->
|
||||||
{% endfor %}
|
<div class="flex flex-wrap gap-6">
|
||||||
</div>
|
<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>
|
</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 %}
|
{% else %}
|
||||||
<div class="text-center py-5">
|
<!-- Empty State -->
|
||||||
<svg class="text-muted mb-3" width="64" height="64" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<div class="text-center py-16">
|
||||||
<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"/>
|
<div class="w-24 h-24 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||||
</svg>
|
<svg class="text-gray-400 w-12 h-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<h5 class="text-muted">No performers found</h5>
|
<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"/>
|
||||||
<p class="text-muted">Performers will appear here once you sync content from your adult video sources.</p>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
<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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,113 +1,280 @@
|
|||||||
{% extends "layouts/app.twig" %}
|
{% extends "layouts/app.twig" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block sidebar %}
|
||||||
<div class="px-4 py-3">
|
{% if actor.metadata %}
|
||||||
<!-- Back button -->
|
{% set metadata = actor.metadata|json_decode %}
|
||||||
<div class="mb-4">
|
<div class="space-y-6">
|
||||||
<a href="{{ path_for('actors.index') }}" class="btn btn-outline-secondary btn-sm">
|
<!-- Personal Information -->
|
||||||
<svg class="me-2" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
{% if metadata.biography or metadata.birth_date or metadata.birth_place or metadata.nationality %}
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
<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>
|
</svg>
|
||||||
Back to Performers
|
Personal Information
|
||||||
</a>
|
</h3>
|
||||||
</div>
|
<div class="space-y-3">
|
||||||
|
{% if metadata.birth_date %}
|
||||||
<div class="card">
|
<div class="flex justify-between items-center">
|
||||||
<div class="row g-0">
|
<span class="text-sm text-gray-600">Birth Date</span>
|
||||||
<!-- Actor image -->
|
<span class="text-sm font-medium">{{ metadata.birth_date }}</span>
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="text-center">
|
|
||||||
{% 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;">
|
|
||||||
{% 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>
|
</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 %}
|
||||||
|
|
||||||
<!-- Actor details and scenes -->
|
<!-- Physical Attributes -->
|
||||||
<div class="col-md-8">
|
{% if metadata.gender or metadata.height or metadata.weight or metadata.hair_color or metadata.eye_color %}
|
||||||
<div class="card-body">
|
<div class="bg-white rounded-xl shadow-lg p-6">
|
||||||
<!-- Actor stats -->
|
<h3 class="text-lg font-semibold text-gray-900 mb-3 flex items-center">
|
||||||
<div class="row g-3 mb-4">
|
<svg class="mr-2 text-green-600" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<div class="col-sm-6">
|
<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"/>
|
||||||
<div class="d-flex align-items-center">
|
</svg>
|
||||||
<svg class="me-2 text-primary" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
Physical Attributes
|
||||||
<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"/>
|
</h3>
|
||||||
</svg>
|
<div class="space-y-3">
|
||||||
<div>
|
{% if metadata.gender %}
|
||||||
<div class="fw-semibold">{{ actor.scene_count }}</div>
|
<div class="flex justify-between items-center">
|
||||||
<small class="text-muted">Total Scenes</small>
|
<span class="text-sm text-gray-600">Gender</span>
|
||||||
</div>
|
<span class="text-sm font-medium">{{ metadata.gender }}</span>
|
||||||
</div>
|
</div>
|
||||||
</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 %}
|
||||||
|
|
||||||
{% if actor.scene_count > 0 %}
|
<!-- Adult-Specific Information -->
|
||||||
<div class="col-sm-6">
|
{% if metadata.measurements or metadata.cup_size or metadata.fake_tits or metadata.penis_length or metadata.circumcised %}
|
||||||
<div class="d-flex align-items-center">
|
<div class="bg-white rounded-xl shadow-lg p-6">
|
||||||
<svg class="me-2 text-success" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<h3 class="text-lg font-semibold text-gray-900 mb-3 flex items-center">
|
||||||
<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 class="mr-2 text-red-600" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
</svg>
|
<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"/>
|
||||||
<div>
|
</svg>
|
||||||
<div class="fw-semibold">{{ actor.latest_scene_date|date('M j, Y') }}</div>
|
Adult Attributes
|
||||||
<small class="text-muted">Latest Scene</small>
|
</h3>
|
||||||
</div>
|
<div class="space-y-3">
|
||||||
</div>
|
{% if metadata.measurements %}
|
||||||
</div>
|
<div class="flex justify-between items-center">
|
||||||
{% endif %}
|
<span class="text-sm text-gray-600">Measurements</span>
|
||||||
</div>
|
<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 %}
|
||||||
|
|
||||||
<!-- Scenes by this actor -->
|
<!-- Career Information -->
|
||||||
{% if scenes %}
|
{% if metadata.career_length or metadata.scene_count %}
|
||||||
<div>
|
<div class="bg-white rounded-xl shadow-lg p-6">
|
||||||
<h3 class="h5 fw-semibold text-dark mb-3">Scenes featuring {{ actor.name }}</h3>
|
<h3 class="text-lg font-semibold text-gray-900 mb-3 flex items-center">
|
||||||
<div class="row g-3">
|
<svg class="mr-2 text-purple-600" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
{% for scene in scenes %}
|
<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"/>
|
||||||
<div class="col-md-6 col-lg-4">
|
</svg>
|
||||||
<div class="card h-100">
|
Career Information
|
||||||
<div style="background-color: #f8f9fa; height: 150px; overflow: hidden;">
|
</h3>
|
||||||
{% if scene.poster_url %}
|
<div class="space-y-3">
|
||||||
<img src="{{ scene.poster_url }}" alt="{{ scene.title }}" class="w-100 h-100" style="background-size: cover;">
|
{% if metadata.career_length %}
|
||||||
{% else %}
|
<div class="flex justify-between items-center">
|
||||||
<div class="w-100 h-100 d-flex align-items-center justify-content-center">
|
<span class="text-sm text-gray-600">Career Length</span>
|
||||||
<svg class="text-muted" width="48" height="48" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<span class="text-sm font-medium">{{ metadata.career_length }}</span>
|
||||||
<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>
|
||||||
</svg>
|
{% endif %}
|
||||||
</div>
|
{% if metadata.scene_count %}
|
||||||
{% endif %}
|
<div class="flex justify-between items-center">
|
||||||
</div>
|
<span class="text-sm text-gray-600">Scene Count</span>
|
||||||
<div class="card-body">
|
<span class="text-sm font-medium">{{ metadata.scene_count }}</span>
|
||||||
<h6 class="card-title mb-1">
|
</div>
|
||||||
<a href="{{ path_for('adult.show', {'id': scene.id}) }}" class="text-decoration-none">{{ scene.title }}</a>
|
{% endif %}
|
||||||
</h6>
|
{% if metadata.adult_specific.debut_year %}
|
||||||
<p class="card-text small text-muted">
|
<div class="flex justify-between items-center">
|
||||||
{{ scene.release_date|date('M j, Y') }}
|
<span class="text-sm text-gray-600">Debut Year</span>
|
||||||
{% if scene.runtime_minutes %}
|
<span class="text-sm font-medium">{{ metadata.adult_specific.debut_year }}</span>
|
||||||
• {{ (scene.runtime_minutes / 60)|round(1) }}h {{ scene.runtime_minutes % 60 }}m
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
{% if metadata.adult_specific.retirement_year %}
|
||||||
<small class="text-muted">{{ scene.source_name }}</small>
|
<div class="flex justify-between items-center">
|
||||||
</div>
|
<span class="text-sm text-gray-600">Retirement Year</span>
|
||||||
</div>
|
<span class="text-sm font-medium">{{ metadata.adult_specific.retirement_year }}</span>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endif %}
|
||||||
</div>
|
{% if metadata.adult_specific.active is defined %}
|
||||||
</div>
|
<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 %}
|
||||||
|
<!-- 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>
|
||||||
|
|
||||||
|
<!-- 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="{% 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 %}
|
{% else %}
|
||||||
<div class="text-center py-5">
|
<div class="w-full h-full bg-gradient-to-br from-blue-400 to-purple-500 flex items-center justify-center">
|
||||||
<svg class="text-muted mb-3" width="64" height="64" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<span class="text-4xl md:text-5xl font-bold text-white">{{ actor.name|first|upper }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 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 %}
|
||||||
|
|
||||||
|
{% 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"/>
|
<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>
|
</svg>
|
||||||
<h5 class="text-muted">No scenes found</h5>
|
<span class="font-medium">{{ actor.tv_show_count }} TV Show{{ actor.tv_show_count != 1 ? 's' : '' }}</span>
|
||||||
<p class="text-muted">This performer hasn't appeared in any scenes yet.</p>
|
</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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@@ -115,4 +282,171 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
|
||||||
|
<!-- 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 %}
|
||||||
@@ -1,175 +1,264 @@
|
|||||||
{% extends "layouts/app.twig" %}
|
{% extends "layouts/app.twig" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block nav_controls %}
|
||||||
<div class="container-fluid">
|
<!-- Search form -->
|
||||||
<div class="row">
|
<form method="GET" class="flex gap-2">
|
||||||
<!-- Sidebar with filters -->
|
<input type="hidden" name="view" value="{{ view_mode }}">
|
||||||
<div class="col-lg-3 col-xl-2">
|
<input type="hidden" name="per_page" value="{{ pagination.per_page }}">
|
||||||
<div class="card">
|
{% for genre in filters.genres %}
|
||||||
<div class="card-header">
|
<input type="hidden" name="genres[]" value="{{ genre }}">
|
||||||
<h5 class="mb-0">Filters</h5>
|
{% endfor %}
|
||||||
</div>
|
{% for director in filters.directors %}
|
||||||
<div class="card-body">
|
<input type="hidden" name="directors[]" value="{{ director }}">
|
||||||
<!-- Filter form -->
|
{% endfor %}
|
||||||
<form method="GET" id="filterForm">
|
{% for source in filters.sources %}
|
||||||
<input type="hidden" name="view" value="{{ view_mode }}">
|
<input type="hidden" name="sources[]" value="{{ source }}">
|
||||||
<input type="hidden" name="per_page" value="{{ pagination.per_page }}">
|
{% endfor %}
|
||||||
<input type="hidden" name="search" value="{{ search }}">
|
<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>
|
||||||
|
<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>
|
||||||
|
|
||||||
<!-- Genre filter -->
|
<!-- View mode switcher -->
|
||||||
{% if available_filters.genres %}
|
<div class="flex gap-1" role="group">
|
||||||
<div class="mb-4">
|
{% for mode in view_modes %}
|
||||||
<h6 class="fw-bold text-dark mb-2">Genres</h6>
|
<a
|
||||||
<select class="form-select select2" name="genres[]" multiple data-placeholder="Select genres...">
|
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 %}"
|
||||||
{% for genre in available_filters.genres %}
|
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' : '' }}"
|
||||||
<option value="{{ genre }}" {{ genre in filters.genres ? 'selected' : '' }}>
|
title="{{ mode|title }} View"
|
||||||
{{ genre }}
|
>
|
||||||
</option>
|
{% if mode == 'grid' %}
|
||||||
{% endfor %}
|
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
</select>
|
<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"/>
|
||||||
</div>
|
</svg>
|
||||||
{% endif %}
|
{% 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>
|
||||||
|
|
||||||
<!-- Director filter -->
|
<!-- Sort dropdown -->
|
||||||
{% if available_filters.directors %}
|
<div class="relative" x-data="{ open: false }">
|
||||||
<div class="mb-4">
|
<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">
|
||||||
<h6 class="fw-bold text-dark mb-2">Directors</h6>
|
<svg class="mr-1 h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<select class="form-select select2" name="directors[]" multiple data-placeholder="Select directors...">
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4h14M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12"/>
|
||||||
{% for director in available_filters.directors %}
|
</svg>
|
||||||
<option value="{{ director }}" {{ director in filters.directors ? 'selected' : '' }}>
|
<span class="hidden sm:inline">Sort</span>
|
||||||
{{ director }}
|
</button>
|
||||||
</option>
|
<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">
|
||||||
{% endfor %}
|
<div class="py-1">
|
||||||
</select>
|
{% for key, label in sort_options %}
|
||||||
</div>
|
<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' : '' }}"
|
||||||
{% 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 %}{% for source in filters.sources %}&sources[]={{ source }}{% endfor %}">
|
||||||
|
{{ label }}
|
||||||
<!-- Filter actions -->
|
{% if sort == key %}
|
||||||
<div class="d-grid gap-2">
|
<svg class="h-4 w-4 text-blue-600" fill="currentColor" viewBox="0 0 16 16">
|
||||||
<button type="submit" class="btn btn-primary btn-sm">
|
<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"/>
|
||||||
Apply Filters
|
</svg>
|
||||||
</button>
|
{% endif %}
|
||||||
<a href="{{ path_for('adult.index') }}" class="btn btn-outline-secondary btn-sm">
|
</a>
|
||||||
Clear All
|
{% endfor %}
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
<!-- Main content area -->
|
{% block sidebar %}
|
||||||
<div class="col-lg-9 col-xl-10">
|
<div class="space-y-4">
|
||||||
<div class="px-4 py-3">
|
<!-- Filters -->
|
||||||
<!-- Header with search and view controls -->
|
<div class="bg-gray-50 rounded-lg p-4">
|
||||||
<div class="d-flex flex-column flex-sm-row justify-content-between align-items-start align-items-sm-center mb-4 gap-3">
|
<h3 class="text-sm font-medium text-gray-900 mb-4">Filters</h3>
|
||||||
<div>
|
|
||||||
<h1 class="display-4 fw-bold text-dark">Adult Videos</h1>
|
|
||||||
{% if pagination.total_items > 0 %}
|
|
||||||
<div class="text-muted small mt-1">
|
|
||||||
{{ pagination.total_items }} videos
|
|
||||||
{% 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>
|
|
||||||
{% endif %}
|
|
||||||
{% if filters.directors %}
|
|
||||||
<span class="badge bg-secondary ms-1">{{ 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">
|
<!-- Filter form -->
|
||||||
<!-- Search form -->
|
<form method="GET" id="filterForm">
|
||||||
<form method="GET" class="d-flex gap-2">
|
<input type="hidden" name="view" value="{{ view_mode }}">
|
||||||
<input type="hidden" name="view" value="{{ view_mode }}">
|
<input type="hidden" name="per_page" value="{{ pagination.per_page }}">
|
||||||
<input type="hidden" name="per_page" value="{{ pagination.per_page }}">
|
<input type="hidden" name="search" value="{{ search }}">
|
||||||
<input type="hidden" name="sort" value="{{ sort }}">
|
<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>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary">
|
|
||||||
Search
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- Sort dropdown -->
|
<!-- Genre filter -->
|
||||||
<div class="dropdown">
|
{% if available_filters.genres %}
|
||||||
<button class="btn btn-outline-secondary dropdown-toggle d-flex align-items-center" type="button" id="sortDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
<div class="mb-4">
|
||||||
<i class="bi bi-sort-down me-1"></i>
|
<label class="block text-xs font-medium text-gray-700 mb-1">Genres</label>
|
||||||
<span class="d-none d-sm-inline">Sort</span>
|
<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...">
|
||||||
</button>
|
{% for genre in available_filters.genres %}
|
||||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="sortDropdown">
|
<option value="{{ genre }}" {{ genre in filters.genres ? 'selected' : '' }}>
|
||||||
{% for key, label in sort_options %}
|
{{ genre }}
|
||||||
<li>
|
</option>
|
||||||
{% set queryParams = {
|
{% endfor %}
|
||||||
'sort': key,
|
</select>
|
||||||
'view': view_mode,
|
</div>
|
||||||
'per_page': pagination.per_page != 24 ? pagination.per_page : null,
|
{% endif %}
|
||||||
'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 -->
|
<!-- Director filter -->
|
||||||
<div class="btn-group" role="group">
|
{% if available_filters.directors %}
|
||||||
{% for mode in view_modes %}
|
<div class="mb-4">
|
||||||
<a
|
<label class="block text-xs font-medium text-gray-700 mb-1">Directors</label>
|
||||||
href="?{{ {
|
<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...">
|
||||||
'view': mode,
|
{% for director in available_filters.directors %}
|
||||||
'sort': sort,
|
<option value="{{ director }}" {{ director in filters.directors ? 'selected' : '' }}>
|
||||||
'per_page': pagination.per_page != 24 ? pagination.per_page : null,
|
{{ director }}
|
||||||
'search': search,
|
</option>
|
||||||
'genres': filters.genres,
|
{% endfor %}
|
||||||
'directors': filters.directors
|
</select>
|
||||||
}|filter((v, k) => v != '' and v is not empty)|url_encode }}"
|
</div>
|
||||||
class="btn btn-outline-secondary {{ view_mode == mode ? 'active' : '' }}"
|
{% endif %}
|
||||||
>
|
|
||||||
{% if mode == 'grid' %}
|
<!-- Source filter -->
|
||||||
<svg class="me-1" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
{% if available_filters.sources %}
|
||||||
<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"/>
|
<div class="mb-4">
|
||||||
</svg>
|
<label class="block text-xs font-medium text-gray-700 mb-1">Sources</label>
|
||||||
{% endif %}
|
<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...">
|
||||||
{% if mode == 'list' %}
|
{% for source in available_filters.sources %}
|
||||||
<svg class="me-1" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<option value="{{ source }}" {{ source in filters.sources ? 'selected' : '' }}>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"/>
|
{{ source }}
|
||||||
</svg>
|
</option>
|
||||||
{% endif %}
|
{% endfor %}
|
||||||
{{ mode|title }}
|
</select>
|
||||||
</a>
|
</div>
|
||||||
{% endfor %}
|
{% endif %}
|
||||||
</div>
|
|
||||||
</div>
|
<!-- Filter actions -->
|
||||||
|
<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="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>
|
||||||
|
|
||||||
|
<!-- 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-gray-500 text-sm mt-1">
|
||||||
|
{{ pagination.total_items }} videos
|
||||||
|
{% if search %}
|
||||||
|
matching "{{ search }}"
|
||||||
|
{% endif %}
|
||||||
|
{% if filters.genres or filters.directors or filters.sources %}
|
||||||
|
{% if filters.genres %}
|
||||||
|
<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="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>
|
||||||
|
|
||||||
|
{% if error %}
|
||||||
|
<div class="alert alert-danger mb-4">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="text-muted small">
|
||||||
|
Sorted by: {{ sort_options[sort] }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if error %}
|
{% 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"/>
|
<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>
|
</svg>
|
||||||
<h3 class="h5 fw-medium text-dark">
|
<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
|
No adult videos found matching your criteria
|
||||||
{% else %}
|
{% else %}
|
||||||
No adult videos found
|
No adult videos found
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-muted">
|
<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.
|
Try adjusting your search terms or filters.
|
||||||
{% else %}
|
{% else %}
|
||||||
Adult videos will appear here after syncing with XBVR or Stash sources.
|
Adult videos will appear here after syncing with XBVR or Stash sources.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</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">
|
<a href="{{ path_for('adult.index') }}" class="btn btn-primary mt-3">
|
||||||
Clear filters
|
Clear filters
|
||||||
</a>
|
</a>
|
||||||
@@ -211,28 +300,28 @@
|
|||||||
<!-- Adult videos content based on view mode -->
|
<!-- Adult videos content based on view mode -->
|
||||||
{% if view_mode == 'list' %}
|
{% if view_mode == 'list' %}
|
||||||
<!-- List view -->
|
<!-- List view -->
|
||||||
<div class="card">
|
<div class="bg-white rounded-lg shadow-md border border-gray-200">
|
||||||
<ul class="list-group list-group-flush">
|
<ul class="divide-y divide-gray-200">
|
||||||
{% for movie in movies %}
|
{% for movie in movies %}
|
||||||
<li class="list-group-item">
|
<li class="px-4 py-3">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="flex justify-between items-center">
|
||||||
<div class="d-flex align-items-center">
|
<div class="flex items-center">
|
||||||
{% if movie.poster_url %}
|
{% 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 %}
|
{% else %}
|
||||||
<div class="bg-light rounded me-3 d-flex align-items-center justify-content-center" style="width: 64px; height: 96px;">
|
<div class="bg-gray-100 rounded mr-3 flex items-center justify-center" style="width: 64px; height: 96px;">
|
||||||
<svg class="text-muted" width="32" height="32" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<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"/>
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="flex-grow-1">
|
<div class="flex-1">
|
||||||
<h3 class="h6 mb-1">
|
<h3 class="text-sm font-semibold mb-1">
|
||||||
<a href="{{ path_for('adult.show', {'id': movie.id}) }}" class="text-decoration-none">
|
<a href="{{ path_for('adult.show', {'id': movie.id}) }}" class="no-underline text-gray-900 hover:text-blue-600">
|
||||||
{{ movie.title }}
|
{{ movie.title }}
|
||||||
</a>
|
</a>
|
||||||
</h3>
|
</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 %}
|
{% if movie.release_date %}
|
||||||
<span>{{ movie.release_date|date('Y') }}</span>
|
<span>{{ movie.release_date|date('Y') }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -246,14 +335,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2">
|
<div class="flex gap-2">
|
||||||
{% if movie.watched %}
|
{% 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
|
Watched
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if movie.is_favorite %}
|
{% 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
|
Favorite
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -265,32 +354,58 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% elseif view_mode == 'covers' %}
|
{% elseif view_mode == 'covers' %}
|
||||||
<!-- Cover grid view -->
|
<!-- Enhanced 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 xl:grid-cols-8 gap-4">
|
||||||
{% for movie in movies %}
|
{% for movie in movies %}
|
||||||
<div class="col-6 col-sm-4 col-md-3 col-lg-2">
|
<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">
|
||||||
<div class="card h-100">
|
{% if movie.poster_url %}
|
||||||
{% if movie.poster_url %}
|
<div class="relative aspect-[2/3] overflow-hidden">
|
||||||
<div class="position-relative" style="background-color: #f8f9fa; border-radius: 0.375rem; 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">
|
||||||
<img src="{{ movie.poster_url }}" alt="{{ movie.title }}" class="card-img-top w-100" style="max-height: 300px; object-fit: contain;">
|
<!-- 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>
|
||||||
<div class="d-flex align-items-center justify-content-center bg-light" style="aspect-ratio: 2/3; min-height: 200px;">
|
{% else %}
|
||||||
<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]">
|
||||||
<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 class="text-gray-400 w-12 h-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
</svg>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 4V2a1 1 0 011-1h8a1 1 0 011 1v2h4a1 1 0 010 2h-1v14a2 2 0 01-2 2H6a2 2 0 01-2-2V6H3a1 1 0 010-2h4z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="p-4">
|
||||||
|
<h6 class="text-sm font-bold truncate mb-1" title="{{ movie.title }}">
|
||||||
|
<a href="{{ path_for('adult.show', {'id': movie.id}) }}" class="no-underline text-gray-900 hover:text-blue-600 transition-colors">
|
||||||
|
{{ movie.title }}
|
||||||
|
</a>
|
||||||
|
</h6>
|
||||||
|
{% if movie.release_date %}
|
||||||
|
<p class="text-xs text-gray-600 font-medium">{{ movie.release_date|date('Y') }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% if 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>
|
</div>
|
||||||
{% endif %}
|
{% 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">
|
|
||||||
{{ movie.title }}
|
|
||||||
</a>
|
|
||||||
</h6>
|
|
||||||
{% if movie.release_date %}
|
|
||||||
<p class="card-text small text-muted">{{ movie.release_date|date('Y') }}</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -298,67 +413,65 @@
|
|||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<!-- Default grid view -->
|
<!-- 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 %}
|
{% for movie in movies %}
|
||||||
<div class="col-12 col-md-6 col-lg-3 col-xl-2">
|
<div class="bg-white rounded-lg shadow-md border border-gray-200 h-full">
|
||||||
<div class="card h-100">
|
<div class="p-4">
|
||||||
<div class="card-body">
|
<div class="flex items-center">
|
||||||
<div class="d-flex align-items-center">
|
<div class="flex-shrink-0">
|
||||||
<div class="flex-shrink-0">
|
{% if movie.poster_url %}
|
||||||
{% 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 }}">
|
||||||
<img class="rounded" style="width: 64px; height: 96px; object-fit: contain; background-color: #f8f9fa;" src="{{ movie.poster_url }}" alt="{{ movie.title }}">
|
{% else %}
|
||||||
{% else %}
|
<div class="bg-gray-100 rounded flex items-center justify-center" style="width: 64px; height: 96px;">
|
||||||
<div class="bg-light rounded d-flex align-items-center justify-content-center" style="width: 64px; height: 96px;">
|
<svg class="text-gray-600" width="32" height="32" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<svg class="text-muted" 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"/>
|
||||||
<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>
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</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">
|
|
||||||
{{ movie.title }}
|
|
||||||
</a>
|
|
||||||
</h5>
|
|
||||||
<div class="d-flex align-items-center gap-2 small text-muted mb-2">
|
|
||||||
{% if movie.release_date %}
|
|
||||||
<span>{{ movie.release_date|date('Y') }}</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if movie.rating %}
|
|
||||||
<span>⭐ {{ movie.rating }}/10</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% if movie.source_name %}
|
|
||||||
<p class="card-text small text-muted mb-2">
|
|
||||||
{{ movie.source_name }}
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</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;">
|
|
||||||
{{ 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">
|
|
||||||
{% if movie.runtime_minutes %}
|
|
||||||
<span>{{ (movie.runtime_minutes / 60)|round(1) }}h {{ movie.runtime_minutes % 60 }}m</span>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="d-flex gap-1">
|
</div>
|
||||||
{% if movie.watched %}
|
<div class="ml-3 flex-1">
|
||||||
<span class="badge bg-success">
|
<h5 class="text-lg font-semibold mb-1">
|
||||||
Watched
|
<a href="{{ path_for('adult.show', {'id': movie.id}) }}" class="no-underline text-gray-900 hover:text-blue-600">
|
||||||
</span>
|
{{ movie.title }}
|
||||||
|
</a>
|
||||||
|
</h5>
|
||||||
|
<div class="flex items-center gap-2 text-sm text-gray-600 mb-2">
|
||||||
|
{% if movie.release_date %}
|
||||||
|
<span>{{ movie.release_date|date('Y') }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if movie.is_favorite %}
|
{% if movie.rating %}
|
||||||
<span class="badge bg-danger">
|
<span>⭐ {{ movie.rating }}/10</span>
|
||||||
Favorite
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% if movie.source_name %}
|
||||||
|
<p class="text-sm text-gray-600 mb-2">
|
||||||
|
{{ movie.source_name }}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if movie.overview %}
|
||||||
|
<div class="mt-3">
|
||||||
|
<p class="text-sm text-gray-600" style="display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;">
|
||||||
|
{{ movie.overview|slice(0, 150) }}{% if movie.overview|length > 150 %}...{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="mt-3 flex justify-between items-center text-sm text-gray-600">
|
||||||
|
{% if movie.runtime_minutes %}
|
||||||
|
<span>{{ (movie.runtime_minutes / 60)|round(1) }}h {{ movie.runtime_minutes % 60 }}m</span>
|
||||||
|
{% endif %}
|
||||||
|
<div class="flex gap-1">
|
||||||
|
{% if movie.watched %}
|
||||||
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||||
|
Watched
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if movie.is_favorite %}
|
||||||
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
|
||||||
|
Favorite
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -367,42 +480,89 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Top Pagination & Controls -->
|
||||||
{% if pagination.total_pages > 1 %}
|
{% if pagination.total_pages > 1 %}
|
||||||
<div class="d-flex align-items-center justify-content-between mt-4">
|
<div class="flex items-center justify-between mb-6 p-4 bg-gray-50 rounded-lg">
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="flex items-center gap-4">
|
||||||
<label for="per_page" class="form-label mb-0">Show:</label>
|
<div class="text-sm text-gray-700">
|
||||||
<select id="per_page" class="form-select form-select-sm w-auto">
|
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
|
||||||
<option value="12" {{ pagination.per_page == 12 ? 'selected' : '' }}>12</option>
|
</div>
|
||||||
<option value="24" {{ pagination.per_page == 24 ? 'selected' : '' }}>24</option>
|
<div class="flex items-center gap-2">
|
||||||
<option value="48" {{ pagination.per_page == 48 ? 'selected' : '' }}>48</option>
|
<label for="per_page_top" class="text-sm font-medium text-gray-700">Show:</label>
|
||||||
<option value="100" {{ pagination.per_page == 100 ? 'selected' : '' }}>100</option>
|
<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">
|
||||||
</select>
|
<option value="12" {{ pagination.per_page == 12 ? 'selected' : '' }}>12</option>
|
||||||
<span class="text-muted small">per page</span>
|
<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-sm text-gray-600">per page</span>
|
||||||
|
</div>
|
||||||
</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 %}
|
{% 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 %}"
|
<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="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
|
Previous
|
||||||
</a>
|
</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 %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="btn-group" role="group">
|
<!-- Page Numbers -->
|
||||||
{% for page_num in range(max(1, pagination.current_page - 2), min(pagination.total_pages, pagination.current_page + 2)) %}
|
{% set start_page = max(1, 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 %}"
|
{% set end_page = min(pagination.total_pages, pagination.current_page + 2) %}
|
||||||
class="btn btn-sm {{ page_num == pagination.current_page ? 'btn-primary' : 'btn-outline-secondary' }}">
|
|
||||||
{{ page_num }}
|
|
||||||
</a>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
{% 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 %}
|
{% 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 %}"
|
<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="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
|
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>
|
</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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -462,6 +622,13 @@ document.getElementById('per_page')?.addEventListener('change', function() {
|
|||||||
url.searchParams.set('page', '1'); // Reset to first page
|
url.searchParams.set('page', '1'); // Reset to first page
|
||||||
window.location = url.toString();
|
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>
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,330 +1,275 @@
|
|||||||
{% extends 'layouts/app.twig' %}
|
{% extends 'layouts/app.twig' %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="mb-4">
|
<div class="p-6">
|
||||||
<h1 class="display-4 fw-bold text-dark">Dashboard</h1>
|
<div class="mb-6">
|
||||||
<p class="lead text-muted">Overview of your media collection</p>
|
<h1 class="text-3xl font-bold text-gray-900">Dashboard</h1>
|
||||||
</div>
|
<p class="text-lg text-gray-600 mt-2">Overview of your media collection</p>
|
||||||
|
|
||||||
{% if error %}
|
|
||||||
<div class="alert alert-danger mb-4">
|
|
||||||
{{ error }}
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Stats Grid -->
|
{% if error %}
|
||||||
<div class="row g-3 mt-4">
|
<div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-6">
|
||||||
<!-- Total Media -->
|
{{ error }}
|
||||||
<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="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">
|
|
||||||
<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>
|
|
||||||
</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="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">
|
|
||||||
<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>
|
|
||||||
</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="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">
|
|
||||||
<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>
|
|
||||||
</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="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">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Additional Stats -->
|
|
||||||
<div class="row g-3 mt-4">
|
|
||||||
<!-- 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="flex-shrink-0">
|
|
||||||
<svg class="text-info" width="24" height="24" 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">
|
|
||||||
{% 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="flex-shrink-0">
|
|
||||||
<svg class="text-secondary" width="24" height="24" 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>
|
|
||||||
</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="flex-shrink-0">
|
|
||||||
<svg class="text-muted" width="24" height="24" 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">
|
|
||||||
{% 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>
|
|
||||||
|
|
||||||
<!-- 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">
|
|
||||||
{% 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">
|
|
||||||
{% if game.image_url %}
|
|
||||||
<img class="rounded me-3" style="width: 40px; height: 40px; object-fit: cover;" 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">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-muted small">
|
|
||||||
{% if game.playtime_minutes %}
|
|
||||||
{{ (game.playtime_minutes / 60)|round }}h played
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Recent Movies -->
|
<!-- Stats Grid -->
|
||||||
{% if recent_movies %}
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||||
<div class="mt-3">
|
<!-- Total Media -->
|
||||||
<h3 class="h5 fw-medium text-dark mb-3">Recently Watched Movies</h3>
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||||
<div class="card">
|
<div class="flex items-center">
|
||||||
<ul class="list-group list-group-flush">
|
<div class="flex-shrink-0">
|
||||||
{% for movie in recent_movies %}
|
<svg class="text-blue-600 w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<li class="list-group-item">
|
<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>
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
</svg>
|
||||||
<div class="d-flex align-items-center">
|
</div>
|
||||||
{% if movie.poster_url %}
|
<div class="ml-4 flex-1">
|
||||||
<img class="rounded me-3" style="width: 40px; height: 40px; object-fit: cover;" src="{{ movie.poster_url }}" alt="">
|
<dt class="text-sm font-medium text-gray-500 truncate">Total Media</dt>
|
||||||
{% else %}
|
<dd class="text-2xl font-semibold text-gray-900">{{ stats.total_media|number_format }}</dd>
|
||||||
<div class="bg-light rounded me-3 d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;">
|
</div>
|
||||||
<svg class="text-muted" width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
</div>
|
||||||
<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>
|
</div>
|
||||||
</svg>
|
|
||||||
</div>
|
<!-- Games -->
|
||||||
{% endif %}
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||||
<div>
|
<div class="flex items-center">
|
||||||
<p class="mb-0 fw-medium">{{ movie.title }}</p>
|
<div class="flex-shrink-0">
|
||||||
<p class="mb-0 text-muted small">{{ movie.source_name }}</p>
|
<svg class="text-green-600 w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
</div>
|
<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>
|
||||||
</div>
|
</svg>
|
||||||
<div class="text-muted small">
|
</div>
|
||||||
{% if movie.watch_count %}
|
<div class="ml-4 flex-1">
|
||||||
Watched {{ movie.watch_count }} times
|
<dt class="text-sm font-medium text-gray-500 truncate">Games</dt>
|
||||||
{% endif %}
|
<dd class="text-2xl font-semibold text-gray-900">{{ stats.total_games|number_format }}</dd>
|
||||||
</div>
|
<div class="text-sm text-gray-500">{{ stats.favorite_games }} favorites</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</div>
|
||||||
{% endfor %}
|
</div>
|
||||||
</ul>
|
|
||||||
|
<!-- Movies & TV -->
|
||||||
|
<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-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="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="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-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="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>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Recent Syncs -->
|
<!-- Additional Stats -->
|
||||||
{% if recent_syncs %}
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||||
<div class="mt-3">
|
<!-- Total Playtime -->
|
||||||
<h3 class="h5 fw-medium text-dark mb-3">Recent Sync Activities</h3>
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||||
<div class="card">
|
<div class="flex items-center">
|
||||||
<ul class="list-group list-group-flush">
|
<div class="flex-shrink-0">
|
||||||
{% for sync in recent_syncs %}
|
<svg class="text-cyan-600 w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<li class="list-group-item">
|
<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>
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
</svg>
|
||||||
<div class="d-flex align-items-center">
|
</div>
|
||||||
<div class="flex-shrink-0">
|
<div class="ml-4 flex-1">
|
||||||
{% if sync.status == 'completed' %}
|
<dt class="text-sm font-medium text-gray-500 truncate">Total Playtime</dt>
|
||||||
<svg class="text-success" width="20" height="20" fill="currentColor" viewBox="0 0 20 20">
|
<dd class="text-2xl font-semibold text-gray-900">
|
||||||
<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>
|
{% if stats.total_playtime %}
|
||||||
</svg>
|
{{ (stats.total_playtime / 60)|round }}h
|
||||||
{% elseif sync.status == 'failed' %}
|
{% else %}
|
||||||
<svg class="text-danger" width="20" height="20" fill="currentColor" viewBox="0 0 20 20">
|
0h
|
||||||
<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>
|
{% endif %}
|
||||||
</svg>
|
</dd>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Total Episodes -->
|
||||||
|
<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-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="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="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-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="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 %}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recent Activity -->
|
||||||
|
<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>
|
||||||
|
<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="px-6 py-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
{% if game.image_url %}
|
||||||
|
<img class="w-10 h-10 rounded-lg object-cover mr-4" src="{{ game.image_url }}" alt="">
|
||||||
{% else %}
|
{% else %}
|
||||||
<svg class="text-warning" width="20" height="20" fill="currentColor" viewBox="0 0 20 20">
|
<div class="w-10 h-10 rounded-lg bg-gray-100 flex items-center justify-center mr-4">
|
||||||
<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 class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
</svg>
|
<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="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-sm text-gray-500">
|
||||||
|
{% if game.playtime_minutes %}
|
||||||
|
{{ (game.playtime_minutes / 60)|round }}h played
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="ms-3">
|
</div>
|
||||||
<p class="mb-0 fw-medium">{{ sync.source_name }}</p>
|
</li>
|
||||||
<p class="mb-0 text-muted small">{{ sync.sync_type|title }} sync</p>
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Recent Movies -->
|
||||||
|
{% if recent_movies %}
|
||||||
|
<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="px-6 py-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
{% if movie.poster_url %}
|
||||||
|
<img class="w-10 h-10 rounded-lg object-cover mr-4" src="{{ movie.poster_url }}" alt="">
|
||||||
|
{% else %}
|
||||||
|
<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="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-sm text-gray-500">
|
||||||
|
{% if movie.watch_count %}
|
||||||
|
Watched {{ movie.watch_count }} times
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-muted small">
|
</li>
|
||||||
{{ sync.processed_items }} items • {{ sync.created_at|date('M j, Y') }}
|
{% endfor %}
|
||||||
</div>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{% endif %}
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if not recent_games and not recent_movies and not recent_syncs %}
|
<!-- Recent Syncs -->
|
||||||
<div class="card mt-3">
|
{% if recent_syncs %}
|
||||||
<div class="card-body text-center">
|
<div>
|
||||||
<svg class="mx-auto text-muted mb-3" width="48" height="48" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<h3 class="text-lg font-medium text-gray-900 mb-4">Recent Sync Activities</h3>
|
||||||
<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>
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
||||||
</svg>
|
<ul class="divide-y divide-gray-200">
|
||||||
<h3 class="h5 fw-medium text-dark">No recent activity</h3>
|
{% for sync in recent_syncs %}
|
||||||
<p class="text-muted">Start adding media to see your activity here.</p>
|
<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="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="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="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="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-sm text-gray-500">
|
||||||
|
{{ sync.processed_items }} items • {{ sync.created_at|date('M j, Y') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if not recent_games and not recent_movies and not recent_syncs %}
|
||||||
|
<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="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 %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,172 +1,224 @@
|
|||||||
{% extends "layouts/app.twig" %}
|
{% extends "layouts/app.twig" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block nav_controls %}
|
||||||
<div class="container-fluid">
|
<!-- Search form -->
|
||||||
<div class="row">
|
<form method="GET" class="flex gap-2">
|
||||||
<!-- Sidebar with filters -->
|
<input type="hidden" name="view" value="{{ view_mode }}">
|
||||||
<div class="col-lg-3 col-xl-2">
|
<input type="hidden" name="per_page" value="{{ pagination.per_page }}">
|
||||||
<div class="card">
|
{% for genre in filters.genres %}
|
||||||
<div class="card-header">
|
<input type="hidden" name="genres[]" value="{{ genre }}">
|
||||||
<h5 class="mb-0">Filters</h5>
|
{% endfor %}
|
||||||
</div>
|
{% for platform in filters.platforms %}
|
||||||
<div class="card-body">
|
<input type="hidden" name="platforms[]" value="{{ platform }}">
|
||||||
<!-- Filter form -->
|
{% endfor %}
|
||||||
<form method="GET" id="filterForm">
|
<div class="relative">
|
||||||
<input type="hidden" name="view" value="{{ view_mode }}">
|
<input
|
||||||
<input type="hidden" name="per_page" value="{{ pagination.per_page }}">
|
type="text"
|
||||||
<input type="hidden" name="search" value="{{ search }}">
|
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>
|
||||||
|
<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>
|
||||||
|
|
||||||
<!-- Genre filter -->
|
<!-- View mode switcher -->
|
||||||
{% if available_filters.genres %}
|
<div class="flex gap-1" role="group">
|
||||||
<div class="mb-4">
|
{% for mode in view_modes %}
|
||||||
<h6 class="fw-bold text-dark mb-2">Genres</h6>
|
<a
|
||||||
<select class="form-select select2" name="genres[]" multiple data-placeholder="Select genres...">
|
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 %}"
|
||||||
{% for genre in available_filters.genres %}
|
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' : '' }}"
|
||||||
<option value="{{ genre }}" {{ genre in filters.genres ? 'selected' : '' }}>
|
title="{{ mode|title }} View"
|
||||||
{{ genre }}
|
>
|
||||||
</option>
|
{% if mode == 'grid' %}
|
||||||
{% endfor %}
|
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
</select>
|
<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"/>
|
||||||
</div>
|
</svg>
|
||||||
{% endif %}
|
{% 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>
|
||||||
|
|
||||||
<!-- Platform filter -->
|
<!-- Sort dropdown -->
|
||||||
{% if available_filters.platforms %}
|
<div class="relative" x-data="{ open: false }">
|
||||||
<div class="mb-4">
|
<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">
|
||||||
<h6 class="fw-bold text-dark mb-2">Platforms</h6>
|
<svg class="mr-1 h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<select class="form-select select2" name="platforms[]" multiple data-placeholder="Select platforms...">
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4h14M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12"/>
|
||||||
{% for platform in available_filters.platforms %}
|
</svg>
|
||||||
<option value="{{ platform }}" {{ platform in filters.platforms ? 'selected' : '' }}>
|
<span class="hidden sm:inline">Sort</span>
|
||||||
{{ platform }}
|
</button>
|
||||||
</option>
|
<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">
|
||||||
{% endfor %}
|
<div class="py-1">
|
||||||
</select>
|
{% for key, label in sort_options %}
|
||||||
</div>
|
<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' : '' }}"
|
||||||
{% 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 platform in filters.platforms %}&platforms[]={{ platform }}{% endfor %}">
|
||||||
|
{{ label }}
|
||||||
<!-- Filter actions -->
|
{% if sort == key %}
|
||||||
<div class="d-grid gap-2">
|
<svg class="h-4 w-4 text-blue-600" fill="currentColor" viewBox="0 0 16 16">
|
||||||
<button type="submit" class="btn btn-primary btn-sm">
|
<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"/>
|
||||||
Apply Filters
|
</svg>
|
||||||
</button>
|
{% endif %}
|
||||||
<a href="{{ path_for('games.index') }}" class="btn btn-outline-secondary btn-sm">
|
</a>
|
||||||
Clear All
|
{% endfor %}
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
<!-- Main content area -->
|
{% block sidebar %}
|
||||||
<div class="col-lg-9 col-xl-10">
|
<div class="space-y-4">
|
||||||
<div class="px-4 py-3">
|
<!-- Filters -->
|
||||||
<!-- Header with search and view controls -->
|
<div class="bg-gray-50 rounded-lg p-4">
|
||||||
<div class="d-flex flex-column flex-sm-row justify-content-between align-items-start align-items-sm-center mb-4 gap-3">
|
<h3 class="text-sm font-medium text-gray-900 mb-4">Filters</h3>
|
||||||
<div>
|
|
||||||
<h1 class="display-4 fw-bold text-dark">Games</h1>
|
|
||||||
{% if pagination.total_items > 0 %}
|
|
||||||
<div class="text-muted small 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>
|
|
||||||
{% endif %}
|
|
||||||
{% if filters.platforms %}
|
|
||||||
<span class="badge bg-secondary ms-1">{{ 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">
|
<!-- Filter form -->
|
||||||
<!-- Search form -->
|
<form method="GET" id="filterForm">
|
||||||
<form method="GET" class="d-flex gap-2">
|
<input type="hidden" name="view" value="{{ view_mode }}">
|
||||||
<input type="hidden" name="view" value="{{ view_mode }}">
|
<input type="hidden" name="per_page" value="{{ pagination.per_page }}">
|
||||||
<input type="hidden" name="per_page" value="{{ pagination.per_page }}">
|
<input type="hidden" name="search" value="{{ search }}">
|
||||||
{% 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">
|
<!-- Genre filter -->
|
||||||
<!-- Sort dropdown -->
|
{% if available_filters.genres %}
|
||||||
<div class="dropdown">
|
<div class="mb-4">
|
||||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button" id="sortDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
<label class="block text-xs font-medium text-gray-700 mb-1">Genres</label>
|
||||||
<svg class="me-1" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<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...">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12"/>
|
{% for genre in available_filters.genres %}
|
||||||
</svg>
|
<option value="{{ genre }}" {{ genre in filters.genres ? 'selected' : '' }}>
|
||||||
<span class="d-none d-sm-inline">Sort</span>
|
{{ genre }}
|
||||||
</button>
|
</option>
|
||||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="sortDropdown">
|
{% endfor %}
|
||||||
{% for key, label in sort_options %}
|
</select>
|
||||||
<li>
|
</div>
|
||||||
<a class="dropdown-item d-flex justify-content-between align-items-center {{ 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 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 -->
|
<!-- Platform filter -->
|
||||||
<div class="btn-group" role="group">
|
{% if available_filters.platforms %}
|
||||||
{% for mode in view_modes %}
|
<div class="mb-4">
|
||||||
<a
|
<label class="block text-xs font-medium text-gray-700 mb-1">Platforms</label>
|
||||||
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 %}"
|
<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...">
|
||||||
class="btn btn-outline-secondary {{ view_mode == mode ? 'active' : '' }}"
|
{% for platform in available_filters.platforms %}
|
||||||
title="{{ mode|title }} View"
|
<option value="{{ platform }}" {{ platform in filters.platforms ? 'selected' : '' }}>
|
||||||
>
|
{{ platform }}
|
||||||
{% if mode == 'grid' %}
|
</option>
|
||||||
<svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
{% endfor %}
|
||||||
<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"/>
|
</select>
|
||||||
</svg>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if mode == 'list' %}
|
|
||||||
<svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<!-- Filter actions -->
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"/>
|
<div class="space-y-2">
|
||||||
</svg>
|
<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">
|
||||||
{% endif %}
|
Apply Filters
|
||||||
{% if mode == 'covers' %}
|
</button>
|
||||||
<svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<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">
|
||||||
<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"/>
|
Clear All
|
||||||
</svg>
|
</a>
|
||||||
{% endif %}
|
</div>
|
||||||
<span class="d-none d-sm-inline ms-1">{{ mode|title }}</span>
|
</form>
|
||||||
</a>
|
</div>
|
||||||
{% endfor %}
|
|
||||||
</div>
|
<!-- Active Filters Summary -->
|
||||||
</div>
|
{% if filters.genres or filters.platforms or search %}
|
||||||
</div>
|
<div class="bg-blue-50 rounded-lg p-4">
|
||||||
</div>
|
<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-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="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="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>
|
||||||
|
|
||||||
{% if games is empty %}
|
{% if games is empty %}
|
||||||
<div class="text-center py-5">
|
<div class="text-center py-5">
|
||||||
@@ -197,31 +249,31 @@
|
|||||||
<!-- Games content based on view mode -->
|
<!-- Games content based on view mode -->
|
||||||
{% if view_mode == 'list' %}
|
{% if view_mode == 'list' %}
|
||||||
<!-- List view -->
|
<!-- List view -->
|
||||||
<div class="card">
|
<div class="bg-white rounded-lg shadow-md border border-gray-200">
|
||||||
<ul class="list-group list-group-flush">
|
<ul class="divide-y divide-gray-200">
|
||||||
{% for game in games %}
|
{% for game in games %}
|
||||||
<li class="list-group-item">
|
<li class="px-4 py-3">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="flex justify-between items-center">
|
||||||
<div class="d-flex align-items-center">
|
<div class="flex items-center">
|
||||||
{% if game.image_url %}
|
{% 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 %}
|
{% else %}
|
||||||
<div class="bg-light rounded me-3 d-flex align-items-center justify-content-center" style="width: 64px; height: 64px;">
|
<div class="bg-gray-100 rounded mr-3 flex items-center justify-center" style="width: 64px; height: 64px;">
|
||||||
<svg class="text-muted" width="32" height="32" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<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"/>
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="flex-grow-1">
|
<div class="flex-1">
|
||||||
<h3 class="h6 mb-1">
|
<h3 class="text-sm font-semibold mb-1">
|
||||||
<a href="{{ path_for('games.show', {'game_key': game.game_key}) }}" class="text-decoration-none">
|
<a href="{{ path_for('games.show', {'game_key': game.game_key}) }}" class="no-underline text-gray-900 hover:text-blue-600">
|
||||||
{{ game.title }}
|
{{ game.title }}
|
||||||
</a>
|
</a>
|
||||||
</h3>
|
</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>
|
<span>{{ game.platform_count }} platform{{ game.platform_count > 1 ? 's' : '' }}</span>
|
||||||
{% if game.platforms %}
|
{% 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(', ') }}
|
{{ game.platforms|join(', ') }}
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -233,9 +285,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if game.genres %}
|
{% 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) %}
|
{% 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 }}
|
{{ genre }}
|
||||||
</span>
|
</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -249,27 +301,25 @@
|
|||||||
|
|
||||||
{% elseif view_mode == 'covers' %}
|
{% elseif view_mode == 'covers' %}
|
||||||
<!-- Cover grid view -->
|
<!-- 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 %}
|
{% for game in games %}
|
||||||
<div class="col-6 col-sm-4 col-md-3 col-lg-1">
|
<div class="bg-white rounded-lg shadow-md border border-gray-200 overflow-hidden h-full">
|
||||||
<div class="card h-100">
|
{% if game.image_url %}
|
||||||
{% if game.image_url %}
|
<div class="relative aspect-[3/4] overflow-hidden">
|
||||||
<div class="position-relative" style="aspect-ratio: 3/4; overflow: hidden;">
|
<img src="/images/playnite/{{ game.image_url }}" alt="{{ game.title }}" class="w-full h-full object-cover">
|
||||||
<img src="/images/playnite/{{ game.image_url }}" alt="{{ game.title }}" class="card-img-top" style="width: 100%; height: 100%; object-fit: cover;">
|
</div>
|
||||||
</div>
|
{% else %}
|
||||||
{% else %}
|
<div class="flex items-center justify-center bg-gray-100 aspect-[3/4] min-h-[200px]">
|
||||||
<div class="d-flex align-items-center justify-content-center bg-light" style="aspect-ratio: 3/4; min-height: 200px;">
|
<svg class="text-gray-600" width="48" height="48" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<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="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"/>
|
||||||
<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>
|
||||||
</svg>
|
</div>
|
||||||
</div>
|
{% endif %}
|
||||||
{% endif %}
|
<div class="p-3">
|
||||||
<div class="card-body">
|
<h6 class="text-sm font-semibold truncate" title="{{ game.title }}">
|
||||||
<h6 class="card-title text-truncate" title="{{ game.title }}">
|
{{ game.title }}
|
||||||
{{ game.title }}
|
</h6>
|
||||||
</h6>
|
<p class="text-xs text-gray-600 mt-1">{{ game.platform_count }} platform{{ game.platform_count > 1 ? 's' : '' }}</p>
|
||||||
<p class="card-text small text-muted">{{ game.platform_count }} platform{{ game.platform_count > 1 ? 's' : '' }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -277,56 +327,54 @@
|
|||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<!-- Default grid view -->
|
<!-- 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 %}
|
{% for game in games %}
|
||||||
<div class="col-12 col-md-6 col-lg-4">
|
<div class="bg-white rounded-lg shadow-md border border-gray-200 h-full">
|
||||||
<div class="card h-100">
|
<div class="p-4">
|
||||||
<div class="card-body">
|
<div class="flex items-center">
|
||||||
<div class="d-flex align-items-center">
|
<div class="flex-shrink-0">
|
||||||
<div class="flex-shrink-0">
|
{% if game.image_url %}
|
||||||
{% if game.image_url %}
|
<img class="rounded" style="width: 64px; height: 64px; object-fit: cover;" src="/images/playnite/{{ game.image_url }}" alt="{{ game.title }}">
|
||||||
<img class="rounded" style="width: 64px; height: 64px; object-fit: cover;" src="/images/playnite/{{ game.image_url }}" alt="{{ game.title }}">
|
{% else %}
|
||||||
{% else %}
|
<div class="bg-gray-100 rounded flex items-center justify-center" style="width: 64px; height: 64px;">
|
||||||
<div class="bg-light rounded d-flex align-items-center justify-content-center" style="width: 64px; height: 64px;">
|
<svg class="text-gray-600" width="32" height="32" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<svg class="text-muted" 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"/>
|
||||||
<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>
|
||||||
</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">
|
|
||||||
{{ game.title }}
|
|
||||||
</a>
|
|
||||||
</h5>
|
|
||||||
<p class="card-text small text-muted mb-2">
|
|
||||||
{{ game.platform_count }} platform{{ game.platform_count > 1 ? 's' : '' }}
|
|
||||||
{% if game.platforms %}
|
|
||||||
<span class="badge bg-light text-dark ms-2">
|
|
||||||
{{ game.platforms|join(', ') }}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-3">
|
|
||||||
<div class="d-flex justify-content-between align-items-center small text-muted">
|
|
||||||
<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">
|
|
||||||
{% for genre in game.genres|slice(0, 3) %}
|
|
||||||
<span class="badge bg-primary">
|
|
||||||
{{ genre }}
|
|
||||||
</span>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
<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="text-sm text-gray-600 mb-2">
|
||||||
|
{{ game.platform_count }} platform{{ game.platform_count > 1 ? 's' : '' }}
|
||||||
|
{% if game.platforms %}
|
||||||
|
<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 %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<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 flex flex-wrap gap-1">
|
||||||
|
{% for genre in game.genres|slice(0, 3) %}
|
||||||
|
<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 %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -334,42 +382,89 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Top Pagination & Controls -->
|
||||||
{% if pagination.total_pages > 1 %}
|
{% if pagination.total_pages > 1 %}
|
||||||
<div class="d-flex align-items-center justify-content-between mt-4">
|
<div class="flex items-center justify-between mb-6 p-4 bg-gray-50 rounded-lg">
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="flex items-center gap-4">
|
||||||
<label for="per_page" class="form-label mb-0">Show:</label>
|
<div class="text-sm text-gray-700">
|
||||||
<select id="per_page" class="form-select form-select-sm w-auto">
|
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
|
||||||
<option value="12" {{ pagination.per_page == 12 ? 'selected' : '' }}>12</option>
|
</div>
|
||||||
<option value="24" {{ pagination.per_page == 24 ? 'selected' : '' }}>24</option>
|
<div class="flex items-center gap-2">
|
||||||
<option value="48" {{ pagination.per_page == 48 ? 'selected' : '' }}>48</option>
|
<label for="per_page_top" class="text-sm font-medium text-gray-700">Show:</label>
|
||||||
<option value="100" {{ pagination.per_page == 100 ? 'selected' : '' }}>100</option>
|
<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">
|
||||||
</select>
|
<option value="12" {{ pagination.per_page == 12 ? 'selected' : '' }}>12</option>
|
||||||
<span class="text-muted small">per page</span>
|
<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-sm text-gray-600">per page</span>
|
||||||
|
</div>
|
||||||
</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 %}
|
{% 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 %}"
|
<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
|
Previous
|
||||||
</a>
|
</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 %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="btn-group" role="group">
|
<!-- Page Numbers -->
|
||||||
{% for page_num in range(max(1, pagination.current_page - 2), min(pagination.total_pages, pagination.current_page + 2)) %}
|
{% set start_page = max(1, 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 %}"
|
{% set end_page = min(pagination.total_pages, pagination.current_page + 2) %}
|
||||||
class="btn btn-sm {{ page_num == pagination.current_page ? 'btn-primary' : 'btn-outline-secondary' }}">
|
|
||||||
{{ page_num }}
|
|
||||||
</a>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
{% 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 %}
|
{% 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 %}"
|
<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
|
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>
|
</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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -429,6 +524,13 @@ document.getElementById('per_page')?.addEventListener('change', function() {
|
|||||||
url.searchParams.set('page', '1'); // Reset to first page
|
url.searchParams.set('page', '1'); // Reset to first page
|
||||||
window.location = url.toString();
|
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>
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -6,11 +6,9 @@
|
|||||||
<title>{{ title }} - Media Collector</title>
|
<title>{{ title }} - Media Collector</title>
|
||||||
<!-- Iconify Icons -->
|
<!-- Iconify Icons -->
|
||||||
<script src="https://code.iconify.design/iconify-icon/1.0.7/iconify-icon.min.js"></script>
|
<script src="https://code.iconify.design/iconify-icon/1.0.7/iconify-icon.min.js"></script>
|
||||||
<!-- Bootstrap CSS CDN -->
|
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
||||||
<!-- Select2 CSS -->
|
<!-- Tailwind 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 -->
|
|
||||||
{% if app_env == 'production' %}
|
{% if app_env == 'production' %}
|
||||||
{% if manifest['resources/css/app.css'] is defined %}
|
{% if manifest['resources/css/app.css'] is defined %}
|
||||||
<link rel="stylesheet" href="{{ base_url() }}/build/assets/{{ manifest['resources/css/app.css'].file }}">
|
<link rel="stylesheet" href="{{ base_url() }}/build/assets/{{ manifest['resources/css/app.css'].file }}">
|
||||||
@@ -18,119 +16,377 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<link rel="stylesheet" href="{{ base_url() }}/css/app.css">
|
<link rel="stylesheet" href="{{ base_url() }}/css/app.css">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" type="image/svg+xml" href="{{ base_url() }}/favicon.svg">
|
<link rel="icon" type="image/svg+xml" href="{{ base_url() }}/favicon.svg">
|
||||||
|
|
||||||
<!-- Iconify Icons -->
|
<!-- Iconify Icons -->
|
||||||
<script src="https://code.iconify.design/iconify-icon/1.0.7/iconify-icon.min.js"></script>
|
<script src="https://code.iconify.design/iconify-icon/1.0.7/iconify-icon.min.js"></script>
|
||||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
|
|
||||||
{# DebugBar Assets #}
|
{# DebugBar Assets #}
|
||||||
{% if debugbarRenderer is defined %}
|
{% if debugbarRenderer is defined %}
|
||||||
{{ debugbarRenderer.renderHead()|raw }}
|
{{ debugbarRenderer.renderHead()|raw }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- In the head section -->
|
<!-- Alpine.js -->
|
||||||
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
<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) -->
|
||||||
<!-- jQuery (required for Select2) -->
|
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
<!-- Select2 CSS -->
|
||||||
<!-- Select2 JS -->
|
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet">
|
||||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
<!-- Select2 JS -->
|
||||||
{% if app_env == 'production' and manifest['resources/js/app.js'] is defined %}
|
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||||
<script type="module" src="{{ base_url() }}/build/assets/{{ manifest['resources/js/app.js'].file }}"></script>
|
|
||||||
{% else %}
|
{% if app_env == 'production' and manifest['resources/js/app.js'] is defined %}
|
||||||
<script type="module" src="{{ base_url() }}/resources/js/app.js"></script>
|
<script type="module" src="{{ base_url() }}/build/assets/{{ manifest['resources/js/app.js'].file }}"></script>
|
||||||
{% endif %}
|
{% else %}
|
||||||
|
<script type="module" src="{{ base_url() }}/resources/js/app.js"></script>
|
||||||
|
{% endif %}
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-light">
|
<body class="bg-gray-50 text-gray-900 min-h-screen" x-data="{ mobileMenuOpen: false }">
|
||||||
<!-- Navigation -->
|
<!-- Navigation -->
|
||||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary shadow-lg">
|
<nav class="bg-slate-900 text-white shadow-lg">
|
||||||
<div class="container-fluid">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<a class="navbar-brand fw-bold" href="{{ path_for('home') }}">Media Collector</a>
|
<div class="flex justify-between items-center h-16">
|
||||||
|
<!-- Left Section: App Title + Mobile Menu -->
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
<div class="flex items-center">
|
||||||
<span class="navbar-toggler-icon"></span>
|
<!-- Mobile menu button -->
|
||||||
</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">
|
||||||
<div class="collapse navbar-collapse" id="navbarNav">
|
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<ul class="navbar-nav me-auto">
|
<path x-show="!mobileMenuOpen" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||||
<li class="nav-item">
|
<path x-show="mobileMenuOpen" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
<a class="nav-link {% if current_route == 'home' %}active{% endif %}" href="{{ path_for('home') }}">Dashboard</a>
|
</svg>
|
||||||
</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"/>
|
|
||||||
</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>
|
</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>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</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 -->
|
<!-- DebugBar -->
|
||||||
{% if debugbarRenderer is defined %}
|
{% if debugbarRenderer is defined %}
|
||||||
{{ debugbarRenderer.render()|raw }}
|
{{ debugbarRenderer.render()|raw }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Page Content -->
|
<!-- Page Content -->
|
||||||
<main class="container-fluid py-4">
|
<main class="flex min-h-screen">
|
||||||
{% block content %}{% endblock %}
|
<!-- 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>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,184 +1,294 @@
|
|||||||
{% extends "layouts/app.twig" %}
|
{% extends "layouts/app.twig" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block nav_controls %}
|
||||||
<div class="container-fluid">
|
<!-- Search form -->
|
||||||
<div class="row">
|
<form method="GET" class="flex gap-2">
|
||||||
<!-- Sidebar with filters -->
|
<input type="hidden" name="view" value="{{ view_mode }}">
|
||||||
<div class="col-lg-3 col-xl-2">
|
<input type="hidden" name="per_page" value="{{ pagination.per_page }}">
|
||||||
<div class="card">
|
{% for genre in filters.genres %}
|
||||||
<div class="card-header">
|
<input type="hidden" name="genres[]" value="{{ genre }}">
|
||||||
<h5 class="mb-0">Filters</h5>
|
{% endfor %}
|
||||||
</div>
|
{% for director in filters.directors %}
|
||||||
<div class="card-body">
|
<input type="hidden" name="directors[]" value="{{ director }}">
|
||||||
<!-- Filter form -->
|
{% endfor %}
|
||||||
<form method="GET" id="filterForm">
|
<div class="relative">
|
||||||
<input type="hidden" name="view" value="{{ view_mode }}">
|
<input
|
||||||
<input type="hidden" name="per_page" value="{{ pagination.per_page }}">
|
type="text"
|
||||||
<input type="hidden" name="search" value="{{ search }}">
|
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>
|
||||||
|
<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>
|
||||||
|
|
||||||
<!-- Genre filter -->
|
<!-- View mode switcher -->
|
||||||
{% if available_filters.genres %}
|
<div class="flex gap-1" role="group">
|
||||||
<div class="mb-4">
|
{% for mode in view_modes %}
|
||||||
<h6 class="fw-bold text-dark mb-2">Genres</h6>
|
<a
|
||||||
<select class="form-select select2" name="genres[]" multiple data-placeholder="Select genres...">
|
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 genre in available_filters.genres %}
|
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' : '' }}"
|
||||||
<option value="{{ genre }}" {{ genre in filters.genres ? 'selected' : '' }}>
|
title="{{ mode|title }} View"
|
||||||
{{ genre }}
|
>
|
||||||
</option>
|
{% if mode == 'grid' %}
|
||||||
{% endfor %}
|
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
</select>
|
<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"/>
|
||||||
</div>
|
</svg>
|
||||||
{% endif %}
|
{% 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>
|
||||||
|
|
||||||
<!-- Director filter -->
|
<!-- Sort dropdown -->
|
||||||
{% if available_filters.directors %}
|
<div class="relative" x-data="{ open: false }">
|
||||||
<div class="mb-4">
|
<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">
|
||||||
<h6 class="fw-bold text-dark mb-2">Directors</h6>
|
<svg class="mr-1 h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<select class="form-select select2" name="directors[]" multiple data-placeholder="Select directors...">
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4h14M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12"/>
|
||||||
{% for director in available_filters.directors %}
|
</svg>
|
||||||
<option value="{{ director }}" {{ director in filters.directors ? 'selected' : '' }}>
|
<span class="hidden sm:inline">Sort</span>
|
||||||
{{ director }}
|
</button>
|
||||||
</option>
|
<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">
|
||||||
{% endfor %}
|
<div class="py-1">
|
||||||
</select>
|
{% for key, label in sort_options %}
|
||||||
</div>
|
<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' : '' }}"
|
||||||
{% 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 }}
|
||||||
<!-- Filter actions -->
|
{% if sort == key %}
|
||||||
<div class="d-grid gap-2">
|
<svg class="h-4 w-4 text-blue-600" fill="currentColor" viewBox="0 0 16 16">
|
||||||
<button type="submit" class="btn btn-primary btn-sm">
|
<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"/>
|
||||||
Apply Filters
|
</svg>
|
||||||
</button>
|
{% endif %}
|
||||||
<a href="{{ path_for('movies.index') }}" class="btn btn-outline-secondary btn-sm">
|
</a>
|
||||||
Clear All
|
{% endfor %}
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
<!-- Main content area -->
|
{% block item_list %}
|
||||||
<div class="col-lg-9 col-xl-10">
|
{% if view_mode == 'list' and movies %}
|
||||||
<div class="px-4 py-3">
|
<div class="space-y-4">
|
||||||
<!-- Header with search and view controls -->
|
<!-- Group movies by first letter -->
|
||||||
<div class="d-flex flex-column flex-sm-row justify-content-between align-items-start align-items-sm-center mb-4 gap-3">
|
{% set grouped_movies = {} %}
|
||||||
<div>
|
{% for movie in movies %}
|
||||||
<h1 class="display-4 fw-bold text-dark">Movies</h1>
|
{% set first_letter = movie.title|first|upper %}
|
||||||
{% if pagination.total_items > 0 %}
|
{% if grouped_movies[first_letter] is not defined %}
|
||||||
<div class="text-muted small mt-1">
|
{% set grouped_movies = grouped_movies|merge({(first_letter): []}) %}
|
||||||
{{ pagination.total_items }} movies
|
{% endif %}
|
||||||
{% if search %}
|
{% set grouped_movies = grouped_movies|merge({(first_letter): grouped_movies[first_letter]|merge([movie])}) %}
|
||||||
matching "{{ search }}"
|
{% endfor %}
|
||||||
{% endif %}
|
|
||||||
{% if filters.genres or filters.directors %}
|
|
||||||
{% if filters.genres %}
|
|
||||||
<span class="badge bg-primary ms-1">{{ filters.genres|join(', ') }}</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if filters.directors %}
|
|
||||||
<span class="badge bg-secondary ms-1">{{ 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">
|
{% for letter, letter_movies in grouped_movies|sort %}
|
||||||
<!-- Search form -->
|
<div class="space-y-2">
|
||||||
<form method="GET" class="d-flex gap-2">
|
<h4 class="text-sm font-semibold text-gray-900 border-b border-gray-200 pb-1">{{ letter }}</h4>
|
||||||
<input type="hidden" name="view" value="{{ view_mode }}">
|
<div class="space-y-1">
|
||||||
<input type="hidden" name="per_page" value="{{ pagination.per_page }}">
|
{% for movie in letter_movies %}
|
||||||
{% for genre in filters.genres %}
|
<a href="{{ path_for('movies.show', {'id': movie.id}) }}" class="flex items-center p-2 rounded-md hover:bg-gray-100 transition-colors">
|
||||||
<input type="hidden" name="genres[]" value="{{ genre }}">
|
{% if movie.poster_url %}
|
||||||
{% endfor %}
|
<img class="w-8 h-12 object-cover rounded mr-3 flex-shrink-0" src="/images/{{ movie.poster_url }}" alt="{{ movie.title }}">
|
||||||
{% for director in filters.directors %}
|
{% else %}
|
||||||
<input type="hidden" name="directors[]" value="{{ director }}">
|
<div class="w-8 h-12 bg-gray-200 rounded mr-3 flex-shrink-0 flex items-center justify-center">
|
||||||
{% endfor %}
|
<svg class="w-4 h-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<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">
|
|
||||||
<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 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>
|
</svg>
|
||||||
<h3 class="h5 fw-medium text-dark">
|
</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 }}">
|
||||||
|
<input type="hidden" name="per_page" value="{{ pagination.per_page }}">
|
||||||
|
<input type="hidden" name="search" value="{{ search }}">
|
||||||
|
|
||||||
|
<!-- Genre filter -->
|
||||||
|
{% if available_filters.genres %}
|
||||||
|
<div class="mb-4">
|
||||||
|
<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 }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Director filter -->
|
||||||
|
{% if available_filters.directors %}
|
||||||
|
<div class="mb-4">
|
||||||
|
<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 }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Filter actions -->
|
||||||
|
<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="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>
|
||||||
|
|
||||||
|
<!-- 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-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="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="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>
|
||||||
|
|
||||||
|
{% if movies is empty %}
|
||||||
|
<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="text-lg font-medium text-gray-900 mb-2">
|
||||||
{% if search or filters.genres or filters.directors %}
|
{% if search or filters.genres or filters.directors %}
|
||||||
No movies found matching your criteria
|
No movies found matching your criteria
|
||||||
{% else %}
|
{% else %}
|
||||||
No movies found
|
No movies found
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-muted">
|
<p class="text-gray-500 mb-4">
|
||||||
{% if search or filters.genres or filters.directors %}
|
{% if search or filters.genres or filters.directors %}
|
||||||
Try adjusting your search terms or filters.
|
Try adjusting your search terms or filters.
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -186,7 +296,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
{% if search or filters.genres or filters.directors %}
|
{% 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
|
Clear filters
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -195,28 +305,28 @@
|
|||||||
<!-- Movies content based on view mode -->
|
<!-- Movies content based on view mode -->
|
||||||
{% if view_mode == 'list' %}
|
{% if view_mode == 'list' %}
|
||||||
<!-- List view -->
|
<!-- List view -->
|
||||||
<div class="card">
|
<div class="bg-white rounded-lg shadow-md border border-gray-200">
|
||||||
<ul class="list-group list-group-flush">
|
<ul class="divide-y divide-gray-200">
|
||||||
{% for movie in movies %}
|
{% for movie in movies %}
|
||||||
<li class="list-group-item">
|
<li class="px-4 py-3">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="flex justify-between items-center">
|
||||||
<div class="d-flex align-items-center">
|
<div class="flex items-center">
|
||||||
{% if movie.poster_url %}
|
{% 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 %}
|
{% else %}
|
||||||
<div class="bg-light rounded me-3 d-flex align-items-center justify-content-center" style="width: 64px; height: 96px;">
|
<div class="bg-gray-100 rounded mr-3 flex items-center justify-center" style="width: 64px; height: 96px;">
|
||||||
<svg class="text-muted" width="32" height="32" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<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"/>
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="flex-grow-1">
|
<div class="flex-1">
|
||||||
<h3 class="h6 mb-1">
|
<h3 class="text-sm font-semibold mb-1">
|
||||||
<a href="{{ path_for('movies.show', {'id': movie.id}) }}" class="text-decoration-none">
|
<a href="{{ path_for('movies.show', {'id': movie.id}) }}" class="no-underline text-gray-900 hover:text-blue-600">
|
||||||
{{ movie.title }}
|
{{ movie.title }}
|
||||||
</a>
|
</a>
|
||||||
</h3>
|
</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 %}
|
{% if movie.release_date %}
|
||||||
<span>{{ movie.release_date|date('Y') }}</span>
|
<span>{{ movie.release_date|date('Y') }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -230,14 +340,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2">
|
<div class="flex gap-2">
|
||||||
{% if movie.watched %}
|
{% 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
|
Watched
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if movie.is_favorite %}
|
{% 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
|
Favorite
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -249,114 +359,124 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% elseif view_mode == 'covers' %}
|
{% elseif view_mode == 'covers' %}
|
||||||
<!-- Cover grid view -->
|
<!-- 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">
|
||||||
<div class="items">
|
|
||||||
<div class="row g-3">
|
|
||||||
|
|
||||||
|
|
||||||
{% for movie in movies %}
|
{% for movie in movies %}
|
||||||
|
<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">
|
||||||
<figure class="item" style="padding:0px">
|
{% if movie.poster_url %}
|
||||||
<a href="{{ path_for('movies.show', {'id': movie.id}) }}" class="text-decoration-none">
|
<div class="relative aspect-[2/3] overflow-hidden">
|
||||||
<img src="/images/{{ 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 duration-300">
|
||||||
<figcaption>{{ movie.title }}</figcaption>
|
<!-- Overlay with movie info -->
|
||||||
</a>
|
<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">
|
||||||
</figure>
|
<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">
|
||||||
<div class="col-6 col-sm-4 col-md-3 col-lg-2">
|
<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="card h-100">
|
</svg>
|
||||||
{% if movie.poster_url %}
|
<span class="text-white text-sm font-medium">{{ movie.rating }}</span>
|
||||||
<div class="position-relative" style="aspect-ratio: 2/3; overflow: hidden;">
|
</div>
|
||||||
<img src="/images/{{ movie.poster_url }}" alt="{{ movie.title }}" class="card-img-top" style="width: 100%; height: 100%; object-fit: cover;">
|
{% endif %}
|
||||||
</div>
|
{% if movie.watched %}
|
||||||
{% else %}
|
<div class="flex items-center mb-1">
|
||||||
<div class="d-flex align-items-center justify-content-center bg-light" style="aspect-ratio: 2/3; min-height: 200px;">
|
<svg class="w-3 h-3 text-green-400 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||||
<svg class="text-muted" width="48" height="48" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<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"/>
|
||||||
<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>
|
||||||
</svg>
|
<span class="text-green-400 text-xs">Watched</span>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="card-body">
|
</div>
|
||||||
<h6 class="card-title text-truncate" title="{{ movie.title }}">
|
|
||||||
<a href="{{ path_for('movies.show', {'id': movie.id}) }}" class="text-decoration-none">
|
|
||||||
{{ movie.title }}
|
|
||||||
</a>
|
|
||||||
</h6>
|
|
||||||
{% if movie.release_date %}
|
|
||||||
<p class="card-text small text-muted">{{ movie.release_date|date('Y') }}</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>-->
|
{% else %}
|
||||||
|
<div class="flex items-center justify-center bg-gradient-to-br from-gray-100 to-gray-200 aspect-[2/3] min-h-[200px]">
|
||||||
|
<svg class="text-gray-400 w-12 h-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 4V2a1 1 0 011-1h8a1 1 0 011 1v2h4a1 1 0 010 2h-1v14a2 2 0 01-2 2H6a2 2 0 01-2-2V6H3a1 1 0 010-2h4z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="p-4">
|
||||||
|
<h6 class="text-sm font-bold truncate mb-1" title="{{ movie.title }}">
|
||||||
|
<a href="{{ path_for('movies.show', {'id': movie.id}) }}" class="no-underline text-gray-900 hover:text-blue-600 transition-colors">
|
||||||
|
{{ movie.title }}
|
||||||
|
</a>
|
||||||
|
</h6>
|
||||||
|
{% if movie.release_date %}
|
||||||
|
<p class="text-xs text-gray-600 font-medium">{{ movie.release_date|date('Y') }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% if 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 %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<!-- Default grid view -->
|
<!-- 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 %}
|
{% for movie in movies %}
|
||||||
<div class="col-12 col-md-6 col-lg-3 col-xl-2">
|
<div class="bg-white rounded-lg shadow-md border border-gray-200 h-full">
|
||||||
<div class="card h-100">
|
<div class="p-4">
|
||||||
<div class="card-body">
|
<div class="flex items-center">
|
||||||
<div class="d-flex align-items-center">
|
<div class="flex-shrink-0">
|
||||||
<div class="flex-shrink-0">
|
{% if movie.poster_url %}
|
||||||
{% if movie.poster_url %}
|
<img class="rounded" style="width: 64px; height: 96px; object-fit: cover;" src="/images/{{ movie.poster_url }}" alt="{{ movie.title }}">
|
||||||
<img class="rounded" style="width: 64px; height: 96px; object-fit: cover;" src="/images/{{ movie.poster_url }}" alt="{{ movie.title }}">
|
{% else %}
|
||||||
{% else %}
|
<div class="bg-gray-100 rounded flex items-center justify-center" style="width: 64px; height: 96px;">
|
||||||
<div class="bg-light rounded d-flex align-items-center justify-content-center" style="width: 64px; height: 96px;">
|
<svg class="text-gray-600" width="32" height="32" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<svg class="text-muted" 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"/>
|
||||||
<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>
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</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">
|
|
||||||
{{ movie.title }}
|
|
||||||
</a>
|
|
||||||
</h5>
|
|
||||||
<div class="d-flex align-items-center gap-2 small text-muted mb-2">
|
|
||||||
{% if movie.release_date %}
|
|
||||||
<span>{{ movie.release_date|date('Y') }}</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if movie.rating %}
|
|
||||||
<span>⭐ {{ movie.rating }}/10</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% if movie.source_name %}
|
|
||||||
<p class="card-text small text-muted mb-2">
|
|
||||||
{{ movie.source_name }}
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</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;">
|
|
||||||
{{ 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">
|
|
||||||
{% if movie.runtime_minutes %}
|
|
||||||
<span>{{ (movie.runtime_minutes / 60)|round(1) }}h {{ movie.runtime_minutes % 60 }}m</span>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="d-flex gap-1">
|
</div>
|
||||||
{% if movie.watched %}
|
<div class="ml-3 flex-1">
|
||||||
<span class="badge bg-success">
|
<h5 class="text-lg font-semibold mb-1">
|
||||||
Watched
|
<a href="{{ path_for('movies.show', {'id': movie.id}) }}" class="no-underline text-gray-900 hover:text-blue-600">
|
||||||
</span>
|
{{ movie.title }}
|
||||||
|
</a>
|
||||||
|
</h5>
|
||||||
|
<div class="flex items-center gap-2 text-sm text-gray-600 mb-2">
|
||||||
|
{% if movie.release_date %}
|
||||||
|
<span>{{ movie.release_date|date('Y') }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if movie.is_favorite %}
|
{% if movie.rating %}
|
||||||
<span class="badge bg-danger">
|
<span>⭐ {{ movie.rating }}/10</span>
|
||||||
Favorite
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% if movie.source_name %}
|
||||||
|
<p class="text-sm text-gray-600 mb-2">
|
||||||
|
{{ movie.source_name }}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if movie.overview %}
|
||||||
|
<div class="mt-3">
|
||||||
|
<p class="text-sm text-gray-600" style="display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;">
|
||||||
|
{{ movie.overview|slice(0, 150) }}{% if movie.overview|length > 150 %}...{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="mt-3 flex justify-between items-center text-sm text-gray-600">
|
||||||
|
{% if movie.runtime_minutes %}
|
||||||
|
<span>{{ (movie.runtime_minutes / 60)|round(1) }}h {{ movie.runtime_minutes % 60 }}m</span>
|
||||||
|
{% endif %}
|
||||||
|
<div class="flex gap-1">
|
||||||
|
{% if movie.watched %}
|
||||||
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||||
|
Watched
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if movie.is_favorite %}
|
||||||
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
|
||||||
|
Favorite
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -365,42 +485,89 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Top Pagination & Controls -->
|
||||||
{% if pagination.total_pages > 1 %}
|
{% if pagination.total_pages > 1 %}
|
||||||
<div class="d-flex align-items-center justify-content-between mt-4">
|
<div class="flex items-center justify-between mb-6 p-4 bg-gray-50 rounded-lg">
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="flex items-center gap-4">
|
||||||
<label for="per_page" class="form-label mb-0">Show:</label>
|
<div class="text-sm text-gray-700">
|
||||||
<select id="per_page" class="form-select form-select-sm w-auto">
|
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
|
||||||
<option value="12" {{ pagination.per_page == 12 ? 'selected' : '' }}>12</option>
|
</div>
|
||||||
<option value="24" {{ pagination.per_page == 24 ? 'selected' : '' }}>24</option>
|
<div class="flex items-center gap-2">
|
||||||
<option value="48" {{ pagination.per_page == 48 ? 'selected' : '' }}>48</option>
|
<label for="per_page_top" class="text-sm font-medium text-gray-700">Show:</label>
|
||||||
<option value="100" {{ pagination.per_page == 100 ? 'selected' : '' }}>100</option>
|
<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">
|
||||||
</select>
|
<option value="12" {{ pagination.per_page == 12 ? 'selected' : '' }}>12</option>
|
||||||
<span class="text-muted small">per page</span>
|
<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-sm text-gray-600">per page</span>
|
||||||
|
</div>
|
||||||
</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 %}
|
{% 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 %}"
|
<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
|
Previous
|
||||||
</a>
|
</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 %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="btn-group" role="group">
|
<!-- Page Numbers -->
|
||||||
{% for page_num in range(max(1, pagination.current_page - 2), min(pagination.total_pages, pagination.current_page + 2)) %}
|
{% set start_page = max(1, 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 %}"
|
{% set end_page = min(pagination.total_pages, pagination.current_page + 2) %}
|
||||||
class="btn btn-sm {{ page_num == pagination.current_page ? 'btn-primary' : 'btn-outline-secondary' }}">
|
|
||||||
{{ page_num }}
|
|
||||||
</a>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
{% 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 %}
|
{% 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 %}"
|
<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
|
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>
|
</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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -504,6 +671,13 @@ document.getElementById('per_page')?.addEventListener('change', function() {
|
|||||||
url.searchParams.set('page', '1'); // Reset to first page
|
url.searchParams.set('page', '1'); // Reset to first page
|
||||||
window.location = url.toString();
|
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>
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,349 +1,413 @@
|
|||||||
{% extends "layouts/app.twig" %}
|
{% extends "layouts/app.twig" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid px-4 py-3">
|
<!-- Hero Section with Backdrop -->
|
||||||
<!-- Back button -->
|
<div class="relative">
|
||||||
<div class="mb-4">
|
{% if movie.backdrop_url %}
|
||||||
<a href="{{ path_for('movies.index') }}" class="btn btn-link text-decoration-none p-0">
|
<div class="h-96 md:h-[500px] relative overflow-hidden">
|
||||||
<svg class="me-2" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<img src="/images/{{ movie.backdrop_url }}" alt="{{ movie.title }} backdrop" class="w-full h-full object-cover">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
<div class="absolute inset-0 bg-gradient-to-t from-black via-black/50 to-transparent"></div>
|
||||||
</svg>
|
<div class="absolute inset-0 bg-gradient-to-r from-black/80 via-black/40 to-transparent"></div>
|
||||||
Back to Movies
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card shadow-sm">
|
<!-- Back button -->
|
||||||
<div class="row g-0">
|
<div class="absolute top-4 left-4 z-10">
|
||||||
<!-- Movie poster -->
|
<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">
|
||||||
<div class="col-md-4">
|
<svg class="mr-2" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<div class="card-body p-4">
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
||||||
<div class="ratio ratio-2x3">
|
</svg>
|
||||||
|
Back to Movies
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 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="text-xl md:text-2xl text-gray-200 mb-4 drop-shadow-md">{{ movie.tagline }}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Quick Info -->
|
||||||
|
<div class="flex flex-wrap gap-4 text-sm text-white mb-4">
|
||||||
|
{% if movie.release_date %}
|
||||||
|
<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') }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if movie.rating %}
|
||||||
|
<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
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if movie.runtime_minutes %}
|
||||||
|
<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 %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status badges -->
|
||||||
|
<div class="flex flex-wrap gap-2 mb-4">
|
||||||
|
{% if movie.watched %}
|
||||||
|
<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.is_favorite %}
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- 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 %}
|
{% if movie.poster_url %}
|
||||||
<img src="/images/{{ movie.poster_url }}" alt="{{ movie.title }}" class="img-fluid rounded">
|
<img src="/images/{{ movie.poster_url }}" alt="{{ movie.title }}" class="w-full h-full object-cover">
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="w-100 h-100 d-flex align-items-center justify-content-center bg-light rounded">
|
<div class="w-full h-full flex items-center justify-center bg-gradient-to-br from-gray-100 to-gray-200">
|
||||||
<svg class="text-muted" width="96" height="96" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<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"/>
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Action buttons -->
|
<!-- Quick Stats -->
|
||||||
<div class="mt-4 d-grid gap-2">
|
<div class="bg-white rounded-xl shadow-lg p-6">
|
||||||
<button class="btn btn-primary">
|
<h3 class="text-lg font-semibold text-gray-900 mb-4">Quick Stats</h3>
|
||||||
<svg class="me-2" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<div class="space-y-3">
|
||||||
<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"/>
|
{% if movie.vote_count %}
|
||||||
</svg>
|
<div class="flex justify-between items-center">
|
||||||
Mark as Watched
|
<span class="text-gray-600">Votes</span>
|
||||||
</button>
|
<span class="font-medium">{{ movie.vote_count|number_format }}</span>
|
||||||
<button class="btn btn-outline-danger">
|
</div>
|
||||||
<svg class="me-2" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
{% endif %}
|
||||||
<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"/>
|
{% if movie.watch_count %}
|
||||||
</svg>
|
<div class="flex justify-between items-center">
|
||||||
Add to Favorites
|
<span class="text-gray-600">Watch Count</span>
|
||||||
</button>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Movie details -->
|
<!-- Main Details -->
|
||||||
<div class="col-md-8">
|
<div class="lg:col-span-2 space-y-8">
|
||||||
<div class="card-body p-4">
|
<!-- Overview -->
|
||||||
<div class="mb-4">
|
{% if movie.overview %}
|
||||||
<h1 class="display-4 fw-bold text-dark mb-2">{{ movie.title }}</h1>
|
<div class="bg-white rounded-xl shadow-lg p-6">
|
||||||
{% if movie.tagline %}
|
<h2 class="text-2xl font-bold text-gray-900 mb-4">Overview</h2>
|
||||||
<p class="lead text-muted mb-3">{{ movie.tagline }}</p>
|
<p class="text-gray-700 leading-relaxed">{{ movie.overview }}</p>
|
||||||
{% endif %}
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<!-- Movie metadata -->
|
<!-- Cast & Crew -->
|
||||||
<div class="d-flex flex-wrap gap-3 small text-muted mb-3">
|
<div class="bg-white rounded-xl shadow-lg p-6">
|
||||||
{% if movie.release_date %}
|
<h2 class="text-2xl font-bold text-gray-900 mb-6">Cast & Crew</h2>
|
||||||
<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="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') }}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if movie.rating %}
|
<!-- Director and Writer -->
|
||||||
<span class="d-flex align-items-center">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||||
<svg class="me-1" width="16" height="16" fill="currentColor" viewBox="0 0 20 20">
|
{% if movie.director %}
|
||||||
<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="flex items-start">
|
||||||
</svg>
|
<div class="bg-blue-100 rounded-full p-3 mr-4">
|
||||||
{{ movie.rating }}/10
|
<svg class="text-blue-600" width="24" height="24" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
</span>
|
<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"/>
|
||||||
{% endif %}
|
</svg>
|
||||||
|
|
||||||
{% 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">
|
|
||||||
<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>
|
</div>
|
||||||
|
<div>
|
||||||
<!-- Status badges -->
|
<h3 class="font-semibold text-gray-900">Director</h3>
|
||||||
<div class="d-flex flex-wrap gap-2">
|
<p class="text-gray-600">{{ movie.director }}</p>
|
||||||
{% 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">
|
|
||||||
<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">
|
|
||||||
<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 %}
|
|
||||||
|
|
||||||
{% if movie.status %}
|
|
||||||
<span class="badge bg-secondary">{{ movie.status|title }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 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>
|
|
||||||
{% 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 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">
|
|
||||||
<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>
|
|
||||||
</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">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if actors %}
|
|
||||||
<div class="mt-3">
|
|
||||||
<small class="text-muted d-block mb-2">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>
|
|
||||||
|
|
||||||
<!-- Movie Details Grid -->
|
|
||||||
<div class="row g-4 mb-4">
|
|
||||||
<!-- 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">
|
|
||||||
{% for genre in movie.genre|split(',') %}
|
|
||||||
<span class="badge bg-primary">{{ genre|trim }}</span>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- 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">
|
|
||||||
{% if movie.production_companies %}
|
|
||||||
<small class="text-muted d-block">Companies</small>
|
|
||||||
<span class="fw-medium">{{ movie.production_companies }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% 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>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- 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">
|
|
||||||
{% if movie.budget %}
|
|
||||||
<small class="text-muted d-block">Budget</small>
|
|
||||||
<span class="fw-medium">${{ movie.budget|number_format(0, '.', ',') }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% 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>
|
|
||||||
{% 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>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Collection/Series 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>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- User Reviews & Ratings -->
|
{% if movie.writer %}
|
||||||
{% if movie.user_reviews or movie.critic_reviews %}
|
<div class="flex items-start">
|
||||||
<div class="mb-4">
|
<div class="bg-green-100 rounded-full p-3 mr-4">
|
||||||
<h3 class="h5 fw-semibold text-dark mb-3">Reviews & Ratings</h3>
|
<svg class="text-green-600" width="24" height="24" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<div class="row g-4">
|
<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"/>
|
||||||
{% if movie.user_reviews %}
|
</svg>
|
||||||
<div class="col-md-6">
|
</div>
|
||||||
<h4 class="h6 fw-semibold text-dark mb-2">User Reviews</h4>
|
<div>
|
||||||
<div class="vstack gap-3">
|
<h3 class="font-semibold text-gray-900">Writer</h3>
|
||||||
{% for review in movie.user_reviews[:3] %}
|
<p class="text-gray-600">{{ movie.writer }}</p>
|
||||||
<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"/>
|
|
||||||
</svg>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
<span class="ms-2 small text-muted">{{ review.author }}</span>
|
|
||||||
</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>
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Metadata (for debugging/advanced users) -->
|
|
||||||
{% if metadata %}
|
|
||||||
<div class="mt-4 pt-4 border-top">
|
|
||||||
<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">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
|
||||||
</svg>
|
|
||||||
Technical Details & Metadata
|
|
||||||
</summary>
|
|
||||||
<div class="mt-3 small">
|
|
||||||
<pre class="bg-light p-3 rounded"><code>{{ metadata|json_encode(constant('JSON_PRETTY_PRINT')) }}</code></pre>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 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-3 gap-4">
|
||||||
|
{% for actor in actors %}
|
||||||
|
<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>
|
</div>
|
||||||
|
|
||||||
|
<!-- Details Grid -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
<!-- Genres -->
|
||||||
|
{% if movie.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 movie.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 movie.production_companies or movie.production_countries %}
|
||||||
|
<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 %}
|
||||||
|
<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>
|
||||||
|
<p class="text-sm text-gray-600 mb-1">Countries</p>
|
||||||
|
<p class="font-medium">{{ movie.production_countries }}</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Technical Details -->
|
||||||
|
{% if movie.budget or movie.revenue or movie.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 movie.budget %}
|
||||||
|
<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-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>
|
||||||
|
<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 Info -->
|
||||||
|
{% if movie.belongs_to_collection %}
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold text-gray-900">{{ movie.belongs_to_collection }}</h3>
|
||||||
|
<p class="text-gray-600">Part of a collection</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Technical Details & Metadata -->
|
||||||
|
{% if metadata %}
|
||||||
|
<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 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>
|
||||||
|
<span class="font-medium">Full Raw Metadata</span>
|
||||||
|
</summary>
|
||||||
|
<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>
|
</div>
|
||||||
|
|||||||
@@ -1,139 +1,194 @@
|
|||||||
{% extends "layouts/app.twig" %}
|
{% extends "layouts/app.twig" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block nav_controls %}
|
||||||
<div class="container-fluid">
|
<!-- Search form -->
|
||||||
<div class="row">
|
<form method="GET" class="flex gap-2">
|
||||||
<!-- Sidebar with filters -->
|
<input type="hidden" name="view" value="{{ view_mode }}">
|
||||||
<div class="col-lg-3 col-xl-2">
|
<input type="hidden" name="per_page" value="{{ pagination.per_page }}">
|
||||||
<div class="card">
|
{% for genre in filters.genres %}
|
||||||
<div class="card-header">
|
<input type="hidden" name="genres[]" value="{{ genre }}">
|
||||||
<h5 class="mb-0">Filters</h5>
|
{% endfor %}
|
||||||
</div>
|
{% for year in filters.years %}
|
||||||
<div class="card-body">
|
<input type="hidden" name="years[]" value="{{ year }}">
|
||||||
<!-- Filter form -->
|
{% endfor %}
|
||||||
<form method="GET" id="filterForm">
|
<div class="relative">
|
||||||
<input type="hidden" name="view" value="{{ view_mode }}">
|
<input
|
||||||
<input type="hidden" name="per_page" value="{{ pagination.per_page }}">
|
type="text"
|
||||||
<input type="hidden" name="search" value="{{ search }}">
|
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>
|
||||||
|
<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>
|
||||||
|
|
||||||
<!-- Genre filter -->
|
<!-- View mode switcher -->
|
||||||
{% if available_filters.genres %}
|
<div class="flex gap-1" role="group">
|
||||||
<div class="mb-4">
|
{% for mode in view_modes %}
|
||||||
<h6 class="fw-bold text-dark mb-2">Genres</h6>
|
<a
|
||||||
<select class="form-select select2" name="genres[]" multiple data-placeholder="Select genres...">
|
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 %}"
|
||||||
{% for genre in available_filters.genres %}
|
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' : '' }}"
|
||||||
<option value="{{ genre }}" {{ genre in filters.genres ? 'selected' : '' }}>
|
title="{{ mode|title }} View"
|
||||||
{{ genre }}
|
>
|
||||||
</option>
|
{% if mode == 'grid' %}
|
||||||
{% endfor %}
|
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
</select>
|
<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"/>
|
||||||
</div>
|
</svg>
|
||||||
{% endif %}
|
{% 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 %}
|
||||||
|
|
||||||
<!-- Year filter -->
|
{% block sidebar %}
|
||||||
{% if available_filters.years %}
|
<div class="space-y-4">
|
||||||
<div class="mb-4">
|
<!-- Filters -->
|
||||||
<h6 class="fw-bold text-dark mb-2">Years</h6>
|
<div class="bg-gray-50 rounded-lg p-4">
|
||||||
<select class="form-select select2" name="years[]" multiple data-placeholder="Select years...">
|
<h3 class="text-sm font-medium text-gray-900 mb-4">Filters</h3>
|
||||||
{% for year in available_filters.years %}
|
|
||||||
<option value="{{ year }}" {{ year in filters.years ? 'selected' : '' }}>
|
|
||||||
{{ year }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Filter actions -->
|
<!-- Filter form -->
|
||||||
<div class="d-grid gap-2">
|
<form method="GET" id="filterForm">
|
||||||
<button type="submit" class="btn btn-primary btn-sm">
|
<input type="hidden" name="view" value="{{ view_mode }}">
|
||||||
Apply Filters
|
<input type="hidden" name="per_page" value="{{ pagination.per_page }}">
|
||||||
</button>
|
<input type="hidden" name="search" value="{{ search }}">
|
||||||
<a href="{{ path_for('tvshows.index') }}" class="btn btn-outline-secondary btn-sm">
|
|
||||||
Clear All
|
<!-- Genre filter -->
|
||||||
</a>
|
{% if available_filters.genres %}
|
||||||
</div>
|
<div class="mb-4">
|
||||||
</form>
|
<label class="block text-xs font-medium text-gray-700 mb-1">Genres</label>
|
||||||
</div>
|
<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 }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Year filter -->
|
||||||
|
{% if available_filters.years %}
|
||||||
|
<div class="mb-4">
|
||||||
|
<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 }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Filter actions -->
|
||||||
|
<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="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>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<!-- Main content area -->
|
<!-- Quick Stats -->
|
||||||
<div class="col-lg-9 col-xl-10">
|
<div class="bg-gray-50 rounded-lg p-4">
|
||||||
<div class="px-4 py-3">
|
<h3 class="text-sm font-medium text-gray-900 mb-3">Quick Stats</h3>
|
||||||
<!-- Header with search and view controls -->
|
<div class="space-y-2">
|
||||||
<div class="d-flex flex-column flex-sm-row justify-content-between align-items-start align-items-sm-center mb-4 gap-3">
|
<div class="flex justify-between text-sm">
|
||||||
<div>
|
<span class="text-gray-600">Total TV Shows</span>
|
||||||
<h1 class="display-4 fw-bold text-dark">TV Shows</h1>
|
<span class="font-medium text-gray-900">{{ pagination.total_items }}</span>
|
||||||
{% if pagination.total_items > 0 %}
|
</div>
|
||||||
<div class="text-muted small mt-1">
|
<div class="flex justify-between text-sm">
|
||||||
{{ pagination.total_items }} TV shows
|
<span class="text-gray-600">This Page</span>
|
||||||
{% if search %}
|
<span class="font-medium text-gray-900">{{ tvshows|length }}</span>
|
||||||
matching "{{ search }}"
|
</div>
|
||||||
{% endif %}
|
{% if pagination.total_pages > 1 %}
|
||||||
{% if filters.genres or filters.years %}
|
<div class="flex justify-between text-sm">
|
||||||
{% if filters.genres %}
|
<span class="text-gray-600">Page</span>
|
||||||
<span class="badge bg-primary ms-1">{{ filters.genres|join(', ') }}</span>
|
<span class="font-medium text-gray-900">{{ pagination.current_page }} of {{ pagination.total_pages }}</span>
|
||||||
{% endif %}
|
</div>
|
||||||
{% if filters.years %}
|
{% endif %}
|
||||||
<span class="badge bg-secondary ms-1">{{ filters.years|join(', ') }}</span>
|
</div>
|
||||||
{% endif %}
|
</div>
|
||||||
{% endif %}
|
</div>
|
||||||
</div>
|
{% endblock %}
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-flex flex-column flex-sm-row gap-3 w-100 w-sm-auto">
|
{% block content %}
|
||||||
<!-- Search form -->
|
<!-- Main content area -->
|
||||||
<form method="GET" class="d-flex gap-2">
|
<div class="p-6">
|
||||||
<input type="hidden" name="view" value="{{ view_mode }}">
|
<!-- Header -->
|
||||||
<input type="hidden" name="per_page" value="{{ pagination.per_page }}">
|
<div class="mb-6">
|
||||||
{% for genre in filters.genres %}
|
<h1 class="text-3xl font-bold text-gray-900">TV Shows</h1>
|
||||||
<input type="hidden" name="genres[]" value="{{ genre }}">
|
{% if pagination.total_items > 0 %}
|
||||||
{% endfor %}
|
<div class="text-gray-500 text-sm mt-1">
|
||||||
{% for year in filters.years %}
|
{{ pagination.total_items }} TV shows
|
||||||
<input type="hidden" name="years[]" value="{{ year }}">
|
{% if search %}
|
||||||
{% endfor %}
|
matching "{{ search }}"
|
||||||
<div class="position-relative">
|
{% endif %}
|
||||||
<input
|
{% if filters.genres or filters.years %}
|
||||||
type="text"
|
{% if filters.genres %}
|
||||||
name="search"
|
<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>
|
||||||
value="{{ search }}"
|
{% endif %}
|
||||||
placeholder="Search TV shows..."
|
{% if filters.years %}
|
||||||
class="form-control ps-5"
|
<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 %}
|
||||||
<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">
|
{% endif %}
|
||||||
<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"/>
|
</div>
|
||||||
</svg>
|
{% endif %}
|
||||||
</div>
|
</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 %}
|
{% if tvshows is empty %}
|
||||||
<div class="text-center py-5">
|
<div class="text-center py-5">
|
||||||
@@ -164,28 +219,28 @@
|
|||||||
<!-- TV Shows content based on view mode -->
|
<!-- TV Shows content based on view mode -->
|
||||||
{% if view_mode == 'list' %}
|
{% if view_mode == 'list' %}
|
||||||
<!-- List view -->
|
<!-- List view -->
|
||||||
<div class="card">
|
<div class="bg-white rounded-lg shadow-md border border-gray-200">
|
||||||
<ul class="list-group list-group-flush">
|
<ul class="divide-y divide-gray-200">
|
||||||
{% for tvshow in tvshows %}
|
{% for tvshow in tvshows %}
|
||||||
<li class="list-group-item">
|
<li class="px-4 py-3">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="flex justify-between items-center">
|
||||||
<div class="d-flex align-items-center">
|
<div class="flex items-center">
|
||||||
{% if tvshow.poster_url %}
|
{% 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 %}
|
{% else %}
|
||||||
<div class="bg-light rounded me-3 d-flex align-items-center justify-content-center" style="width: 64px; height: 96px;">
|
<div class="bg-gray-100 rounded mr-3 flex items-center justify-center" style="width: 64px; height: 96px;">
|
||||||
<svg class="text-muted" width="32" height="32" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<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"/>
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="flex-grow-1">
|
<div class="flex-1">
|
||||||
<h3 class="h6 mb-1">
|
<h3 class="text-sm font-semibold mb-1">
|
||||||
<a href="{{ path_for('tvshows.show', {'id': tvshow.id}) }}" class="text-decoration-none">
|
<a href="{{ path_for('tvshows.show', {'id': tvshow.id}) }}" class="no-underline text-gray-900 hover:text-blue-600">
|
||||||
{{ tvshow.title }}
|
{{ tvshow.title }}
|
||||||
</a>
|
</a>
|
||||||
</h3>
|
</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 %}
|
{% if tvshow.first_air_date %}
|
||||||
<span>{{ tvshow.first_air_date|date('Y') }}</span>
|
<span>{{ tvshow.first_air_date|date('Y') }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -202,9 +257,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2">
|
<div class="flex gap-2">
|
||||||
{% if tvshow.is_favorite %}
|
{% 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
|
Favorite
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -217,31 +272,29 @@
|
|||||||
|
|
||||||
{% elseif view_mode == 'covers' %}
|
{% elseif view_mode == 'covers' %}
|
||||||
<!-- Cover grid view -->
|
<!-- 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 %}
|
{% for tvshow in tvshows %}
|
||||||
<div class="col-6 col-sm-4 col-md-3 col-lg-2">
|
<div class="bg-white rounded-lg shadow-md border border-gray-200 overflow-hidden h-full">
|
||||||
<div class="card h-100">
|
{% if tvshow.poster_url %}
|
||||||
{% if tvshow.poster_url %}
|
<div class="relative aspect-[2/3] overflow-hidden">
|
||||||
<div class="position-relative" style="aspect-ratio: 2/3; overflow: hidden;">
|
<img src="/images/{{ tvshow.poster_url }}" alt="{{ tvshow.title }}" class="w-full h-full object-cover">
|
||||||
<img src="/images/{{ tvshow.poster_url }}" alt="{{ tvshow.title }}" class="card-img-top" style="width: 100%; height: 100%; object-fit: cover;">
|
</div>
|
||||||
</div>
|
{% else %}
|
||||||
{% else %}
|
<div class="flex items-center justify-center bg-gray-100 aspect-[2/3] min-h-[200px]">
|
||||||
<div class="d-flex align-items-center justify-content-center bg-light" style="aspect-ratio: 2/3; min-height: 200px;">
|
<svg class="text-gray-600" width="48" height="48" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<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="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 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>
|
||||||
</svg>
|
</div>
|
||||||
</div>
|
{% endif %}
|
||||||
|
<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="text-xs text-gray-600 mt-1">{{ tvshow.first_air_date|date('Y') }}</p>
|
||||||
{% endif %}
|
{% 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">
|
|
||||||
{{ tvshow.title }}
|
|
||||||
</a>
|
|
||||||
</h6>
|
|
||||||
{% if tvshow.first_air_date %}
|
|
||||||
<p class="card-text small text-muted">{{ tvshow.first_air_date|date('Y') }}</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -249,62 +302,60 @@
|
|||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<!-- Default grid view -->
|
<!-- 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 %}
|
{% for tvshow in tvshows %}
|
||||||
<div class="col-12 col-md-6 col-lg-3 col-xl-2">
|
<div class="bg-white rounded-lg shadow-md border border-gray-200 h-full">
|
||||||
<div class="card h-100">
|
<div class="p-4">
|
||||||
<div class="card-body">
|
<div class="flex items-center">
|
||||||
<div class="d-flex align-items-center">
|
<div class="flex-shrink-0">
|
||||||
<div class="flex-shrink-0">
|
{% if tvshow.poster_url %}
|
||||||
{% if tvshow.poster_url %}
|
<img class="rounded" style="width: 64px; height: 96px; object-fit: cover;" src="/images/{{ tvshow.poster_url }}" alt="{{ tvshow.title }}">
|
||||||
<img class="rounded" style="width: 64px; height: 96px; object-fit: cover;" src="/images/{{ tvshow.poster_url }}" alt="{{ tvshow.title }}">
|
{% else %}
|
||||||
{% else %}
|
<div class="bg-gray-100 rounded flex items-center justify-center" style="width: 64px; height: 96px;">
|
||||||
<div class="bg-light rounded d-flex align-items-center justify-content-center" style="width: 64px; height: 96px;">
|
<svg class="text-gray-600" width="32" height="32" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<svg class="text-muted" 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"/>
|
||||||
<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>
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</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">
|
|
||||||
{{ tvshow.title }}
|
|
||||||
</a>
|
|
||||||
</h5>
|
|
||||||
<div class="d-flex align-items-center gap-2 small text-muted mb-2">
|
|
||||||
{% if tvshow.first_air_date %}
|
|
||||||
<span>{{ tvshow.first_air_date|date('Y') }}</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if tvshow.rating %}
|
|
||||||
<span>⭐ {{ tvshow.rating }}/10</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% if tvshow.source_name %}
|
|
||||||
<p class="card-text small text-muted mb-2">
|
|
||||||
{{ tvshow.source_name }}
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</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;">
|
|
||||||
{{ 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">
|
|
||||||
{% if tvshow.number_of_seasons %}
|
|
||||||
<span>{{ tvshow.number_of_seasons }} season{{ tvshow.number_of_seasons > 1 ? 's' : '' }}</span>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="d-flex gap-1">
|
</div>
|
||||||
{% if tvshow.is_favorite %}
|
<div class="ml-3 flex-1">
|
||||||
<span class="badge bg-danger">
|
<h5 class="text-lg font-semibold mb-1">
|
||||||
Favorite
|
<a href="{{ path_for('tvshows.show', {'id': tvshow.id}) }}" class="no-underline text-gray-900 hover:text-blue-600">
|
||||||
</span>
|
{{ tvshow.title }}
|
||||||
|
</a>
|
||||||
|
</h5>
|
||||||
|
<div class="flex items-center gap-2 text-sm text-gray-600 mb-2">
|
||||||
|
{% if tvshow.first_air_date %}
|
||||||
|
<span>{{ tvshow.first_air_date|date('Y') }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if tvshow.rating %}
|
||||||
|
<span>⭐ {{ tvshow.rating }}/10</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% if tvshow.source_name %}
|
||||||
|
<p class="text-sm text-gray-600 mb-2">
|
||||||
|
{{ tvshow.source_name }}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if tvshow.overview %}
|
||||||
|
<div class="mt-3">
|
||||||
|
<p class="text-sm text-gray-600" style="display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;">
|
||||||
|
{{ tvshow.overview|slice(0, 150) }}{% if tvshow.overview|length > 150 %}...{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="mt-3 flex justify-between items-center text-sm text-gray-600">
|
||||||
|
{% if tvshow.number_of_seasons %}
|
||||||
|
<span>{{ tvshow.number_of_seasons }} season{{ tvshow.number_of_seasons > 1 ? 's' : '' }}</span>
|
||||||
|
{% endif %}
|
||||||
|
<div class="flex gap-1">
|
||||||
|
{% if tvshow.is_favorite %}
|
||||||
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
|
||||||
|
Favorite
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -313,42 +364,89 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Top Pagination & Controls -->
|
||||||
{% if pagination.total_pages > 1 %}
|
{% if pagination.total_pages > 1 %}
|
||||||
<div class="d-flex align-items-center justify-content-between mt-4">
|
<div class="flex items-center justify-between mb-6 p-4 bg-gray-50 rounded-lg">
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="flex items-center gap-4">
|
||||||
<label for="per_page" class="form-label mb-0">Show:</label>
|
<div class="text-sm text-gray-700">
|
||||||
<select id="per_page" class="form-select form-select-sm w-auto">
|
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
|
||||||
<option value="12" {{ pagination.per_page == 12 ? 'selected' : '' }}>12</option>
|
</div>
|
||||||
<option value="24" {{ pagination.per_page == 24 ? 'selected' : '' }}>24</option>
|
<div class="flex items-center gap-2">
|
||||||
<option value="48" {{ pagination.per_page == 48 ? 'selected' : '' }}>48</option>
|
<label for="per_page_top" class="text-sm font-medium text-gray-700">Show:</label>
|
||||||
<option value="100" {{ pagination.per_page == 100 ? 'selected' : '' }}>100</option>
|
<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">
|
||||||
</select>
|
<option value="12" {{ pagination.per_page == 12 ? 'selected' : '' }}>12</option>
|
||||||
<span class="text-muted small">per page</span>
|
<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-sm text-gray-600">per page</span>
|
||||||
|
</div>
|
||||||
</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 %}
|
{% 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 %}"
|
<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
|
Previous
|
||||||
</a>
|
</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 %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="btn-group" role="group">
|
<!-- Page Numbers -->
|
||||||
{% for page_num in range(max(1, pagination.current_page - 2), min(pagination.total_pages, pagination.current_page + 2)) %}
|
{% set start_page = max(1, 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 %}"
|
{% set end_page = min(pagination.total_pages, pagination.current_page + 2) %}
|
||||||
class="btn btn-sm {{ page_num == pagination.current_page ? 'btn-primary' : 'btn-outline-secondary' }}">
|
|
||||||
{{ page_num }}
|
|
||||||
</a>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
{% 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 %}
|
{% 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 %}"
|
<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
|
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>
|
</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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -408,6 +506,13 @@ document.getElementById('per_page')?.addEventListener('change', function() {
|
|||||||
url.searchParams.set('page', '1'); // Reset to first page
|
url.searchParams.set('page', '1'); // Reset to first page
|
||||||
window.location = url.toString();
|
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>
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,383 +1,533 @@
|
|||||||
{% extends "layouts/app.twig" %}
|
{% extends "layouts/app.twig" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid px-4 py-3">
|
<!-- Hero Section with Backdrop -->
|
||||||
<!-- Back button -->
|
<div class="relative">
|
||||||
<div class="mb-4">
|
{% if tvshow.backdrop_url %}
|
||||||
<a href="{{ path_for('tvshows.index') }}" class="btn btn-link text-decoration-none p-0">
|
<div class="h-96 md:h-[500px] relative overflow-hidden">
|
||||||
<svg class="me-2" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<img src="/images/{{ tvshow.backdrop_url }}" alt="{{ tvshow.title }} backdrop" class="w-full h-full object-cover">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
<div class="absolute inset-0 bg-gradient-to-t from-black via-black/50 to-transparent"></div>
|
||||||
</svg>
|
<div class="absolute inset-0 bg-gradient-to-r from-black/80 via-black/40 to-transparent"></div>
|
||||||
Back to TV Shows
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card shadow-sm">
|
<!-- Back button -->
|
||||||
<div class="row g-0">
|
<div class="absolute top-4 left-4 z-10">
|
||||||
<!-- TV Show poster -->
|
<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">
|
||||||
<div class="col-md-4">
|
<svg class="mr-2" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<div class="card-body p-4">
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
||||||
<div class="ratio ratio-2x3">
|
</svg>
|
||||||
|
Back to TV Shows
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 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="text-xl md:text-2xl text-gray-200 mb-4 drop-shadow-md">{{ tvshow.tagline }}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Quick Info -->
|
||||||
|
<div class="flex flex-wrap gap-4 text-sm text-white mb-4">
|
||||||
|
{% if tvshow.first_air_date %}
|
||||||
|
<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') }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if tvshow.last_air_date %}
|
||||||
|
<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') }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if tvshow.vote_average %}
|
||||||
|
<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
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if tvshow.number_of_seasons and tvshow.number_of_episodes %}
|
||||||
|
<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="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 }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status badges -->
|
||||||
|
<div class="flex flex-wrap gap-2 mb-4">
|
||||||
|
{% if tvshow.status == 'Ended' %}
|
||||||
|
<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="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
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if tvshow.is_favorite %}
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- 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('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 %}
|
{% if tvshow.poster_url %}
|
||||||
<img src="/images/{{ tvshow.poster_url }}" alt="{{ tvshow.title }}" class="img-fluid rounded">
|
<img src="/images/{{ tvshow.poster_url }}" alt="{{ tvshow.title }}" class="w-full h-full object-cover">
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="w-100 h-100 d-flex align-items-center justify-content-center bg-light rounded">
|
<div class="w-full h-full flex items-center justify-center bg-gradient-to-br from-gray-100 to-gray-200">
|
||||||
<svg class="text-muted" width="96" height="96" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<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"/>
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</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>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- TV Show details -->
|
<!-- Quick Stats -->
|
||||||
<div class="col-md-8">
|
<div class="bg-white rounded-xl shadow-lg p-6">
|
||||||
<div class="card-body p-4">
|
<h3 class="text-lg font-semibold text-gray-900 mb-4">Quick Stats</h3>
|
||||||
<div class="mb-4">
|
<div class="space-y-3">
|
||||||
<h1 class="display-4 fw-bold text-dark mb-2">{{ tvshow.title }}</h1>
|
{% if tvshow.vote_count %}
|
||||||
{% if tvshow.tagline %}
|
<div class="flex justify-between items-center">
|
||||||
<p class="lead text-muted mb-3">{{ tvshow.tagline }}</p>
|
<span class="text-gray-600">Votes</span>
|
||||||
{% endif %}
|
<span class="font-medium">{{ tvshow.vote_count|number_format }}</span>
|
||||||
|
|
||||||
<!-- TV Show metadata -->
|
|
||||||
<div class="d-flex flex-wrap gap-3 small text-muted mb-3">
|
|
||||||
{% 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">
|
|
||||||
<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') }}
|
|
||||||
</span>
|
|
||||||
{% 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">
|
|
||||||
<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') }}
|
|
||||||
</span>
|
|
||||||
{% 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">
|
|
||||||
<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
|
|
||||||
</span>
|
|
||||||
{% 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">
|
|
||||||
<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">
|
|
||||||
<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 }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Status badges -->
|
|
||||||
<div class="d-flex flex-wrap gap-2">
|
|
||||||
{% 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">
|
|
||||||
<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">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
|
||||||
</svg>
|
|
||||||
Ongoing
|
|
||||||
</span>
|
|
||||||
{% 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">
|
|
||||||
<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"/>
|
|
||||||
</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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
{% if tvshow.number_of_seasons %}
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
<!-- TV Show Details Grid -->
|
<span class="text-gray-600">Seasons</span>
|
||||||
<div class="row g-4 mb-4">
|
<span class="font-medium">{{ tvshow.number_of_seasons }}</span>
|
||||||
<!-- 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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if tvshow.number_of_episodes %}
|
||||||
<!-- Networks & Production -->
|
<div class="flex justify-between items-center">
|
||||||
{% if tvshow.networks or tvshow.production_companies %}
|
<span class="text-gray-600">Episodes</span>
|
||||||
<div class="col-md-4">
|
<span class="font-medium">{{ tvshow.number_of_episodes }}</span>
|
||||||
<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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
<!-- Episode Details -->
|
<span class="text-gray-600">Source</span>
|
||||||
{% if tvshow.episode_run_time or tvshow.origin_country %}
|
<span class="font-medium">{{ tvshow.source_name }}</span>
|
||||||
<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>
|
|
||||||
</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;">
|
|
||||||
{% 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">
|
|
||||||
<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>
|
|
||||||
{% endif %}
|
|
||||||
{% if episode.is_watched %}
|
|
||||||
<span class="badge bg-success">Watched</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<h6 class="mb-1">{{ episode.title }}</h6>
|
|
||||||
{% if episode.overview %}
|
|
||||||
<p class="small text-muted mb-0">{{ episode.overview|slice(0, 100) }}{% if episode.overview|length > 100 %}...{% endif %}</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<p class="text-muted">No episodes available.</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- 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">
|
|
||||||
{% 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">
|
|
||||||
{% if episode.still_url %}
|
|
||||||
<img src="/images/{{ episode.still_url }}" alt="{{ episode.title }}" class="rounded" style="width: 120px; height: 68px; object-fit: cover;">
|
|
||||||
{% 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">
|
|
||||||
<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>
|
|
||||||
<h6 class="fw-medium mb-1">{{ episode.title }}</h6>
|
|
||||||
{% 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">
|
|
||||||
<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
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Streaming & Availability -->
|
|
||||||
{% 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">
|
|
||||||
{% 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">
|
|
||||||
{% for provider in tvshow.streaming_providers %}
|
|
||||||
<span class="badge bg-success">{{ 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>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Metadata (for debugging/advanced users) -->
|
|
||||||
{% if metadata %}
|
|
||||||
<div class="mt-4 pt-4 border-top">
|
|
||||||
<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">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
|
||||||
</svg>
|
|
||||||
Technical Details & Metadata
|
|
||||||
</summary>
|
|
||||||
<div class="mt-3 small">
|
|
||||||
<pre class="bg-light p-3 rounded"><code>{{ metadata|json_encode(constant('JSON_PRETTY_PRINT')) }}</code></pre>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</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>
|
||||||
|
<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="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-gray-600">No episodes available.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Recent Episodes -->
|
||||||
|
{% if recent_episodes %}
|
||||||
|
<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="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-24 h-14 object-cover rounded">
|
||||||
|
{% else %}
|
||||||
|
<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">
|
||||||
|
<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>
|
||||||
|
<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="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
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Where to Watch -->
|
||||||
|
{% if tvshow.streaming_providers or tvshow.availability %}
|
||||||
|
<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>
|
||||||
|
<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="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>
|
||||||
|
<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 %}
|
||||||
|
|
||||||
|
<!-- Technical Details & Metadata -->
|
||||||
|
{% if metadata %}
|
||||||
|
<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 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>
|
||||||
|
<span class="font-medium">Full Raw Metadata</span>
|
||||||
|
</summary>
|
||||||
|
<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 %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ $app->group('', function (RouteCollectorProxy $group) {
|
|||||||
// Adult Performers (Actors)
|
// Adult Performers (Actors)
|
||||||
$mediaGroup->get('/actors', 'App\Controllers\ActorController:index')->setName('actors.index');
|
$mediaGroup->get('/actors', 'App\Controllers\ActorController:index')->setName('actors.index');
|
||||||
$mediaGroup->get('/actors/{id:\d+}', 'App\Controllers\ActorController:show')->setName('actors.show');
|
$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
13
tailwind.config.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./resources/views/**/*.twig",
|
||||||
|
"./public/**/*.js",
|
||||||
|
"./resources/**/*.js",
|
||||||
|
"./resources/**/*.css",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user