feat(games): show games in library mode also

This commit is contained in:
2026-10-16 02:41:27 +02:00
parent 54ffe89445
commit 2f8fdb755b
7 changed files with 97 additions and 4 deletions
@@ -0,0 +1,18 @@
# Generated by Django 6.0.5 on 2026-10-16 00:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("games", "0010_game_steam_store_info"),
]
operations = [
migrations.AddField(
model_name="game",
name="library_image",
field=models.URLField(blank=True, default="", max_length=500),
),
]
+2
View File
@@ -15,6 +15,8 @@ class Game(models.Model):
custom_path = models.BooleanField(default=False) custom_path = models.BooleanField(default=False)
image = models.URLField(null=True, blank=True) image = models.URLField(null=True, blank=True)
description = models.TextField(blank=True, default="") description = models.TextField(blank=True, default="")
# Vertical (600x900) cover for games/showcase, empty = Steam's library capsule
library_image = models.URLField(max_length=500, blank=True, default="")
# Steam store info, refreshed in the background (see games.tasks). # Steam store info, refreshed in the background (see games.tasks).
# Prices are in cents, in steam_currency. # Prices are in cents, in steam_currency.
+9
View File
@@ -25,6 +25,7 @@ class SteamGameOut(Schema):
steam_app_id: int steam_app_id: int
store_url: str store_url: str
header_image: str header_image: str
library_image: str
short_description: str short_description: str
is_free: bool is_free: bool
currency: str currency: str
@@ -44,6 +45,14 @@ class SteamGameOut(Schema):
or f"https://cdn.akamai.steamstatic.com/steam/apps/{obj.steam_app_id}/header.jpg" or f"https://cdn.akamai.steamstatic.com/steam/apps/{obj.steam_app_id}/header.jpg"
) )
@staticmethod
def resolve_library_image(obj) -> str:
# Vertical capsule, like in the Steam library
return (
obj.library_image
or f"https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/{obj.steam_app_id}/library_600x900.jpg"
)
@staticmethod @staticmethod
def resolve_short_description(obj) -> str: def resolve_short_description(obj) -> str:
return obj.steam_short_description return obj.steam_short_description
+18 -3
View File
@@ -1,9 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, computed } from "vue"; import { ref, onMounted, computed } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useTitle } from "@vueuse/core"; import { useStorage, useTitle } from "@vueuse/core";
import { gamesApiListEventSteamGames } from "./api"; import { gamesApiListEventSteamGames } from "./api";
import type { SteamGameOut } from "./api/types.gen"; import type { SteamGameOut } from "./api/types.gen";
import SteamLibraryCard from "./components/SteamLibraryCard.vue";
import SteamStoreCard from "./components/SteamStoreCard.vue"; import SteamStoreCard from "./components/SteamStoreCard.vue";
interface Props { interface Props {
@@ -17,6 +18,8 @@ useTitle(computed(() => t("games.title")));
const games = ref<SteamGameOut[]>([]); const games = ref<SteamGameOut[]>([]);
const loading = ref(true); const loading = ref(true);
// Library (vertical covers) by default, the store cards show the prices
const showPrices = useStorage("games.showPrices", true);
onMounted(async () => { onMounted(async () => {
const response = await gamesApiListEventSteamGames({ const response = await gamesApiListEventSteamGames({
@@ -33,11 +36,18 @@ onMounted(async () => {
<div class="flex-1 bg-base-300 flex justify-center px-4 py-12"> <div class="flex-1 bg-base-300 flex justify-center px-4 py-12">
<div class="w-full max-w-7xl"> <div class="w-full max-w-7xl">
<!-- Header --> <!-- Header -->
<div class="text-center mb-12"> <div class="text-center mb-8">
<h1 class="text-5xl font-bold mb-4">{{ t("games.title") }}</h1> <h1 class="text-5xl font-bold mb-4">{{ t("games.title") }}</h1>
<p class="text-xl text-base-content/70">{{ t("games.subtitle") }}</p> <p class="text-xl text-base-content/70">{{ t("games.subtitle") }}</p>
</div> </div>
<div class="flex justify-end mb-6">
<label class="label cursor-pointer gap-2">
<span>{{ t("games.showPrices") }}</span>
<input v-model="showPrices" type="checkbox" class="toggle toggle-primary" />
</label>
</div>
<!-- Loading --> <!-- Loading -->
<div v-if="loading" class="flex justify-center py-20"> <div v-if="loading" class="flex justify-center py-20">
<span class="loading loading-spinner loading-lg"></span> <span class="loading loading-spinner loading-lg"></span>
@@ -47,9 +57,14 @@ onMounted(async () => {
{{ t("games.empty") }} {{ t("games.empty") }}
</p> </p>
<div v-else class="grid grid-cols-1 lg:grid-cols-2 gap-8"> <div v-else-if="showPrices" class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<SteamStoreCard v-for="game in games" :key="game.id" :game="game" /> <SteamStoreCard v-for="game in games" :key="game.id" :game="game" />
</div> </div>
<div v-else
class="grid grid-cols-[repeat(auto-fill,minmax(9rem,1fr))] sm:grid-cols-[repeat(auto-fill,minmax(11rem,1fr))] gap-4 sm:gap-6">
<SteamLibraryCard v-for="game in games" :key="game.id" :game="game" />
</div>
</div> </div>
</div> </div>
</template> </template>
+4
View File
@@ -952,6 +952,10 @@ export type SteamGameOut = {
* Header Image * Header Image
*/ */
header_image: string; header_image: string;
/**
* Library Image
*/
library_image: string;
/** /**
* Short Description * Short Description
*/ */
@@ -0,0 +1,44 @@
<script setup lang="ts">
import { ref } from "vue";
import { useI18n } from "vue-i18n";
import type { SteamGameOut } from "../api/types.gen";
defineProps<{ game: SteamGameOut }>();
const { t } = useI18n();
// Not every game has a library capsule on Steam, fall back to the header + name
const capsuleFailed = ref(false);
</script>
<template>
<div class="group relative aspect-[2/3] rounded-md overflow-hidden bg-base-200 hover:bg-base-100
shadow-xl hover:shadow-2xl transition-all transform hover:-translate-y-2 hover:scale-[1.05]">
<a :href="game.store_url" target="_blank" rel="noopener noreferrer" :title="game.name" class="block w-full h-full">
<img v-if="!capsuleFailed" :src="game.library_image" :alt="game.name" loading="lazy"
class="w-full h-full object-cover" @error="capsuleFailed = true" />
<div v-else class="w-full h-full flex flex-col justify-center gap-4 p-3 bg-gradient-to-b from-base-100 to-base-300">
<img :src="game.header_image" :alt="game.name" loading="lazy" class="w-full rounded-sm" />
<span class="text-center font-medium line-clamp-3">{{ game.name }}</span>
</div>
</a>
<!-- Opens the Steam client: launches the game if installed, offers to install it otherwise -->
<a :href="`steam://run/${game.steam_app_id}`" :title="t('games.play')"
class="steam-play absolute bottom-2 right-2 rounded-full w-10 h-10 flex items-center justify-center text-2xl
opacity-0 translate-y-1 transition-all group-hover:opacity-100 group-hover:translate-y-0 focus:opacity-100">
<i class="mdi mdi-play"></i>
</a>
</div>
</template>
<style scoped>
.steam-play {
color: #ffffff;
background: linear-gradient(to right, #47bfff 5%, #1a44c2 95%);
}
.steam-play:hover {
background: linear-gradient(to right, #6fceff 5%, #2d5fd8 95%);
}
</style>
+2 -1
View File
@@ -76,7 +76,8 @@
"buy": "Buy {game}", "buy": "Buy {game}",
"free": "Free to Play", "free": "Free to Play",
"viewOnSteam": "View on Steam", "viewOnSteam": "View on Steam",
"play": "Play" "play": "Play",
"showPrices": "Show prices"
}, },
"clock": { "clock": {
"pageTitle": "Beat the Clock", "pageTitle": "Beat the Clock",