diff --git a/app/Controllers/GameController.php b/app/Controllers/GameController.php index 90d3325..2cbd5dd 100644 --- a/app/Controllers/GameController.php +++ b/app/Controllers/GameController.php @@ -189,6 +189,14 @@ class GameController extends Controller } $platforms = array_filter($platforms); + $features = $queryParams['features'] ?? []; + if (!is_array($features)) { + $features = [$features]; + } + $features = array_filter($features); + + $playtimeFilter = $queryParams['playtime'] ?? ''; + // Get view mode $viewMode = $queryParams['view'] ?? 'grid'; // grid, list, covers @@ -196,14 +204,15 @@ class GameController extends Controller $sort = $queryParams['sort'] ?? 'title_asc'; // Get games with pagination, filters, and sorting - $games = Game::getGroupedGamesWithPagination($this->pdo, $page, $perPage, $search, $genres, $platforms, $sort); + $games = Game::getGroupedGamesWithPagination($this->pdo, $page, $perPage, $search, $genres, $platforms, $features, $playtimeFilter, $sort); // Get total count for pagination - $totalCount = Game::getTotalCount($this->pdo, $search, $genres, $platforms); + $totalCount = Game::getTotalCount($this->pdo, $search, $genres, $platforms, $features, $playtimeFilter); // Get available filter options $availableGenres = Game::getAvailableGenres($this->pdo); $availablePlatforms = Game::getAvailablePlatforms($this->pdo); + $availableFeatures = Game::getAvailableFeatures($this->pdo); // Calculate pagination info $totalPages = ceil($totalCount / $perPage); @@ -228,11 +237,14 @@ class GameController extends Controller 'view_modes' => ['grid', 'list', 'covers'], 'filters' => [ 'genres' => $genres, - 'platforms' => $platforms + 'platforms' => $platforms, + 'features' => $features, + 'playtime' => $playtimeFilter ], 'available_filters' => [ 'genres' => $availableGenres, - 'platforms' => $availablePlatforms + 'platforms' => $availablePlatforms, + 'features' => $availableFeatures ], 'sort' => $sort, 'sort_options' => [ diff --git a/app/Models/Game.php b/app/Models/Game.php index 24c3e7f..351f713 100644 --- a/app/Models/Game.php +++ b/app/Models/Game.php @@ -264,7 +264,7 @@ class Game extends Model /** * Get total count of games for pagination */ - public static function getTotalCount(\PDO $pdo, string $search = '', array $genres = [], array $platforms = []): int + public static function getTotalCount(\PDO $pdo, string $search = '', array $genres = [], array $platforms = [], array $features = [], string $playtimeFilter = ''): int { $sql = "SELECT COUNT(*) as count FROM games WHERE game_key IS NOT NULL"; $params = []; @@ -275,12 +275,12 @@ class Game extends Model } if (!empty($genres)) { - $placeholders = []; + $genreConditions = []; foreach ($genres as $index => $genre) { - $placeholders[] = ":genre_{$index}"; + $genreConditions[] = "JSON_SEARCH(metadata, 'one', :genre_{$index}) IS NOT NULL"; $params["genre_{$index}"] = $genre; } - $sql .= " AND genre IN (" . implode(',', $placeholders) . ")"; + $sql .= " AND (" . implode(' OR ', $genreConditions) . ")"; } if (!empty($platforms)) { @@ -292,6 +292,38 @@ class Game extends Model $sql .= " AND platform IN (" . implode(',', $placeholders) . ")"; } + if (!empty($features)) { + $featureConditions = []; + foreach ($features as $index => $feature) { + $featureConditions[] = "JSON_SEARCH(metadata, 'one', :feature_{$index}) IS NOT NULL"; + $params["feature_{$index}"] = $feature; + } + $sql .= " AND (" . implode(' OR ', $featureConditions) . ")"; + } + + if (!empty($playtimeFilter)) { + switch ($playtimeFilter) { + case 'none': + $sql .= " AND (playtime_minutes IS NULL OR playtime_minutes = 0)"; + break; + case 'under_1h': + $sql .= " AND playtime_minutes > 0 AND playtime_minutes < 60"; + break; + case '1h_5h': + $sql .= " AND playtime_minutes >= 60 AND playtime_minutes < 300"; + break; + case '5h_10h': + $sql .= " AND playtime_minutes >= 300 AND playtime_minutes < 600"; + break; + case '10h_20h': + $sql .= " AND playtime_minutes >= 600 AND playtime_minutes < 1200"; + break; + case 'over_20h': + $sql .= " AND playtime_minutes >= 1200"; + break; + } + } + $stmt = $pdo->prepare($sql); $stmt->execute($params); return (int) $stmt->fetch()['count']; @@ -300,7 +332,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 $genres = [], array $platforms = [], string $sort = 'title_asc'): array + public static function getGroupedGamesWithPagination(\PDO $pdo, int $page, int $perPage, string $search = '', array $genres = [], array $platforms = [], array $features = [], string $playtimeFilter = '', string $sort = 'title_asc'): array { $offset = ($page - 1) * $perPage; @@ -330,12 +362,12 @@ class Game extends Model } if (!empty($genres)) { - $placeholders = []; + $genreConditions = []; foreach ($genres as $index => $genre) { - $placeholders[] = ":genre_{$index}"; + $genreConditions[] = "JSON_SEARCH(metadata, 'one', :genre_{$index}) IS NOT NULL"; $params["genre_{$index}"] = $genre; } - $sql .= " AND genre IN (" . implode(',', $placeholders) . ")"; + $sql .= " AND (" . implode(' OR ', $genreConditions) . ")"; } if (!empty($platforms)) { @@ -347,6 +379,38 @@ class Game extends Model $sql .= " AND platform IN (" . implode(',', $placeholders) . ")"; } + if (!empty($features)) { + $featureConditions = []; + foreach ($features as $index => $feature) { + $featureConditions[] = "JSON_SEARCH(metadata, 'one', :feature_{$index}) IS NOT NULL"; + $params["feature_{$index}"] = $feature; + } + $sql .= " AND (" . implode(' OR ', $featureConditions) . ")"; + } + + if (!empty($playtimeFilter)) { + switch ($playtimeFilter) { + case 'none': + $sql .= " AND (playtime_minutes IS NULL OR playtime_minutes = 0)"; + break; + case 'under_1h': + $sql .= " AND playtime_minutes > 0 AND playtime_minutes < 60"; + break; + case '1h_5h': + $sql .= " AND playtime_minutes >= 60 AND playtime_minutes < 300"; + break; + case '5h_10h': + $sql .= " AND playtime_minutes >= 300 AND playtime_minutes < 600"; + break; + case '10h_20h': + $sql .= " AND playtime_minutes >= 600 AND playtime_minutes < 1200"; + break; + case 'over_20h': + $sql .= " AND playtime_minutes >= 1200"; + break; + } + } + // Add sorting $sortOptions = [ 'title_asc' => 'title ASC', @@ -485,12 +549,28 @@ class Game extends Model public static function getAvailableGenres(\PDO $pdo): array { $stmt = $pdo->query(" - SELECT DISTINCT genre + SELECT metadata FROM games - WHERE genre IS NOT NULL AND genre != '' - ORDER BY genre + WHERE metadata IS NOT NULL AND metadata != '' AND metadata != '{}' "); - return $stmt->fetchAll(\PDO::FETCH_COLUMN); + $genres = []; + $results = $stmt->fetchAll(\PDO::FETCH_COLUMN); + + foreach ($results as $json) { + $decoded = json_decode($json, true); + if (is_array($decoded) && isset($decoded['genres']) && is_array($decoded['genres'])) { + foreach ($decoded['genres'] as $genre) { + if (is_string($genre)) { + $genres[] = $genre; + } + } + } + } + + $genres = array_unique($genres); + sort($genres); + + return array_values(array_filter($genres)); } /** @@ -507,6 +587,36 @@ class Game extends Model return $stmt->fetchAll(\PDO::FETCH_COLUMN); } + /** + * Get available features for filtering + */ + public static function getAvailableFeatures(\PDO $pdo): array + { + $stmt = $pdo->query(" + SELECT metadata + FROM games + WHERE metadata IS NOT NULL AND metadata != '' AND metadata != '{}' + "); + $features = []; + $results = $stmt->fetchAll(\PDO::FETCH_COLUMN); + + foreach ($results as $json) { + $decoded = json_decode($json, true); + if (is_array($decoded) && isset($decoded['features']) && is_array($decoded['features'])) { + foreach ($decoded['features'] as $feature) { + if (is_string($feature)) { + $features[] = $feature; + } + } + } + } + + $features = array_unique($features); + sort($features); + + return array_values(array_filter($features)); + } + /** * Check if game has rich Playnite data */ diff --git a/resources/views/games/index.twig b/resources/views/games/index.twig index b98acc2..d42924b 100644 --- a/resources/views/games/index.twig +++ b/resources/views/games/index.twig @@ -11,6 +11,12 @@ {% for platform in filters.platforms %} {% endfor %} + {% for feature in filters.features %} + + {% endfor %} + {% if filters.playtime %} + + {% endif %} @@ -18,7 +24,7 @@
{% for mode in view_modes %} @@ -42,6 +48,98 @@ {% endfor %}
+ +{% if available_filters.genres %} +
+ +
+
+
+ + + + {% for platform in filters.platforms %} + + {% endfor %} + {% for feature in filters.features %} + + {% endfor %} + {% if filters.playtime %} + + {% endif %} +
+ {% for genre in available_filters.genres %} + + {% endfor %} +
+
+ +
+
+
+
+
+{% endif %} + + +{% if available_filters.features %} +
+ +
+
+
+ + + + {% for genre in filters.genres %} + + {% endfor %} + {% for platform in filters.platforms %} + + {% endfor %} + {% if filters.playtime %} + + {% endif %} +
+ {% for feature in available_filters.features %} + + {% endfor %} +
+
+ +
+
+
+
+
+{% endif %} +
- {% if filters.genres or filters.platforms or search %} + {% if filters.genres or filters.platforms or filters.features or filters.playtime or search %}

Active Filters

{% if search %}
Search: "{{ search }}" - + @@ -138,7 +264,7 @@ {% for genre in filters.genres %}
Genre: {{ genre }} - + @@ -148,13 +274,33 @@ {% for platform in filters.platforms %} {% endfor %} + {% for feature in filters.features %} +
+ Feature: {{ feature }} + + + + + +
+ {% endfor %} + {% if filters.playtime %} +
+ Playtime: {% if filters.playtime == 'none' %}No playtime{% elseif filters.playtime == 'under_1h' %}Under 1 hour{% elseif filters.playtime == '1h_5h' %}1-5 hours{% elseif filters.playtime == '5h_10h' %}5-10 hours{% elseif filters.playtime == '10h_20h' %}10-20 hours{% elseif filters.playtime == 'over_20h' %}Over 20 hours{% endif %} + + + + + +
+ {% endif %}
{% endif %} @@ -194,13 +340,21 @@ {% if search %} matching "{{ search }}" {% endif %} - {% if filters.genres or filters.platforms %} + {% if filters.genres or filters.platforms or filters.features or filters.playtime %} {% if filters.genres %} {{ filters.genres|join(', ') }} {% endif %} {% if filters.platforms %} {{ filters.platforms|join(', ') }} {% endif %} + {% if filters.features %} + {{ filters.features|join(', ') }} + {% endif %} + {% if filters.playtime %} + + {% if filters.playtime == 'none' %}No playtime{% elseif filters.playtime == 'under_1h' %}Under 1h{% elseif filters.playtime == '1h_5h' %}1-5h{% elseif filters.playtime == '5h_10h' %}5-10h{% elseif filters.playtime == '10h_20h' %}10-20h{% elseif filters.playtime == 'over_20h' %}20h+{% endif %} + + {% endif %} {% endif %}
{% endif %} @@ -212,20 +366,20 @@

- {% if search or filters.genres or filters.platforms %} + {% if search or filters.genres or filters.platforms or filters.features or filters.playtime %} No games found matching your criteria {% else %} No games found {% endif %}

- {% if search or filters.genres or filters.platforms %} + {% if search or filters.genres or filters.platforms or filters.features or filters.playtime %} Try adjusting your search terms or filters. {% else %} Start syncing your gaming libraries to see your games here. {% endif %}

- {% if search or filters.genres or filters.platforms %} + {% if search or filters.genres or filters.platforms or filters.features or filters.playtime %} Clear filters @@ -390,7 +544,7 @@