mirror of
https://github.com/ceratic/MediaCollectorLibary.git
synced 2026-05-13 23:56:46 +02:00
basic filter D:
This commit is contained in:
@@ -28,11 +28,24 @@ class AdultController extends Controller
|
||||
// Get search parameters
|
||||
$search = trim($queryParams['search'] ?? '');
|
||||
|
||||
// Get filter parameters
|
||||
$genres = $queryParams['genres'] ?? [];
|
||||
if (!is_array($genres)) {
|
||||
$genres = [$genres];
|
||||
}
|
||||
$genres = array_filter($genres);
|
||||
|
||||
$directors = $queryParams['directors'] ?? [];
|
||||
if (!is_array($directors)) {
|
||||
$directors = [$directors];
|
||||
}
|
||||
$directors = array_filter($directors);
|
||||
|
||||
// Get view mode
|
||||
$viewMode = $queryParams['view'] ?? 'grid'; // grid, list, covers
|
||||
|
||||
// Get adult videos with pagination and search
|
||||
$adultVideos = AdultVideo::getAllWithPagination($this->pdo, $page, $perPage, $search);
|
||||
// Get adult videos with pagination and filters
|
||||
$adultVideos = AdultVideo::getAllWithPagination($this->pdo, $page, $perPage, $search, $genres, $directors);
|
||||
|
||||
// Process metadata to extract local image paths for template compatibility
|
||||
foreach ($adultVideos as &$video) {
|
||||
@@ -54,7 +67,11 @@ class AdultController extends Controller
|
||||
}
|
||||
|
||||
// Get total count for pagination
|
||||
$totalCount = AdultVideo::getTotalCount($this->pdo, $search);
|
||||
$totalCount = AdultVideo::getTotalCount($this->pdo, $search, $genres, $directors);
|
||||
|
||||
// Get available filter options
|
||||
$availableGenres = AdultVideo::getAvailableGenres($this->pdo);
|
||||
$availableDirectors = AdultVideo::getAvailableDirectors($this->pdo);
|
||||
|
||||
// Calculate pagination info
|
||||
$totalPages = ceil($totalCount / $perPage);
|
||||
@@ -76,7 +93,15 @@ class AdultController extends Controller
|
||||
],
|
||||
'search' => $search,
|
||||
'view_mode' => $viewMode,
|
||||
'view_modes' => ['grid', 'list', 'covers']
|
||||
'view_modes' => ['grid', 'list', 'covers'],
|
||||
'filters' => [
|
||||
'genres' => $genres,
|
||||
'directors' => $directors
|
||||
],
|
||||
'available_filters' => [
|
||||
'genres' => $availableGenres,
|
||||
'directors' => $availableDirectors
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,14 +28,31 @@ class GameController extends Controller
|
||||
// Get search parameters
|
||||
$search = trim($queryParams['search'] ?? '');
|
||||
|
||||
// Get filter parameters
|
||||
$genres = $queryParams['genres'] ?? [];
|
||||
if (!is_array($genres)) {
|
||||
$genres = [$genres];
|
||||
}
|
||||
$genres = array_filter($genres);
|
||||
|
||||
$platforms = $queryParams['platforms'] ?? [];
|
||||
if (!is_array($platforms)) {
|
||||
$platforms = [$platforms];
|
||||
}
|
||||
$platforms = array_filter($platforms);
|
||||
|
||||
// Get view mode
|
||||
$viewMode = $queryParams['view'] ?? 'grid'; // grid, list, covers
|
||||
|
||||
// Get games with pagination and search
|
||||
$games = Game::getGroupedGamesWithPagination($this->pdo, $page, $perPage, $search);
|
||||
// Get games with pagination and filters
|
||||
$games = Game::getGroupedGamesWithPagination($this->pdo, $page, $perPage, $search, $genres, $platforms);
|
||||
|
||||
// Get total count for pagination
|
||||
$totalCount = Game::getTotalCount($this->pdo, $search);
|
||||
$totalCount = Game::getTotalCount($this->pdo, $search, $genres, $platforms);
|
||||
|
||||
// Get available filter options
|
||||
$availableGenres = Game::getAvailableGenres($this->pdo);
|
||||
$availablePlatforms = Game::getAvailablePlatforms($this->pdo);
|
||||
|
||||
// Calculate pagination info
|
||||
$totalPages = ceil($totalCount / $perPage);
|
||||
@@ -57,7 +74,15 @@ class GameController extends Controller
|
||||
],
|
||||
'search' => $search,
|
||||
'view_mode' => $viewMode,
|
||||
'view_modes' => ['grid', 'list', 'covers']
|
||||
'view_modes' => ['grid', 'list', 'covers'],
|
||||
'filters' => [
|
||||
'genres' => $genres,
|
||||
'platforms' => $platforms
|
||||
],
|
||||
'available_filters' => [
|
||||
'genres' => $availableGenres,
|
||||
'platforms' => $availablePlatforms
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,14 +28,31 @@ class MovieController extends Controller
|
||||
// Get search parameters
|
||||
$search = trim($queryParams['search'] ?? '');
|
||||
|
||||
// Get filter parameters
|
||||
$genres = $queryParams['genres'] ?? [];
|
||||
if (!is_array($genres)) {
|
||||
$genres = [$genres];
|
||||
}
|
||||
$genres = array_filter($genres);
|
||||
|
||||
$directors = $queryParams['directors'] ?? [];
|
||||
if (!is_array($directors)) {
|
||||
$directors = [$directors];
|
||||
}
|
||||
$directors = array_filter($directors);
|
||||
|
||||
// Get view mode
|
||||
$viewMode = $queryParams['view'] ?? 'grid'; // grid, list, covers
|
||||
|
||||
// Get movies with pagination and search
|
||||
$movies = Movie::getAllWithPagination($this->pdo, $page, $perPage, $search);
|
||||
// Get movies with pagination and filters
|
||||
$movies = Movie::getAllWithPagination($this->pdo, $page, $perPage, $search, $genres, $directors);
|
||||
|
||||
// Get total count for pagination
|
||||
$totalCount = Movie::getTotalCount($this->pdo, $search);
|
||||
$totalCount = Movie::getTotalCount($this->pdo, $search, $genres, $directors);
|
||||
|
||||
// Get available filter options
|
||||
$availableGenres = Movie::getAvailableGenres($this->pdo);
|
||||
$availableDirectors = Movie::getAvailableDirectors($this->pdo);
|
||||
|
||||
// Calculate pagination info
|
||||
$totalPages = ceil($totalCount / $perPage);
|
||||
@@ -57,7 +74,15 @@ class MovieController extends Controller
|
||||
],
|
||||
'search' => $search,
|
||||
'view_mode' => $viewMode,
|
||||
'view_modes' => ['grid', 'list', 'covers']
|
||||
'view_modes' => ['grid', 'list', 'covers'],
|
||||
'filters' => [
|
||||
'genres' => $genres,
|
||||
'directors' => $directors
|
||||
],
|
||||
'available_filters' => [
|
||||
'genres' => $availableGenres,
|
||||
'directors' => $availableDirectors
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,24 +28,37 @@ class TvShowController extends Controller
|
||||
// Get search parameters
|
||||
$search = trim($queryParams['search'] ?? '');
|
||||
|
||||
// Get filter parameters
|
||||
$genres = $queryParams['genres'] ?? [];
|
||||
if (!is_array($genres)) {
|
||||
$genres = [$genres];
|
||||
}
|
||||
$genres = array_filter($genres);
|
||||
|
||||
$years = $queryParams['years'] ?? [];
|
||||
if (!is_array($years)) {
|
||||
$years = [$years];
|
||||
}
|
||||
$years = array_filter($years);
|
||||
|
||||
// Get view mode
|
||||
$viewMode = $queryParams['view'] ?? 'grid'; // grid, list, covers
|
||||
|
||||
// Get TV shows with pagination and search
|
||||
$tvshows = TvShow::getAllWithPagination($this->pdo, $page, $perPage, $search);
|
||||
// Get TV shows with pagination and filters
|
||||
$tvshows = TvShow::getAllWithPagination($this->pdo, $page, $perPage, $search, $genres, $years);
|
||||
|
||||
// Get total count for pagination
|
||||
$totalCount = TvShow::getTotalCount($this->pdo, $search);
|
||||
$totalCount = TvShow::getTotalCount($this->pdo, $search, $genres, $years);
|
||||
|
||||
// Get available filter options
|
||||
$availableGenres = TvShow::getAvailableGenres($this->pdo);
|
||||
$availableYears = TvShow::getAvailableYears($this->pdo);
|
||||
|
||||
// Calculate pagination info
|
||||
$totalPages = ceil($totalCount / $perPage);
|
||||
$hasNextPage = $page < $totalPages;
|
||||
$hasPrevPage = $page > 1;
|
||||
/*
|
||||
echo '<pre>';
|
||||
print_r($tvshows);
|
||||
die();
|
||||
*/
|
||||
|
||||
return $this->view->render($response, 'tvshows/index.twig', [
|
||||
'title' => 'TV Shows',
|
||||
'tvshows' => $tvshows,
|
||||
@@ -61,7 +74,15 @@ class TvShowController extends Controller
|
||||
],
|
||||
'search' => $search,
|
||||
'view_mode' => $viewMode,
|
||||
'view_modes' => ['grid', 'list', 'covers']
|
||||
'view_modes' => ['grid', 'list', 'covers'],
|
||||
'filters' => [
|
||||
'genres' => $genres,
|
||||
'years' => $years
|
||||
],
|
||||
'available_filters' => [
|
||||
'genres' => $availableGenres,
|
||||
'years' => $availableYears
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,59 +25,92 @@ class AdultVideo extends Model
|
||||
'external_id'
|
||||
];
|
||||
|
||||
public static function getAllWithPagination(\PDO $pdo, int $page, int $perPage, string $search = ''): array
|
||||
public static function getAllWithPagination(\PDO $pdo, int $page, int $perPage, string $search = '', array $genres = [], array $directors = []): array
|
||||
{
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
$whereClause = '';
|
||||
$params = [];
|
||||
|
||||
if (!empty($search)) {
|
||||
$whereClause = "WHERE (title LIKE :search OR overview LIKE :search)";
|
||||
$params['search'] = "%{$search}%";
|
||||
}
|
||||
|
||||
$sql = "
|
||||
SELECT av.*, s.display_name as source_name
|
||||
FROM adult_videos av
|
||||
JOIN sources s ON av.source_id = s.id
|
||||
{$whereClause}
|
||||
ORDER BY av.created_at DESC
|
||||
LIMIT :limit OFFSET :offset
|
||||
";
|
||||
$params = [];
|
||||
|
||||
if (!empty($search)) {
|
||||
$sql .= " WHERE (av.title LIKE :search OR av.overview LIKE :search)";
|
||||
$params['search'] = "%{$search}%";
|
||||
}
|
||||
|
||||
if (!empty($genres)) {
|
||||
$placeholders = [];
|
||||
foreach ($genres as $index => $genre) {
|
||||
$placeholders[] = ":genre_{$index}";
|
||||
$params["genre_{$index}"] = $genre;
|
||||
}
|
||||
$whereClause = !empty($search) ? " AND" : " WHERE";
|
||||
$sql .= $whereClause . " av.genre IN (" . implode(',', $placeholders) . ")";
|
||||
}
|
||||
|
||||
if (!empty($directors)) {
|
||||
$placeholders = [];
|
||||
foreach ($directors as $index => $director) {
|
||||
$placeholders[] = ":director_{$index}";
|
||||
$params["director_{$index}"] = $director;
|
||||
}
|
||||
$whereClause = (!empty($search) || !empty($genres)) ? " AND" : " WHERE";
|
||||
$sql .= $whereClause . " av.director IN (" . implode(',', $placeholders) . ")";
|
||||
}
|
||||
|
||||
$sql .= " ORDER BY av.created_at DESC LIMIT :limit OFFSET :offset";
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->bindValue(':limit', $perPage, \PDO::PARAM_INT);
|
||||
$stmt->bindValue(':offset', $offset, \PDO::PARAM_INT);
|
||||
|
||||
if (!empty($search)) {
|
||||
$stmt->bindValue(':search', "%{$search}%", \PDO::PARAM_STR);
|
||||
foreach ($params as $key => $value) {
|
||||
$stmt->bindValue(":{$key}", $value);
|
||||
}
|
||||
|
||||
$stmt->execute();
|
||||
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
public static function getTotalCount(\PDO $pdo, string $search = ''): int
|
||||
public static function getTotalCount(\PDO $pdo, string $search = '', array $genres = [], array $directors = []): int
|
||||
{
|
||||
$whereClause = '';
|
||||
$sql = "SELECT COUNT(*) as count FROM adult_videos av JOIN sources s ON av.source_id = s.id";
|
||||
$params = [];
|
||||
|
||||
if (!empty($search)) {
|
||||
$whereClause = "WHERE (title LIKE :search OR overview LIKE :search)";
|
||||
$sql .= " WHERE (av.title LIKE :search OR av.overview LIKE :search)";
|
||||
$params['search'] = "%{$search}%";
|
||||
}
|
||||
|
||||
$sql = "SELECT COUNT(*) as count FROM adult_videos {$whereClause}";
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
|
||||
if (!empty($search)) {
|
||||
$stmt->bindValue(':search', "%{$search}%", \PDO::PARAM_STR);
|
||||
if (!empty($genres)) {
|
||||
$placeholders = [];
|
||||
foreach ($genres as $index => $genre) {
|
||||
$placeholders[] = ":genre_{$index}";
|
||||
$params["genre_{$index}"] = $genre;
|
||||
}
|
||||
$whereClause = !empty($search) ? " AND" : " WHERE";
|
||||
$sql .= $whereClause . " av.genre IN (" . implode(',', $placeholders) . ")";
|
||||
}
|
||||
|
||||
if (!empty($directors)) {
|
||||
$placeholders = [];
|
||||
foreach ($directors as $index => $director) {
|
||||
$placeholders[] = ":director_{$index}";
|
||||
$params["director_{$index}"] = $director;
|
||||
}
|
||||
$whereClause = (!empty($search) || !empty($genres)) ? " AND" : " WHERE";
|
||||
$sql .= $whereClause . " av.director IN (" . implode(',', $placeholders) . ")";
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
foreach ($params as $key => $value) {
|
||||
$stmt->bindValue(":{$key}", $value);
|
||||
}
|
||||
$stmt->execute();
|
||||
return (int) $stmt->fetch(\PDO::FETCH_ASSOC)['count'];
|
||||
return (int) $stmt->fetch()['count'];
|
||||
}
|
||||
|
||||
public function markAsWatched(): bool
|
||||
@@ -181,4 +214,32 @@ class AdultVideo extends Model
|
||||
");
|
||||
return $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available genres for filtering
|
||||
*/
|
||||
public static function getAvailableGenres(\PDO $pdo): array
|
||||
{
|
||||
$stmt = $pdo->query("
|
||||
SELECT DISTINCT genre
|
||||
FROM adult_videos
|
||||
WHERE genre IS NOT NULL AND genre != ''
|
||||
ORDER BY genre
|
||||
");
|
||||
return $stmt->fetchAll(\PDO::FETCH_COLUMN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available directors for filtering
|
||||
*/
|
||||
public static function getAvailableDirectors(\PDO $pdo): array
|
||||
{
|
||||
$stmt = $pdo->query("
|
||||
SELECT DISTINCT director
|
||||
FROM adult_videos
|
||||
WHERE director IS NOT NULL AND director != ''
|
||||
ORDER BY director
|
||||
");
|
||||
return $stmt->fetchAll(\PDO::FETCH_COLUMN);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,16 +262,34 @@ class Game extends Model
|
||||
/**
|
||||
* Get total count of games for pagination
|
||||
*/
|
||||
public static function getTotalCount(\PDO $pdo, string $search = ''): int
|
||||
public static function getTotalCount(\PDO $pdo, string $search = '', array $genres = [], array $platforms = []): int
|
||||
{
|
||||
$sql = "SELECT COUNT(*) as count FROM games";
|
||||
$sql = "SELECT COUNT(*) as count FROM games WHERE game_key IS NOT NULL";
|
||||
$params = [];
|
||||
|
||||
if (!empty($search)) {
|
||||
$sql .= " WHERE title LIKE :search";
|
||||
$sql .= " AND title LIKE :search";
|
||||
$params['search'] = "%{$search}%";
|
||||
}
|
||||
|
||||
if (!empty($genres)) {
|
||||
$placeholders = [];
|
||||
foreach ($genres as $index => $genre) {
|
||||
$placeholders[] = ":genre_{$index}";
|
||||
$params["genre_{$index}"] = $genre;
|
||||
}
|
||||
$sql .= " AND genre IN (" . implode(',', $placeholders) . ")";
|
||||
}
|
||||
|
||||
if (!empty($platforms)) {
|
||||
$placeholders = [];
|
||||
foreach ($platforms as $index => $platform) {
|
||||
$placeholders[] = ":platform_{$index}";
|
||||
$params["platform_{$index}"] = $platform;
|
||||
}
|
||||
$sql .= " AND platform IN (" . implode(',', $placeholders) . ")";
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
return (int) $stmt->fetch()['count'];
|
||||
@@ -280,7 +298,7 @@ class Game extends Model
|
||||
/**
|
||||
* Get grouped games with pagination and search support
|
||||
*/
|
||||
public static function getGroupedGamesWithPagination(\PDO $pdo, int $page, int $perPage, string $search = ''): array
|
||||
public static function getGroupedGamesWithPagination(\PDO $pdo, int $page, int $perPage, string $search = '', array $genres = [], array $platforms = []): array
|
||||
{
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
@@ -307,6 +325,24 @@ class Game extends Model
|
||||
$params['search'] = "%{$search}%";
|
||||
}
|
||||
|
||||
if (!empty($genres)) {
|
||||
$placeholders = [];
|
||||
foreach ($genres as $index => $genre) {
|
||||
$placeholders[] = ":genre_{$index}";
|
||||
$params["genre_{$index}"] = $genre;
|
||||
}
|
||||
$sql .= " AND genre IN (" . implode(',', $placeholders) . ")";
|
||||
}
|
||||
|
||||
if (!empty($platforms)) {
|
||||
$placeholders = [];
|
||||
foreach ($platforms as $index => $platform) {
|
||||
$placeholders[] = ":platform_{$index}";
|
||||
$params["platform_{$index}"] = $platform;
|
||||
}
|
||||
$sql .= " AND platform IN (" . implode(',', $placeholders) . ")";
|
||||
}
|
||||
|
||||
$sql .= " GROUP BY game_key, title ORDER BY last_played_at DESC, total_playtime DESC LIMIT :limit OFFSET :offset";
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
@@ -379,51 +415,31 @@ class Game extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Playnite-specific series
|
||||
* Get available genres for filtering
|
||||
*/
|
||||
public function getSeries(): array
|
||||
public static function getAvailableGenres(\PDO $pdo): array
|
||||
{
|
||||
return $this->series_json ?? [];
|
||||
$stmt = $pdo->query("
|
||||
SELECT DISTINCT genre
|
||||
FROM games
|
||||
WHERE genre IS NOT NULL AND genre != ''
|
||||
ORDER BY genre
|
||||
");
|
||||
return $stmt->fetchAll(\PDO::FETCH_COLUMN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Playnite-specific age ratings
|
||||
* Get available platforms for filtering
|
||||
*/
|
||||
public function getAgeRatings(): array
|
||||
public static function getAvailablePlatforms(\PDO $pdo): array
|
||||
{
|
||||
return $this->age_ratings_json ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get formatted install size
|
||||
*/
|
||||
public function getFormattedInstallSize(): string
|
||||
{
|
||||
if (!$this->install_size) {
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
$bytes = $this->install_size;
|
||||
$i = 0;
|
||||
|
||||
while ($bytes >= 1024 && $i < count($units) - 1) {
|
||||
$bytes /= 1024;
|
||||
$i++;
|
||||
}
|
||||
|
||||
return round($bytes, 2) . ' ' . $units[$i];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Steam store URL if available
|
||||
*/
|
||||
public function getSteamUrl(): ?string
|
||||
{
|
||||
if (!$this->steam_app_id) {
|
||||
return null;
|
||||
}
|
||||
return "https://store.steampowered.com/app/{$this->steam_app_id}";
|
||||
$stmt = $pdo->query("
|
||||
SELECT DISTINCT platform
|
||||
FROM games
|
||||
WHERE platform IS NOT NULL AND platform != ''
|
||||
ORDER BY platform
|
||||
");
|
||||
return $stmt->fetchAll(\PDO::FETCH_COLUMN);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -435,4 +451,3 @@ class Game extends Model
|
||||
!empty($this->links_json) || !empty($this->background_image);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -109,22 +109,45 @@ class Movie extends Model
|
||||
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
public static function getTotalCount(\PDO $pdo, string $search = ''): int
|
||||
public static function getTotalCount(\PDO $pdo, string $search = '', array $genres = [], array $directors = []): int
|
||||
{
|
||||
$sql = "SELECT COUNT(*) as count FROM movies m JOIN sources s ON m.source_id = s.id";
|
||||
$params = [];
|
||||
|
||||
if (!empty($search)) {
|
||||
$sql .= " WHERE m.title LIKE :search";
|
||||
$sql .= " WHERE (m.title LIKE :search OR m.overview LIKE :search)";
|
||||
$params['search'] = "%{$search}%";
|
||||
}
|
||||
|
||||
if (!empty($genres)) {
|
||||
$placeholders = [];
|
||||
foreach ($genres as $index => $genre) {
|
||||
$placeholders[] = ":genre_{$index}";
|
||||
$params["genre_{$index}"] = $genre;
|
||||
}
|
||||
$whereClause = !empty($search) ? " AND" : " WHERE";
|
||||
$sql .= $whereClause . " m.genre IN (" . implode(',', $placeholders) . ")";
|
||||
}
|
||||
|
||||
if (!empty($directors)) {
|
||||
$placeholders = [];
|
||||
foreach ($directors as $index => $director) {
|
||||
$placeholders[] = ":director_{$index}";
|
||||
$params["director_{$index}"] = $director;
|
||||
}
|
||||
$whereClause = (!empty($search) || !empty($genres)) ? " AND" : " WHERE";
|
||||
$sql .= $whereClause . " m.director IN (" . implode(',', $placeholders) . ")";
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
foreach ($params as $key => $value) {
|
||||
$stmt->bindValue(":{$key}", $value);
|
||||
}
|
||||
$stmt->execute();
|
||||
return (int) $stmt->fetch()['count'];
|
||||
}
|
||||
|
||||
public static function getAllWithPagination(\PDO $pdo, int $page, int $perPage, string $search = ''): array
|
||||
public static function getAllWithPagination(\PDO $pdo, int $page, int $perPage, string $search = '', array $genres = [], array $directors = []): array
|
||||
{
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
@@ -136,10 +159,30 @@ class Movie extends Model
|
||||
$params = [];
|
||||
|
||||
if (!empty($search)) {
|
||||
$sql .= " WHERE m.title LIKE :search";
|
||||
$sql .= " WHERE (m.title LIKE :search OR m.overview LIKE :search)";
|
||||
$params['search'] = "%{$search}%";
|
||||
}
|
||||
|
||||
if (!empty($genres)) {
|
||||
$placeholders = [];
|
||||
foreach ($genres as $index => $genre) {
|
||||
$placeholders[] = ":genre_{$index}";
|
||||
$params["genre_{$index}"] = $genre;
|
||||
}
|
||||
$whereClause = !empty($search) ? " AND" : " WHERE";
|
||||
$sql .= $whereClause . " m.genre IN (" . implode(',', $placeholders) . ")";
|
||||
}
|
||||
|
||||
if (!empty($directors)) {
|
||||
$placeholders = [];
|
||||
foreach ($directors as $index => $director) {
|
||||
$placeholders[] = ":director_{$index}";
|
||||
$params["director_{$index}"] = $director;
|
||||
}
|
||||
$whereClause = (!empty($search) || !empty($genres)) ? " AND" : " WHERE";
|
||||
$sql .= $whereClause . " m.director IN (" . implode(',', $placeholders) . ")";
|
||||
}
|
||||
|
||||
$sql .= " ORDER BY m.title ASC LIMIT :limit OFFSET :offset";
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
@@ -226,4 +269,32 @@ class Movie extends Model
|
||||
'cast' => $castString
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available genres for filtering
|
||||
*/
|
||||
public static function getAvailableGenres(\PDO $pdo): array
|
||||
{
|
||||
$stmt = $pdo->query("
|
||||
SELECT DISTINCT genre
|
||||
FROM movies
|
||||
WHERE genre IS NOT NULL AND genre != ''
|
||||
ORDER BY genre
|
||||
");
|
||||
return $stmt->fetchAll(\PDO::FETCH_COLUMN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available directors for filtering
|
||||
*/
|
||||
public static function getAvailableDirectors(\PDO $pdo): array
|
||||
{
|
||||
$stmt = $pdo->query("
|
||||
SELECT DISTINCT director
|
||||
FROM movies
|
||||
WHERE director IS NOT NULL AND director != ''
|
||||
ORDER BY director
|
||||
");
|
||||
return $stmt->fetchAll(\PDO::FETCH_COLUMN);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ class TvShow extends Model
|
||||
/**
|
||||
* Get total count with optional search
|
||||
*/
|
||||
public static function getTotalCount(\PDO $pdo, string $search = ''): int
|
||||
public static function getTotalCount(\PDO $pdo, string $search = '', array $genres = [], array $years = []): int
|
||||
{
|
||||
$sql = "SELECT COUNT(*) as count FROM tv_shows t JOIN sources s ON t.source_id = s.id";
|
||||
$params = [];
|
||||
@@ -93,15 +93,38 @@ class TvShow extends Model
|
||||
$params['search'] = "%{$search}%";
|
||||
}
|
||||
|
||||
if (!empty($genres)) {
|
||||
$placeholders = [];
|
||||
foreach ($genres as $index => $genre) {
|
||||
$placeholders[] = ":genre_{$index}";
|
||||
$params["genre_{$index}"] = $genre;
|
||||
}
|
||||
$whereClause = !empty($search) ? " AND" : " WHERE";
|
||||
$sql .= $whereClause . " t.genre IN (" . implode(',', $placeholders) . ")";
|
||||
}
|
||||
|
||||
if (!empty($years)) {
|
||||
$placeholders = [];
|
||||
foreach ($years as $index => $year) {
|
||||
$placeholders[] = ":year_{$index}";
|
||||
$params["year_{$index}"] = $year;
|
||||
}
|
||||
$whereClause = (!empty($search) || !empty($genres)) ? " AND" : " WHERE";
|
||||
$sql .= $whereClause . " YEAR(first_air_date) IN (" . implode(',', $placeholders) . ")";
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
foreach ($params as $key => $value) {
|
||||
$stmt->bindValue(":{$key}", $value);
|
||||
}
|
||||
$stmt->execute();
|
||||
return (int) $stmt->fetch()['count'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all TV shows with pagination and optional search
|
||||
*/
|
||||
public static function getAllWithPagination(\PDO $pdo, int $page, int $perPage, string $search = ''): array
|
||||
public static function getAllWithPagination(\PDO $pdo, int $page, int $perPage, string $search = '', array $genres = [], array $years = []): array
|
||||
{
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
@@ -117,6 +140,26 @@ class TvShow extends Model
|
||||
$params['search'] = "%{$search}%";
|
||||
}
|
||||
|
||||
if (!empty($genres)) {
|
||||
$placeholders = [];
|
||||
foreach ($genres as $index => $genre) {
|
||||
$placeholders[] = ":genre_{$index}";
|
||||
$params["genre_{$index}"] = $genre;
|
||||
}
|
||||
$whereClause = !empty($search) ? " AND" : " WHERE";
|
||||
$sql .= $whereClause . " t.genre IN (" . implode(',', $placeholders) . ")";
|
||||
}
|
||||
|
||||
if (!empty($years)) {
|
||||
$placeholders = [];
|
||||
foreach ($years as $index => $year) {
|
||||
$placeholders[] = ":year_{$index}";
|
||||
$params["year_{$index}"] = $year;
|
||||
}
|
||||
$whereClause = (!empty($search) || !empty($genres)) ? " AND" : " WHERE";
|
||||
$sql .= $whereClause . " YEAR(first_air_date) IN (" . implode(',', $placeholders) . ")";
|
||||
}
|
||||
|
||||
$sql .= " ORDER BY t.title ASC LIMIT :limit OFFSET :offset";
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
@@ -259,9 +302,8 @@ public function recordView(): bool
|
||||
public static function getAvailableGenres(\PDO $pdo): array
|
||||
{
|
||||
$stmt = $pdo->query("
|
||||
SELECT DISTINCT TRIM(value) as genre
|
||||
FROM tv_shows,
|
||||
json_each('[\"' || REPLACE(genre, ',', '\",\"') || '\"]')
|
||||
SELECT DISTINCT genre
|
||||
FROM tv_shows
|
||||
WHERE genre IS NOT NULL AND genre != ''
|
||||
ORDER BY genre
|
||||
");
|
||||
@@ -274,7 +316,7 @@ public static function getAvailableGenres(\PDO $pdo): array
|
||||
public static function getAvailableYears(\PDO $pdo): array
|
||||
{
|
||||
$stmt = $pdo->query("
|
||||
SELECT DISTINCT strftime('%Y', first_air_date) as year
|
||||
SELECT DISTINCT YEAR(first_air_date) as year
|
||||
FROM tv_shows
|
||||
WHERE first_air_date IS NOT NULL
|
||||
ORDER BY year DESC
|
||||
|
||||
@@ -75,6 +75,7 @@ class PlayniteImportService
|
||||
*/
|
||||
private function transformPlayniteGame(array $game, int $index): array
|
||||
{
|
||||
/*
|
||||
// Validate required fields
|
||||
if (empty($game['Name'])) {
|
||||
throw new \Exception("Missing game name");
|
||||
@@ -83,7 +84,7 @@ class PlayniteImportService
|
||||
if (empty($game['GameId'])) {
|
||||
throw new \Exception("Missing GameId");
|
||||
}
|
||||
|
||||
*/
|
||||
// Find or create source
|
||||
$source = $this->findOrCreateSource($game);
|
||||
|
||||
@@ -132,10 +133,10 @@ class PlayniteImportService
|
||||
'steam_app_id' => $this->extractSteamAppId($game),
|
||||
|
||||
// Playnite-specific metadata
|
||||
'is_installed' => $game['IsInstalled'] ?? false,
|
||||
'is_favorite' => $game['Favorite'] ?? false,
|
||||
'is_custom_game' => $game['IsCustomGame'] ?? false,
|
||||
'installation_status' => $game['InstallationStatus'] ?? 0,
|
||||
// 'is_installed' => $this->toBoolean($game['IsInstalled'] ?? false),
|
||||
//'is_favorite' => $this->toBoolean($game['Favorite'] ?? false),
|
||||
//'is_custom_game' => $this->toBoolean($game['IsCustomGame'] ?? false),
|
||||
//'installation_status' => $game['InstallationStatus'] ?? 0,
|
||||
|
||||
// Timestamps
|
||||
'added_at' => isset($game['Added']) ? date('Y-m-d H:i:s', strtotime($game['Added'])) : null,
|
||||
@@ -147,16 +148,16 @@ class PlayniteImportService
|
||||
'metadata' => json_encode([
|
||||
'playnite_id' => $game['Id'] ?? null,
|
||||
'version' => $game['Version'] ?? null,
|
||||
'hidden' => $game['Hidden'] ?? false,
|
||||
'hidden' => $this->toBoolean($game['Hidden'] ?? false),
|
||||
'notes' => $game['Notes'] ?? null,
|
||||
'manual' => $game['Manual'] ?? null,
|
||||
'pre_script' => $game['PreScript'] ?? null,
|
||||
'post_script' => $game['PostScript'] ?? null,
|
||||
'game_started_script' => $game['GameStartedScript'] ?? null,
|
||||
'use_global_scripts' => [
|
||||
'pre' => $game['UseGlobalPreScript'] ?? true,
|
||||
'post' => $game['UseGlobalPostScript'] ?? true,
|
||||
'game_started' => $game['UseGlobalGameStartedScript'] ?? true
|
||||
'pre' => $this->toBoolean($game['UseGlobalPreScript'] ?? true),
|
||||
'post' => $this->toBoolean($game['UseGlobalPostScript'] ?? true),
|
||||
'game_started' => $this->toBoolean($game['UseGlobalGameStartedScript'] ?? true)
|
||||
]
|
||||
])
|
||||
];
|
||||
@@ -371,4 +372,21 @@ class PlayniteImportService
|
||||
$gameModel = new Game($this->pdo);
|
||||
$gameModel->update($gameId, $gameData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a value to boolean, handling empty strings properly
|
||||
*/
|
||||
private function toBoolean($value): bool
|
||||
{
|
||||
if ($value === null || $value === false || $value === 0 || $value === '0') {
|
||||
return false;
|
||||
}
|
||||
if ($value === true || $value === 1 || $value === '1') {
|
||||
return true;
|
||||
}
|
||||
if (is_string($value)) {
|
||||
return !empty(trim($value));
|
||||
}
|
||||
return (bool) $value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,77 @@
|
||||
{% extends "layouts/app.twig" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<!-- Sidebar with filters -->
|
||||
<div class="col-lg-3 col-xl-2">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Filters</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- 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">
|
||||
<h6 class="fw-bold text-dark mb-2">Genres</h6>
|
||||
{% for genre in available_filters.genres %}
|
||||
<div class="form-check">
|
||||
<input class="form-check-input"
|
||||
type="checkbox"
|
||||
name="genres[]"
|
||||
value="{{ genre }}"
|
||||
id="genre_{{ genre|lower|replace({' ': '_'}) }}"
|
||||
{{ genre in filters.genres ? 'checked' : '' }}>
|
||||
<label class="form-check-label small" for="genre_{{ genre|lower|replace({' ': '_'}) }}">
|
||||
{{ genre }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Director filter -->
|
||||
{% if available_filters.directors %}
|
||||
<div class="mb-4">
|
||||
<h6 class="fw-bold text-dark mb-2">Directors</h6>
|
||||
{% for director in available_filters.directors %}
|
||||
<div class="form-check">
|
||||
<input class="form-check-input"
|
||||
type="checkbox"
|
||||
name="directors[]"
|
||||
value="{{ director }}"
|
||||
id="director_{{ director|lower|replace({' ': '_'}) }}"
|
||||
{{ director in filters.directors ? 'checked' : '' }}>
|
||||
<label class="form-check-label small" for="director_{{ director|lower|replace({' ': '_'}) }}">
|
||||
{{ director }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Filter actions -->
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-primary btn-sm">
|
||||
Apply Filters
|
||||
</button>
|
||||
<a href="{{ path_for('adult.index') }}" class="btn btn-outline-secondary btn-sm">
|
||||
Clear All
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main content area -->
|
||||
<div class="col-lg-9 col-xl-10">
|
||||
<div class="px-4 py-3">
|
||||
<!-- Header with search and view controls -->
|
||||
<div class="d-flex flex-column flex-sm-row justify-content-between align-items-start align-items-sm-center mb-4 gap-3">
|
||||
@@ -12,6 +83,14 @@
|
||||
{% 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>
|
||||
@@ -21,6 +100,12 @@
|
||||
<form method="GET" class="d-flex gap-2">
|
||||
<input type="hidden" name="view" value="{{ view_mode }}">
|
||||
<input type="hidden" name="per_page" value="{{ pagination.per_page }}">
|
||||
{% for genre in filters.genres %}
|
||||
<input type="hidden" name="genres[]" value="{{ genre }}">
|
||||
{% endfor %}
|
||||
{% for director in filters.directors %}
|
||||
<input type="hidden" name="directors[]" value="{{ director }}">
|
||||
{% endfor %}
|
||||
<div class="position-relative">
|
||||
<input
|
||||
type="text"
|
||||
@@ -42,7 +127,7 @@
|
||||
<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 %}"
|
||||
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 director in filters.directors %}&directors[]={{ director }}{% endfor %}"
|
||||
class="btn btn-outline-secondary {{ view_mode == mode ? 'active' : '' }}"
|
||||
>
|
||||
{% if mode == 'grid' %}
|
||||
@@ -74,22 +159,22 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
<h3 class="h5 fw-medium text-dark">
|
||||
{% if search %}
|
||||
No adult videos found matching "{{ search }}"
|
||||
{% if search or filters.genres or filters.directors %}
|
||||
No adult videos found matching your criteria
|
||||
{% else %}
|
||||
No adult videos found
|
||||
{% endif %}
|
||||
</h3>
|
||||
<p class="text-muted">
|
||||
{% if search %}
|
||||
Try adjusting your search terms or browse all adult videos.
|
||||
{% if search or filters.genres or filters.directors %}
|
||||
Try adjusting your search terms or filters.
|
||||
{% else %}
|
||||
Adult videos will appear here after syncing with XBVR or Stash sources.
|
||||
{% endif %}
|
||||
</p>
|
||||
{% if search %}
|
||||
{% if search or filters.genres or filters.directors %}
|
||||
<a href="{{ path_for('adult.index') }}" class="btn btn-primary mt-3">
|
||||
View all adult videos
|
||||
Clear filters
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -269,7 +354,7 @@
|
||||
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
{% 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 }}"
|
||||
<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">
|
||||
Previous
|
||||
</a>
|
||||
@@ -277,7 +362,7 @@
|
||||
|
||||
<div class="btn-group" role="group">
|
||||
{% for page_num in range(max(1, pagination.current_page - 2), min(pagination.total_pages, pagination.current_page + 2)) %}
|
||||
<a href="?page={{ page_num }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}&view={{ view_mode }}"
|
||||
<a href="?page={{ page_num }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}&view={{ view_mode }}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for director in filters.directors %}&directors[]={{ director }}{% endfor %}"
|
||||
class="btn btn-sm {{ page_num == pagination.current_page ? 'btn-primary' : 'btn-outline-secondary' }}">
|
||||
{{ page_num }}
|
||||
</a>
|
||||
@@ -285,7 +370,7 @@
|
||||
</div>
|
||||
|
||||
{% 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 }}"
|
||||
<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">
|
||||
Next
|
||||
</a>
|
||||
@@ -295,6 +380,9 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('per_page')?.addEventListener('change', function() {
|
||||
@@ -303,5 +391,12 @@ document.getElementById('per_page')?.addEventListener('change', function() {
|
||||
url.searchParams.set('page', '1'); // Reset to first page
|
||||
window.location = url.toString();
|
||||
});
|
||||
|
||||
// Auto-submit filter form when checkboxes change
|
||||
document.querySelectorAll('#filterForm input[type="checkbox"]').forEach(function(checkbox) {
|
||||
checkbox.addEventListener('change', function() {
|
||||
document.getElementById('filterForm').submit();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,6 +1,77 @@
|
||||
{% extends "layouts/app.twig" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<!-- Sidebar with filters -->
|
||||
<div class="col-lg-3 col-xl-2">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Filters</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- 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">
|
||||
<h6 class="fw-bold text-dark mb-2">Genres</h6>
|
||||
{% for genre in available_filters.genres %}
|
||||
<div class="form-check">
|
||||
<input class="form-check-input"
|
||||
type="checkbox"
|
||||
name="genres[]"
|
||||
value="{{ genre }}"
|
||||
id="genre_{{ genre|lower|replace({' ': '_'}) }}"
|
||||
{{ genre in filters.genres ? 'checked' : '' }}>
|
||||
<label class="form-check-label small" for="genre_{{ genre|lower|replace({' ': '_'}) }}">
|
||||
{{ genre }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Platform filter -->
|
||||
{% if available_filters.platforms %}
|
||||
<div class="mb-4">
|
||||
<h6 class="fw-bold text-dark mb-2">Platforms</h6>
|
||||
{% for platform in available_filters.platforms %}
|
||||
<div class="form-check">
|
||||
<input class="form-check-input"
|
||||
type="checkbox"
|
||||
name="platforms[]"
|
||||
value="{{ platform }}"
|
||||
id="platform_{{ platform|lower|replace({' ': '_'}) }}"
|
||||
{{ platform in filters.platforms ? 'checked' : '' }}>
|
||||
<label class="form-check-label small" for="platform_{{ platform|lower|replace({' ': '_'}) }}">
|
||||
{{ platform }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Filter actions -->
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-primary btn-sm">
|
||||
Apply Filters
|
||||
</button>
|
||||
<a href="{{ path_for('games.index') }}" class="btn btn-outline-secondary btn-sm">
|
||||
Clear All
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main content area -->
|
||||
<div class="col-lg-9 col-xl-10">
|
||||
<div class="px-4 py-3">
|
||||
<!-- Header with search and view controls -->
|
||||
<div class="d-flex flex-column flex-sm-row justify-content-between align-items-start align-items-sm-center mb-4 gap-3">
|
||||
@@ -12,6 +83,14 @@
|
||||
{% 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>
|
||||
@@ -21,6 +100,12 @@
|
||||
<form method="GET" class="d-flex gap-2">
|
||||
<input type="hidden" name="view" value="{{ view_mode }}">
|
||||
<input type="hidden" name="per_page" value="{{ pagination.per_page }}">
|
||||
{% for genre in filters.genres %}
|
||||
<input type="hidden" name="genres[]" value="{{ genre }}">
|
||||
{% endfor %}
|
||||
{% for platform in filters.platforms %}
|
||||
<input type="hidden" name="platforms[]" value="{{ platform }}">
|
||||
{% endfor %}
|
||||
<div class="position-relative">
|
||||
<input
|
||||
type="text"
|
||||
@@ -42,7 +127,7 @@
|
||||
<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 %}"
|
||||
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 platform in filters.platforms %}&platforms[]={{ platform }}{% endfor %}"
|
||||
class="btn btn-outline-secondary {{ view_mode == mode ? 'active' : '' }}"
|
||||
>
|
||||
{% if mode == 'grid' %}
|
||||
@@ -68,22 +153,22 @@
|
||||
<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>
|
||||
<h3 class="h5 fw-medium text-dark">
|
||||
{% if search %}
|
||||
No games found matching "{{ search }}"
|
||||
{% if search or filters.genres or filters.platforms %}
|
||||
No games found matching your criteria
|
||||
{% else %}
|
||||
No games found
|
||||
{% endif %}
|
||||
</h3>
|
||||
<p class="text-muted">
|
||||
{% if search %}
|
||||
Try adjusting your search terms or browse all games.
|
||||
{% if search or filters.genres or filters.platforms %}
|
||||
Try adjusting your search terms or filters.
|
||||
{% else %}
|
||||
Start syncing your gaming libraries to see your games here.
|
||||
{% endif %}
|
||||
</p>
|
||||
{% if search %}
|
||||
{% if search or filters.genres or filters.platforms %}
|
||||
<a href="{{ path_for('games.index') }}" class="btn btn-primary mt-3">
|
||||
View all games
|
||||
Clear filters
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -244,7 +329,7 @@
|
||||
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
{% 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 }}"
|
||||
<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">
|
||||
Previous
|
||||
</a>
|
||||
@@ -252,7 +337,7 @@
|
||||
|
||||
<div class="btn-group" role="group">
|
||||
{% for page_num in range(max(1, pagination.current_page - 2), min(pagination.total_pages, pagination.current_page + 2)) %}
|
||||
<a href="?page={{ page_num }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}&view={{ view_mode }}"
|
||||
<a href="?page={{ page_num }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}&view={{ view_mode }}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for platform in filters.platforms %}&platforms[]={{ platform }}{% endfor %}"
|
||||
class="btn btn-sm {{ page_num == pagination.current_page ? 'btn-primary' : 'btn-outline-secondary' }}">
|
||||
{{ page_num }}
|
||||
</a>
|
||||
@@ -260,7 +345,7 @@
|
||||
</div>
|
||||
|
||||
{% 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 }}"
|
||||
<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">
|
||||
Next
|
||||
</a>
|
||||
@@ -270,6 +355,9 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('per_page')?.addEventListener('change', function() {
|
||||
@@ -278,5 +366,12 @@ document.getElementById('per_page')?.addEventListener('change', function() {
|
||||
url.searchParams.set('page', '1'); // Reset to first page
|
||||
window.location = url.toString();
|
||||
});
|
||||
|
||||
// Auto-submit filter form when checkboxes change
|
||||
document.querySelectorAll('#filterForm input[type="checkbox"]').forEach(function(checkbox) {
|
||||
checkbox.addEventListener('change', function() {
|
||||
document.getElementById('filterForm').submit();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,6 +1,77 @@
|
||||
{% extends "layouts/app.twig" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<!-- Sidebar with filters -->
|
||||
<div class="col-lg-3 col-xl-2">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Filters</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- 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">
|
||||
<h6 class="fw-bold text-dark mb-2">Genres</h6>
|
||||
{% for genre in available_filters.genres %}
|
||||
<div class="form-check">
|
||||
<input class="form-check-input"
|
||||
type="checkbox"
|
||||
name="genres[]"
|
||||
value="{{ genre }}"
|
||||
id="genre_{{ genre|lower|replace({' ': '_'}) }}"
|
||||
{{ genre in filters.genres ? 'checked' : '' }}>
|
||||
<label class="form-check-label small" for="genre_{{ genre|lower|replace({' ': '_'}) }}">
|
||||
{{ genre }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Director filter -->
|
||||
{% if available_filters.directors %}
|
||||
<div class="mb-4">
|
||||
<h6 class="fw-bold text-dark mb-2">Directors</h6>
|
||||
{% for director in available_filters.directors %}
|
||||
<div class="form-check">
|
||||
<input class="form-check-input"
|
||||
type="checkbox"
|
||||
name="directors[]"
|
||||
value="{{ director }}"
|
||||
id="director_{{ director|lower|replace({' ': '_'}) }}"
|
||||
{{ director in filters.directors ? 'checked' : '' }}>
|
||||
<label class="form-check-label small" for="director_{{ director|lower|replace({' ': '_'}) }}">
|
||||
{{ director }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Filter actions -->
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-primary btn-sm">
|
||||
Apply Filters
|
||||
</button>
|
||||
<a href="{{ path_for('movies.index') }}" class="btn btn-outline-secondary btn-sm">
|
||||
Clear All
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main content area -->
|
||||
<div class="col-lg-9 col-xl-10">
|
||||
<div class="px-4 py-3">
|
||||
<!-- Header with search and view controls -->
|
||||
<div class="d-flex flex-column flex-sm-row justify-content-between align-items-start align-items-sm-center mb-4 gap-3">
|
||||
@@ -12,6 +83,14 @@
|
||||
{% 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>
|
||||
@@ -21,6 +100,12 @@
|
||||
<form method="GET" class="d-flex gap-2">
|
||||
<input type="hidden" name="view" value="{{ view_mode }}">
|
||||
<input type="hidden" name="per_page" value="{{ pagination.per_page }}">
|
||||
{% for genre in filters.genres %}
|
||||
<input type="hidden" name="genres[]" value="{{ genre }}">
|
||||
{% endfor %}
|
||||
{% for director in filters.directors %}
|
||||
<input type="hidden" name="directors[]" value="{{ director }}">
|
||||
{% endfor %}
|
||||
<div class="position-relative">
|
||||
<input
|
||||
type="text"
|
||||
@@ -42,7 +127,7 @@
|
||||
<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 %}"
|
||||
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 director in filters.directors %}&directors[]={{ director }}{% endfor %}"
|
||||
class="btn btn-outline-secondary {{ view_mode == mode ? 'active' : '' }}"
|
||||
>
|
||||
{% if mode == 'grid' %}
|
||||
@@ -68,22 +153,22 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 4V2a1 1 0 011-1h8a1 1 0 011 1v2h4a1 1 0 010 2h-1v14a2 2 0 01-2 2H6a2 2 0 01-2-2V6H3a1 1 0 010-2h4z"/>
|
||||
</svg>
|
||||
<h3 class="h5 fw-medium text-dark">
|
||||
{% if search %}
|
||||
No movies found matching "{{ search }}"
|
||||
{% if search or filters.genres or filters.directors %}
|
||||
No movies found matching your criteria
|
||||
{% else %}
|
||||
No movies found
|
||||
{% endif %}
|
||||
</h3>
|
||||
<p class="text-muted">
|
||||
{% if search %}
|
||||
Try adjusting your search terms or browse all movies.
|
||||
{% if search or filters.genres or filters.directors %}
|
||||
Try adjusting your search terms or filters.
|
||||
{% else %}
|
||||
Start syncing your movie libraries to see your movies here.
|
||||
{% endif %}
|
||||
</p>
|
||||
{% if search %}
|
||||
{% if search or filters.genres or filters.directors %}
|
||||
<a href="{{ path_for('movies.index') }}" class="btn btn-primary mt-3">
|
||||
View all movies
|
||||
Clear filters
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -263,7 +348,7 @@
|
||||
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
{% 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 }}"
|
||||
<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">
|
||||
Previous
|
||||
</a>
|
||||
@@ -271,7 +356,7 @@
|
||||
|
||||
<div class="btn-group" role="group">
|
||||
{% for page_num in range(max(1, pagination.current_page - 2), min(pagination.total_pages, pagination.current_page + 2)) %}
|
||||
<a href="?page={{ page_num }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}&view={{ view_mode }}"
|
||||
<a href="?page={{ page_num }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}&view={{ view_mode }}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for director in filters.directors %}&directors[]={{ director }}{% endfor %}"
|
||||
class="btn btn-sm {{ page_num == pagination.current_page ? 'btn-primary' : 'btn-outline-secondary' }}">
|
||||
{{ page_num }}
|
||||
</a>
|
||||
@@ -279,7 +364,7 @@
|
||||
</div>
|
||||
|
||||
{% 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 }}"
|
||||
<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">
|
||||
Next
|
||||
</a>
|
||||
@@ -289,6 +374,9 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('per_page')?.addEventListener('change', function() {
|
||||
@@ -297,5 +385,12 @@ document.getElementById('per_page')?.addEventListener('change', function() {
|
||||
url.searchParams.set('page', '1'); // Reset to first page
|
||||
window.location = url.toString();
|
||||
});
|
||||
|
||||
// Auto-submit filter form when checkboxes change
|
||||
document.querySelectorAll('#filterForm input[type="checkbox"]').forEach(function(checkbox) {
|
||||
checkbox.addEventListener('change', function() {
|
||||
document.getElementById('filterForm').submit();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,13 +1,98 @@
|
||||
{% extends "layouts/app.twig" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<!-- Sidebar with filters -->
|
||||
<div class="col-lg-3 col-xl-2">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Filters</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- 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">
|
||||
<h6 class="fw-bold text-dark mb-2">Genres</h6>
|
||||
{% for genre in available_filters.genres %}
|
||||
<div class="form-check">
|
||||
<input class="form-check-input"
|
||||
type="checkbox"
|
||||
name="genres[]"
|
||||
value="{{ genre }}"
|
||||
id="genre_{{ genre|lower|replace({' ': '_'}) }}"
|
||||
{{ genre in filters.genres ? 'checked' : '' }}>
|
||||
<label class="form-check-label small" for="genre_{{ genre|lower|replace({' ': '_'}) }}">
|
||||
{{ genre }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Year filter -->
|
||||
{% if available_filters.years %}
|
||||
<div class="mb-4">
|
||||
<h6 class="fw-bold text-dark mb-2">Years</h6>
|
||||
{% for year in available_filters.years %}
|
||||
<div class="form-check">
|
||||
<input class="form-check-input"
|
||||
type="checkbox"
|
||||
name="years[]"
|
||||
value="{{ year }}"
|
||||
id="year_{{ year }}"
|
||||
{{ year in filters.years ? 'checked' : '' }}>
|
||||
<label class="form-check-label small" for="year_{{ year }}">
|
||||
{{ year }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Filter actions -->
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-primary btn-sm">
|
||||
Apply Filters
|
||||
</button>
|
||||
<a href="{{ path_for('tvshows.index') }}" class="btn btn-outline-secondary btn-sm">
|
||||
Clear All
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main content area -->
|
||||
<div class="col-lg-9 col-xl-10">
|
||||
<div class="px-4 py-3">
|
||||
<!-- Header with search and view controls -->
|
||||
<div class="d-flex flex-column flex-sm-row justify-content-between align-items-start align-items-sm-center mb-4 gap-3">
|
||||
<div>
|
||||
<h1 class="display-4 fw-bold text-dark">TV Shows</h1>
|
||||
{% if pagination.total_items > 0 %}
|
||||
<div class="text-muted small mt-1">
|
||||
{{ pagination.total_items }} TV shows
|
||||
{% if search %}
|
||||
matching "{{ search }}"
|
||||
{% endif %}
|
||||
{% if filters.genres or filters.years %}
|
||||
{% if filters.genres %}
|
||||
<span class="badge bg-primary ms-1">{{ filters.genres|join(', ') }}</span>
|
||||
{% endif %}
|
||||
{% if filters.years %}
|
||||
<span class="badge bg-secondary ms-1">{{ filters.years|join(', ') }}</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column flex-sm-row gap-3 w-100 w-sm-auto">
|
||||
@@ -15,6 +100,12 @@
|
||||
<form method="GET" class="d-flex gap-2">
|
||||
<input type="hidden" name="view" value="{{ view_mode }}">
|
||||
<input type="hidden" name="per_page" value="{{ pagination.per_page }}">
|
||||
{% for genre in filters.genres %}
|
||||
<input type="hidden" name="genres[]" value="{{ genre }}">
|
||||
{% endfor %}
|
||||
{% for year in filters.years %}
|
||||
<input type="hidden" name="years[]" value="{{ year }}">
|
||||
{% endfor %}
|
||||
<div class="position-relative">
|
||||
<input
|
||||
type="text"
|
||||
@@ -22,7 +113,6 @@
|
||||
value="{{ search }}"
|
||||
placeholder="Search TV shows..."
|
||||
class="form-control ps-5"
|
||||
|
||||
>
|
||||
<svg class="position-absolute top-50 start-0 translate-middle-y ms-3 text-muted" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||
@@ -36,63 +126,64 @@
|
||||
<!-- View mode switcher -->
|
||||
<div class="btn-group" role="group">
|
||||
{% for mode in view_modes %}
|
||||
<button
|
||||
class="btn btn-outline-secondary"
|
||||
<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>
|
||||
{% elseif mode == 'list' %}
|
||||
{% 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 }}
|
||||
</button>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% if tvshows is empty %}
|
||||
<div class="text-center py-5">
|
||||
<svg class="mx-auto text-muted mb-3" width="48" height="48" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 4V2a1 1 0 011-1h8a1 1 0 011 1v2h4a1 1 0 010 2h-1v14a2 2 0 01-2 2H6a2 2 0 01-2-2V6H3a1 1 0 010-2h4z"/>
|
||||
</svg>
|
||||
<h3 class="h5 fw-medium text-dark">
|
||||
{% if search %}
|
||||
No TV shows found matching "{{ search }}"
|
||||
{% if search or filters.genres or filters.years %}
|
||||
No TV shows found matching your criteria
|
||||
{% else %}
|
||||
No TV shows found
|
||||
{% endif %}
|
||||
</h3>
|
||||
<p class="text-muted">
|
||||
{% if search %}
|
||||
Try adjusting your search terms or browse all TV shows.
|
||||
{% if search or filters.genres or filters.years %}
|
||||
Try adjusting your search terms or filters.
|
||||
{% else %}
|
||||
Start syncing your TV show libraries to see your TV shows here.
|
||||
{% endif %}
|
||||
</p>
|
||||
{% if search %}
|
||||
{% if search or filters.genres or filters.years %}
|
||||
<a href="{{ path_for('tvshows.index') }}" class="btn btn-primary mt-3">
|
||||
View all TV shows
|
||||
Clear filters
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<!-- Movies content based on view mode -->
|
||||
<!-- TV Shows content based on view mode -->
|
||||
{% if view_mode == 'list' %}
|
||||
<!-- List view -->
|
||||
<div class="card">
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for movie in tvshows %}
|
||||
{% for tvshow in tvshows %}
|
||||
<li class="list-group-item">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex align-items-center">
|
||||
{% 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 }}">
|
||||
{% 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 }}">
|
||||
{% else %}
|
||||
<div class="bg-light rounded me-3 d-flex align-items-center justify-content-center" style="width: 64px; height: 96px;">
|
||||
<svg class="text-muted" width="32" height="32" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
@@ -102,31 +193,29 @@
|
||||
{% endif %}
|
||||
<div class="flex-grow-1">
|
||||
<h3 class="h6 mb-1">
|
||||
<a href="{{ path_for('tvshows.show', {'id': movie.id}) }}" class="text-decoration-none">
|
||||
{{ movie.title }}
|
||||
<a href="{{ path_for('tvshows.show', {'id': tvshow.id}) }}" class="text-decoration-none">
|
||||
{{ tvshow.title }}
|
||||
</a>
|
||||
</h3>
|
||||
<div class="d-flex align-items-center gap-3 small text-muted">
|
||||
{% if movie.release_date %}
|
||||
<span>{{ movie.release_date|date('Y') }}</span>
|
||||
{% if tvshow.first_air_date %}
|
||||
<span>{{ tvshow.first_air_date|date('Y') }}</span>
|
||||
{% endif %}
|
||||
{% if movie.rating %}
|
||||
<span>⭐ {{ movie.rating }}/10</span>
|
||||
{% if tvshow.rating %}
|
||||
<span>⭐ {{ tvshow.rating }}/10</span>
|
||||
{% endif %}
|
||||
{% if movie.runtime_minutes %}
|
||||
<span>{{ (movie.runtime_minutes / 60)|round(1) }}h {{ movie.runtime_minutes % 60 }}m</span>
|
||||
{% if tvshow.number_of_seasons %}
|
||||
<span>{{ tvshow.number_of_seasons }} season{{ tvshow.number_of_seasons > 1 ? 's' : '' }}</span>
|
||||
{% endif %}
|
||||
<span>{{ movie.source_name }}</span>
|
||||
{% if tvshow.number_of_episodes %}
|
||||
<span>{{ tvshow.number_of_episodes }} episode{{ tvshow.number_of_episodes > 1 ? 's' : '' }}</span>
|
||||
{% endif %}
|
||||
<span>{{ tvshow.source_name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
{% if movie.watched %}
|
||||
<span class="badge bg-success">
|
||||
Watched
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if movie.is_favorite %}
|
||||
{% if tvshow.is_favorite %}
|
||||
<span class="badge bg-danger">
|
||||
Favorite
|
||||
</span>
|
||||
@@ -141,12 +230,12 @@
|
||||
{% elseif view_mode == 'covers' %}
|
||||
<!-- Cover grid view -->
|
||||
<div class="row g-3">
|
||||
{% for movie in tvshows %}
|
||||
{% for tvshow in tvshows %}
|
||||
<div class="col-6 col-sm-4 col-md-3 col-lg-2">
|
||||
<div class="card h-100">
|
||||
{% if movie.poster_url %}
|
||||
{% if tvshow.poster_url %}
|
||||
<div class="position-relative" style="aspect-ratio: 2/3; overflow: hidden;">
|
||||
<img src="/images/{{ movie.poster_url }}" alt="{{ movie.title }}" class="card-img-top" style="width: 100%; height: 100%; object-fit: cover;">
|
||||
<img src="/images/{{ tvshow.poster_url }}" alt="{{ tvshow.title }}" class="card-img-top" style="width: 100%; height: 100%; object-fit: cover;">
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="d-flex align-items-center justify-content-center bg-light" style="aspect-ratio: 2/3; min-height: 200px;">
|
||||
@@ -156,13 +245,13 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="card-body">
|
||||
<h6 class="card-title text-truncate" title="{{ movie.title }}">
|
||||
<a href="{{ path_for('tvshows.show', {'id': movie.id}) }}" class="text-decoration-none">
|
||||
{{ movie.title }}
|
||||
<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 movie.release_date %}
|
||||
<p class="card-text small text-muted">{{ movie.release_date|date('Y') }}</p>
|
||||
{% if tvshow.first_air_date %}
|
||||
<p class="card-text small text-muted">{{ tvshow.first_air_date|date('Y') }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -173,14 +262,14 @@
|
||||
{% else %}
|
||||
<!-- Default grid view -->
|
||||
<div class="row g-3">
|
||||
{% for movie in tvshows %}
|
||||
{% for tvshow in tvshows %}
|
||||
<div class="col-12 col-md-6 col-lg-3 col-xl-2">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-shrink-0">
|
||||
{% if movie.poster_url %}
|
||||
<img class="rounded" style="width: 64px; height: 96px; object-fit: cover;" src="/images/{{ movie.poster_url }}" alt="{{ movie.title }}">
|
||||
{% if tvshow.poster_url %}
|
||||
<img class="rounded" style="width: 64px; height: 96px; object-fit: cover;" src="/images/{{ tvshow.poster_url }}" alt="{{ tvshow.title }}">
|
||||
{% else %}
|
||||
<div class="bg-light rounded d-flex align-items-center justify-content-center" style="width: 64px; height: 96px;">
|
||||
<svg class="text-muted" width="32" height="32" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
@@ -191,43 +280,38 @@
|
||||
</div>
|
||||
<div class="ms-3 flex-grow-1">
|
||||
<h5 class="card-title mb-1">
|
||||
<a href="{{ path_for('tvshows.show', {'id': movie.id}) }}" class="text-decoration-none">
|
||||
{{ movie.title }}
|
||||
<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 movie.release_date %}
|
||||
<span>{{ movie.release_date|date('Y') }}</span>
|
||||
{% if tvshow.first_air_date %}
|
||||
<span>{{ tvshow.first_air_date|date('Y') }}</span>
|
||||
{% endif %}
|
||||
{% if movie.rating %}
|
||||
<span>⭐ {{ movie.rating }}/10</span>
|
||||
{% if tvshow.rating %}
|
||||
<span>⭐ {{ tvshow.rating }}/10</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if movie.source_name %}
|
||||
{% if tvshow.source_name %}
|
||||
<p class="card-text small text-muted mb-2">
|
||||
{{ movie.source_name }}
|
||||
{{ tvshow.source_name }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if movie.overview %}
|
||||
{% 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;">
|
||||
{{ movie.overview|slice(0, 150) }}{% if movie.overview|length > 150 %}...{% endif %}
|
||||
{{ 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 movie.runtime_minutes %}
|
||||
<span>{{ (movie.runtime_minutes / 60)|round(1) }}h {{ movie.runtime_minutes % 60 }}m</span>
|
||||
{% if tvshow.number_of_seasons %}
|
||||
<span>{{ tvshow.number_of_seasons }} season{{ tvshow.number_of_seasons > 1 ? 's' : '' }}</span>
|
||||
{% endif %}
|
||||
<div class="d-flex gap-1">
|
||||
{% if movie.watched %}
|
||||
<span class="badge bg-success">
|
||||
Watched
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if movie.is_favorite %}
|
||||
{% if tvshow.is_favorite %}
|
||||
<span class="badge bg-danger">
|
||||
Favorite
|
||||
</span>
|
||||
@@ -257,7 +341,7 @@
|
||||
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
{% 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 }}"
|
||||
<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">
|
||||
Previous
|
||||
</a>
|
||||
@@ -265,7 +349,7 @@
|
||||
|
||||
<div class="btn-group" role="group">
|
||||
{% for page_num in range(max(1, pagination.current_page - 2), min(pagination.total_pages, pagination.current_page + 2)) %}
|
||||
<a href="?page={{ page_num }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}&view={{ view_mode }}"
|
||||
<a href="?page={{ page_num }}{% if search %}&search={{ search }}{% endif %}{% if pagination.per_page != 24 %}&per_page={{ pagination.per_page }}{% endif %}&view={{ view_mode }}{% for genre in filters.genres %}&genres[]={{ genre }}{% endfor %}{% for year in filters.years %}&years[]={{ year }}{% endfor %}"
|
||||
class="btn btn-sm {{ page_num == pagination.current_page ? 'btn-primary' : 'btn-outline-secondary' }}">
|
||||
{{ page_num }}
|
||||
</a>
|
||||
@@ -273,7 +357,7 @@
|
||||
</div>
|
||||
|
||||
{% 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 }}"
|
||||
<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">
|
||||
Next
|
||||
</a>
|
||||
@@ -283,4 +367,23 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('per_page')?.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();
|
||||
});
|
||||
|
||||
// Auto-submit filter form when checkboxes change
|
||||
document.querySelectorAll('#filterForm input[type="checkbox"]').forEach(function(checkbox) {
|
||||
checkbox.addEventListener('change', function() {
|
||||
document.getElementById('filterForm').submit();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user