feat(games): show games in library mode also
This commit is contained in:
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -15,6 +15,8 @@ class Game(models.Model):
|
||||
custom_path = models.BooleanField(default=False)
|
||||
image = models.URLField(null=True, blank=True)
|
||||
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).
|
||||
# Prices are in cents, in steam_currency.
|
||||
|
||||
@@ -25,6 +25,7 @@ class SteamGameOut(Schema):
|
||||
steam_app_id: int
|
||||
store_url: str
|
||||
header_image: str
|
||||
library_image: str
|
||||
short_description: str
|
||||
is_free: bool
|
||||
currency: str
|
||||
@@ -44,6 +45,14 @@ class SteamGameOut(Schema):
|
||||
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
|
||||
def resolve_short_description(obj) -> str:
|
||||
return obj.steam_short_description
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useTitle } from "@vueuse/core";
|
||||
import { useStorage, useTitle } from "@vueuse/core";
|
||||
import { gamesApiListEventSteamGames } from "./api";
|
||||
import type { SteamGameOut } from "./api/types.gen";
|
||||
import SteamLibraryCard from "./components/SteamLibraryCard.vue";
|
||||
import SteamStoreCard from "./components/SteamStoreCard.vue";
|
||||
|
||||
interface Props {
|
||||
@@ -17,6 +18,8 @@ useTitle(computed(() => t("games.title")));
|
||||
|
||||
const games = ref<SteamGameOut[]>([]);
|
||||
const loading = ref(true);
|
||||
// Library (vertical covers) by default, the store cards show the prices
|
||||
const showPrices = useStorage("games.showPrices", true);
|
||||
|
||||
onMounted(async () => {
|
||||
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="w-full max-w-7xl">
|
||||
<!-- 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>
|
||||
<p class="text-xl text-base-content/70">{{ t("games.subtitle") }}</p>
|
||||
</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 -->
|
||||
<div v-if="loading" class="flex justify-center py-20">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
@@ -47,9 +57,14 @@ onMounted(async () => {
|
||||
{{ t("games.empty") }}
|
||||
</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" />
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@@ -952,6 +952,10 @@ export type SteamGameOut = {
|
||||
* Header Image
|
||||
*/
|
||||
header_image: string;
|
||||
/**
|
||||
* Library Image
|
||||
*/
|
||||
library_image: string;
|
||||
/**
|
||||
* 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>
|
||||
@@ -76,7 +76,8 @@
|
||||
"buy": "Buy {game}",
|
||||
"free": "Free to Play",
|
||||
"viewOnSteam": "View on Steam",
|
||||
"play": "Play"
|
||||
"play": "Play",
|
||||
"showPrices": "Show prices"
|
||||
},
|
||||
"clock": {
|
||||
"pageTitle": "Beat the Clock",
|
||||
|
||||
Reference in New Issue
Block a user