Merge remote-tracking branch 'aden/develop' into develop

This commit is contained in:
quexeky
2025-02-18 14:45:09 +11:00
7 changed files with 513 additions and 121 deletions

View File

@ -12,7 +12,8 @@
<div class="h-full">
<MenuButton :class="[
styles[props.status.type],
'inline-flex w-full h-full justify-center items-center rounded-r-md px-1 py-2 text-sm font-semibold shadow-sm'
'inline-flex w-full h-full justify-center items-center rounded-r-md px-1 py-2 text-sm font-semibold shadow-sm group',
'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2'
]">
<ChevronDownIcon class="size-5" aria-hidden="true" />
</MenuButton>
@ -26,7 +27,8 @@
<div class="py-1">
<MenuItem v-slot="{ active }">
<button @click="() => emit('uninstall')"
:class="[active ? 'bg-zinc-800 text-zinc-100 outline-none' : 'text-zinc-400', 'w-full block px-4 py-2 text-sm inline-flex justify-between']">Uninstall
:class="[active ? 'bg-zinc-800 text-zinc-100 outline-none' : 'text-zinc-400', 'w-full block px-4 py-2 text-sm inline-flex justify-between']">
Uninstall
<TrashIcon class="size-5" />
</button>
</MenuItem>
@ -63,14 +65,14 @@ const emit = defineEmits<{
const showDropdown = computed(() => props.status.type === GameStatusEnum.Installed || props.status.type === GameStatusEnum.SetupRequired);
const styles: { [key in GameStatusEnum]: string } = {
[GameStatusEnum.Remote]: "bg-blue-600 text-white hover:bg-blue-500 focus-visible:outline-blue-600",
[GameStatusEnum.Queued]: "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700",
[GameStatusEnum.Downloading]: "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700",
[GameStatusEnum.SetupRequired]: "bg-yellow-600 text-white hover:bg-yellow-500 focus-visible:outline-yellow-600",
[GameStatusEnum.Installed]: "bg-green-600 text-white hover:bg-green-500 focus-visible:outline-green-600",
[GameStatusEnum.Updating]: "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700",
[GameStatusEnum.Uninstalling]: "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700",
[GameStatusEnum.Running]: "bg-zinc-800 text-white focus-visible:outline-zinc-700"
[GameStatusEnum.Remote]: "bg-blue-600 text-white hover:bg-blue-500 focus-visible:outline-blue-600 hover:bg-blue-500",
[GameStatusEnum.Queued]: "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
[GameStatusEnum.Downloading]: "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
[GameStatusEnum.SetupRequired]: "bg-yellow-600 text-white hover:bg-yellow-500 focus-visible:outline-yellow-600 hover:bg-yellow-500",
[GameStatusEnum.Installed]: "bg-green-600 text-white hover:bg-green-500 focus-visible:outline-green-600 hover:bg-green-500",
[GameStatusEnum.Updating]: "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
[GameStatusEnum.Uninstalling]: "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
[GameStatusEnum.Running]: "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700"
};
const buttonNames: { [key in GameStatusEnum]: string } = {

View File

@ -0,0 +1,166 @@
<template>
<div>
<div class="relative mb-3 transition-transform duration-300 hover:scale-105 active:scale-95">
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<MagnifyingGlassIcon class="h-5 w-5 text-zinc-400" aria-hidden="true" />
</div>
<input
type="text"
v-model="searchQuery"
class="block w-full rounded-lg border-0 bg-zinc-800/50 py-2 pl-10 pr-3 text-zinc-100 placeholder:text-zinc-500 focus:bg-zinc-800 focus:ring-2 focus:ring-inset focus:ring-blue-500 sm:text-sm sm:leading-6"
placeholder="Search library..."
/>
</div>
<TransitionGroup
name="list"
tag="ul"
class="flex flex-col gap-y-1.5"
>
<NuxtLink
v-for="nav in filteredNavigation"
:key="nav.id"
:class="[
'transition-all duration-300 rounded-lg flex items-center py-2 px-3 hover:scale-105 active:scale-95 hover:shadow-lg hover:shadow-zinc-950/50',
nav.index === currentNavigation
? 'bg-zinc-800 text-zinc-100 shadow-md shadow-zinc-950/20'
: nav.isInstalled.value
? 'text-zinc-300 hover:bg-zinc-800/90 hover:text-zinc-200'
: 'text-zinc-500 hover:bg-zinc-800/70 hover:text-zinc-300',
]"
:href="nav.route"
>
<div class="flex items-center w-full gap-x-3">
<div class="flex-none transition-transform duration-300 hover:-rotate-2">
<img
class="size-8 object-cover bg-zinc-900 rounded-lg transition-all duration-300 shadow-sm"
:src="icons[nav.id]"
alt=""
/>
</div>
<div class="flex flex-col flex-1">
<p class="truncate text-xs font-display leading-5 flex-1 font-semibold">
{{ nav.label }}
</p>
<p
class="text-[11px] font-medium"
:class="[
games[nav.id].status.value.type === GameStatusEnum.Installed ? 'text-green-500' :
games[nav.id].status.value.type === GameStatusEnum.Downloading ? 'text-blue-500' :
games[nav.id].status.value.type === GameStatusEnum.Running ? 'text-green-500' :
'text-zinc-500'
]"
>
{{
games[nav.id].status.value.type === GameStatusEnum.Downloading ? 'Downloading' :
games[nav.id].status.value.type === GameStatusEnum.Running ? 'Running' :
games[nav.id].status.value.type ? 'Installed' : 'Not Installed'
}}
</p>
</div>
</div>
</NuxtLink>
</TransitionGroup>
</div>
</template>
<script setup lang="ts">
import { MagnifyingGlassIcon } from '@heroicons/vue/20/solid';
import { invoke } from "@tauri-apps/api/core";
import { GameStatusEnum, type Game, type GameStatus } from "~/types";
import { TransitionGroup } from 'vue';
import { listen } from '@tauri-apps/api/event';
const router = useRouter();
const searchQuery = ref('');
let libraryDownloadError = false;
const games: { [key: string]: { game: Game, status: Ref<GameStatus, GameStatus> } } = {};
const icons: { [key: string]: string } = {};
const rawGames: Ref<Game[], Game[]> = ref([])
async function calculateGames() {
try {
rawGames.value = await invoke("fetch_library");
for (const game of rawGames.value) {
if (games[game.id]) continue;
games[game.id] = await useGame(game.id);
}
for (const game of rawGames.value) {
if (icons[game.id]) continue;
icons[game.id] = await useObject(game.mIconId);
}
}
catch (e) {
console.log(e)
libraryDownloadError = true;
return new Array();
}
}
await calculateGames();
const navigation = computed(() => rawGames.value.map((game) => {
const status = games[game.id].status;
const isInstalled = computed(
() =>
status.value.type == GameStatusEnum.Installed ||
status.value.type == GameStatusEnum.SetupRequired
);
const item = {
label: game.mName,
route: `/library/${game.id}`,
prefix: `/library/${game.id}`,
isInstalled,
id: game.id,
};
return item;
})
);
const { currentNavigation, recalculateNavigation } = useCurrentNavigationIndex(navigation.value);
const filteredNavigation = computed(() => {
if (!searchQuery.value) return navigation.value.map((e, i) => ({...e, index: i}));
const query = searchQuery.value.toLowerCase();
return navigation.value.filter(nav =>
nav.label.toLowerCase().includes(query)
).map((e, i) => ({...e, index: i}));
});
listen("update_library", async (event) => {
console.log("Updating library");
let oldNavigation = navigation.value[currentNavigation.value];
await calculateGames()
recalculateNavigation();
if (oldNavigation !== navigation.value[currentNavigation.value]) {
console.log("Triggered")
router.push("/library")
}
})
</script>
<style scoped>
.list-move,
.list-enter-active,
.list-leave-active {
transition: all 0.3s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateX(-30px);
}
.list-leave-active {
position: absolute;
}
</style>

View File

@ -1,23 +1,10 @@
<template>
<div class="flex flex-row h-full">
<div class="flex-none max-h-full overflow-y-auto w-64 bg-zinc-950 px-2 py-1">
<ul class="flex flex-col gap-y-1">
<NuxtLink v-for="(nav, navIdx) in navigation" :key="nav.route" :class="[
'transition-all duration-200 rounded-lg flex items-center py-1.5 px-3',
navIdx === currentNavigation
? 'bg-zinc-800 text-zinc-100'
: nav.isInstalled.value
? 'text-zinc-300 hover:bg-zinc-800/90 hover:text-zinc-200'
: 'text-zinc-500 hover:bg-zinc-800/70 hover:text-zinc-300',
]" :href="nav.route">
<div class="flex items-center w-full gap-x-3">
<img class="size-6 flex-none object-cover bg-zinc-900 rounded" :src="icons[nav.id]" alt="" />
<p class="truncate text-sm font-display leading-6 flex-1">
{{ nav.label }}
</p>
</div>
</NuxtLink>
</ul>
<!-- Sidebar -->
<div
class="flex-none max-h-full overflow-y-auto w-72 bg-zinc-950/50 backdrop-blur-xl px-4 py-3 border-r border-zinc-800/50"
>
<LibrarySearch />
</div>
<div class="grow overflow-y-auto">
<NuxtPage :libraryDownloadError="libraryDownloadError" />
@ -26,64 +13,25 @@
</template>
<script setup lang="ts">
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { GameStatusEnum, type Game, type GameStatus, type NavigationItem } from "~/types";
let libraryDownloadError = false;
const games: { [key: string]: { game: Game, status: Ref<GameStatus, GameStatus> } } = {};
const icons: { [key: string]: string } = {};
</script>
const rawGames: Ref<Game[], Game[]> = ref([])
async function calculateGames() {
try {
rawGames.value = await invoke("fetch_library");
for (const game of rawGames.value) {
if (games[game.id]) continue;
games[game.id] = await useGame(game.id);
}
for (const game of rawGames.value) {
if (icons[game.id]) continue;
icons[game.id] = await useObject(game.mIconId);
}
}
catch (e) {
console.log(e)
libraryDownloadError = true;
return new Array();
}
<style scoped>
.list-move,
.list-enter-active,
.list-leave-active {
transition: all 0.3s ease;
}
await calculateGames();
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateX(-30px);
}
const navigation = computed(() => rawGames.value.map((game) => {
const status = games[game.id].status;
const isInstalled = computed(
() =>
status.value.type == GameStatusEnum.Installed ||
status.value.type == GameStatusEnum.SetupRequired
);
const item = {
label: game.mName,
route: `/library/${game.id}`,
prefix: `/library/${game.id}`,
isInstalled,
id: game.id,
};
return item;
})
);
const { currentNavigation, recalculateNavigation } = useCurrentNavigationIndex(navigation.value);
listen("update_library", async (event) => {
console.log("Updating library");
await calculateGames()
recalculateNavigation();
})
</script>
.list-leave-active {
position: absolute;
}
</style>

View File

@ -1,23 +1,24 @@
<template>
<div
class="mx-auto w-full relative flex flex-col justify-center pt-64 z-10 overflow-hidden"
>
<!-- banner image -->
<div class="absolute flex top-0 h-fit inset-x-0 z-[-20]">
<img :src="bannerUrl" class="w-full h-auto object-cover" />
<h1
class="absolute inset-x-0 w-fit mx-auto text-center top-32 -translate-y-[50%] text-4xl text-zinc-100 font-bold font-display z-50 p-4 shadow-xl bg-zinc-900/80 rounded-xl"
class="mx-auto w-full relative flex flex-col justify-center pt-72 overflow-hidden"
>
<div class="absolute inset-0 z-0">
<img
:src="bannerUrl"
class="w-full h-[24rem] object-cover blur-sm scale-105"
/>
<div class="absolute inset-0 bg-gradient-to-t from-zinc-900 via-zinc-900/80 to-transparent opacity-90" />
<div class="absolute inset-0 bg-gradient-to-r from-zinc-900/95 via-zinc-900/80 to-transparent opacity-90" />
</div>
<div class="relative z-10">
<div class="px-8 pb-4">
<h1 class="text-5xl text-zinc-100 font-bold font-display drop-shadow-lg mb-8">
{{ game.mName }}
</h1>
<div
class="absolute inset-0 bg-gradient-to-b from-transparent to-50% to-zinc-900"
/>
</div>
<!-- main page -->
<div class="w-full min-h-screen mx-auto bg-zinc-900 px-5 py-6">
<!-- game toolbar -->
<div class="h-full flex flex-row gap-x-4 items-stretch">
<div class="flex flex-row gap-x-4 items-stretch mb-8">
<div class="transition-transform duration-300 hover:scale-105 active:scale-95 shadow-xl">
<GameStatusButton
@install="() => installFlow()"
@launch="() => launch()"
@ -26,18 +27,145 @@
@kill="() => kill()"
:status="status"
/>
</div>
<a
:href="remoteUrl"
target="_blank"
type="button"
class="inline-flex items-center rounded-md bg-zinc-800/50 px-4 font-semibold text-white shadow-sm hover:bg-zinc-800/80 uppercase font-display"
class="transition-transform duration-300 hover:scale-105 active:scale-95 inline-flex items-center rounded-md bg-zinc-800/50 px-6 font-semibold text-white shadow-xl backdrop-blur-sm hover:bg-zinc-800/80 uppercase font-display"
>
<BuildingStorefrontIcon class="mr-2 size-5" aria-hidden="true" />
Store
</a>
</div>
</div>
<!-- Main content -->
<div class="w-full bg-zinc-900 px-8 py-6">
<div class="grid grid-cols-[2fr,1fr] gap-8">
<div class="space-y-6">
<div class="bg-zinc-800/50 rounded-xl p-6 backdrop-blur-sm">
<h2 class="text-xl font-display font-semibold text-zinc-100 mb-4">About</h2>
<div class="max-h-48 overflow-y-auto custom-scrollbar">
<p class="text-zinc-400">
{{ game.mDescription || "No description available." }}
</p>
</div>
</div>
<div class="bg-zinc-800/50 rounded-xl p-6 backdrop-blur-sm">
<h2 class="text-xl font-display font-semibold text-zinc-100 mb-4">Installation</h2>
<dl class="grid grid-cols-1 gap-4 text-sm">
<div>
<dt class="text-zinc-400">Status</dt>
<dd class="text-zinc-200 font-medium flex items-center gap-x-2">
<span
class="size-2 rounded-full"
:class="{
'bg-green-500': status.type === GameStatusEnum.Installed || status.type === GameStatusEnum.Running,
'bg-yellow-500': status.type === GameStatusEnum.SetupRequired,
'bg-blue-500 animate-pulse': status.type === GameStatusEnum.Downloading,
'bg-zinc-500': status.type === GameStatusEnum.Remote
}"
/>
{{
status.type === GameStatusEnum.Installed ? 'Installed' :
status.type === GameStatusEnum.Running ? 'Running' :
status.type === GameStatusEnum.SetupRequired ? 'Setup Required' :
status.type === GameStatusEnum.Downloading ? 'Downloading' :
'Not Installed'
}}
</dd>
</div>
</dl>
</div>
</div>
<div class="space-y-6">
<div class="bg-zinc-800/50 rounded-xl p-6 backdrop-blur-sm">
<h2 class="text-xl font-display font-semibold text-zinc-100 mb-4">Game Images</h2>
<div class="relative pb-8">
<div v-if="mediaUrls.length > 0">
<div
class="relative aspect-video rounded-lg overflow-hidden cursor-pointer group"
>
<div
class="absolute inset-0"
@click="fullscreenImage = mediaUrls[currentImageIndex]"
>
<TransitionGroup
name="slide"
tag="div"
class="h-full"
>
<img
v-for="(url, index) in mediaUrls"
:key="url"
:src="url"
class="absolute inset-0 w-full h-full object-cover"
v-show="index === currentImageIndex"
/>
</TransitionGroup>
</div>
<div class="absolute inset-0 flex items-center justify-between px-4 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none">
<div class="pointer-events-auto">
<button
v-if="mediaUrls.length > 1"
@click.stop="previousImage()"
class="p-2 rounded-full bg-zinc-900/50 text-zinc-100 hover:bg-zinc-900/80 transition-all duration-300 hover:scale-110"
>
<ChevronLeftIcon class="size-5" />
</button>
</div>
<div class="pointer-events-auto">
<button
v-if="mediaUrls.length > 1"
@click.stop="nextImage()"
class="p-2 rounded-full bg-zinc-900/50 text-zinc-100 hover:bg-zinc-900/80 transition-all duration-300 hover:scale-110"
>
<ChevronRightIcon class="size-5" />
</button>
</div>
</div>
<div class="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none" />
<div class="absolute bottom-4 right-4 flex items-center gap-x-2 text-white opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none">
<ArrowsPointingOutIcon class="size-5" />
<span class="text-sm font-medium">View Fullscreen</span>
</div>
</div>
<div class="absolute -bottom-2 left-1/2 -translate-x-1/2 flex gap-x-2">
<button
v-for="(_, index) in mediaUrls"
:key="index"
@click.stop="currentImageIndex = index"
class="w-1.5 h-1.5 rounded-full transition-all"
:class="[
currentImageIndex === index
? 'bg-zinc-100 scale-125'
: 'bg-zinc-600 hover:bg-zinc-500'
]"
/>
</div>
</div>
<div
v-else
class="aspect-video rounded-lg overflow-hidden bg-zinc-900/50 flex flex-col items-center justify-center text-center px-4"
>
<PhotoIcon class="size-12 text-zinc-700 mb-2" />
<p class="text-zinc-600 font-medium">No images available</p>
<p class="text-zinc-700 text-sm">Game screenshots will appear here when available</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<ModalTemplate v-model="installFlowOpen">
@ -256,6 +384,70 @@
</button>
</template>
</ModalTemplate>
<Transition
enter="transition ease-out duration-300"
enter-from="opacity-0"
enter-to="opacity-100"
leave="transition ease-in duration-200"
leave-from="opacity-100"
leave-to="opacity-0"
>
<div
v-if="fullscreenImage"
class="fixed inset-0 z-50 bg-black/95 flex items-center justify-center"
@click="fullscreenImage = null"
>
<div
class="relative w-full h-full flex items-center justify-center"
@click.stop
>
<button
class="absolute top-4 right-4 p-2 rounded-full bg-zinc-900/50 text-zinc-100 hover:bg-zinc-900 transition-colors"
@click.stop="fullscreenImage = null"
>
<XMarkIcon class="size-6" />
</button>
<button
v-if="mediaUrls.length > 1"
@click.stop="previousImage()"
class="absolute left-4 p-3 rounded-full bg-zinc-900/50 text-zinc-100 hover:bg-zinc-900 transition-colors"
>
<ChevronLeftIcon class="size-6" />
</button>
<button
v-if="mediaUrls.length > 1"
@click.stop="nextImage()"
class="absolute right-4 p-3 rounded-full bg-zinc-900/50 text-zinc-100 hover:bg-zinc-900 transition-colors"
>
<ChevronRightIcon class="size-6" />
</button>
<TransitionGroup
name="slide"
tag="div"
class="w-full h-full flex items-center justify-center"
@click.stop
>
<img
v-for="(url, index) in mediaUrls"
v-show="currentImageIndex === index"
:key="url"
:src="url"
class="max-h-[90vh] max-w-[90vw] object-contain"
:alt="`${game.mName} screenshot ${index + 1}`"
/>
</TransitionGroup>
<div class="absolute bottom-4 left-1/2 -translate-x-1/2 px-4 py-2 rounded-full bg-zinc-900/50 backdrop-blur-sm">
<p class="text-zinc-100 text-sm font-medium">
{{ currentImageIndex + 1 }} / {{ mediaUrls.length }}
</p>
</div>
</div>
</div>
</Transition>
</template>
<script setup lang="ts">
@ -274,10 +466,16 @@ import {
CheckIcon,
ChevronUpDownIcon,
WrenchIcon,
ChevronLeftIcon,
ChevronRightIcon,
XMarkIcon,
ArrowsPointingOutIcon,
PhotoIcon,
} from "@heroicons/vue/20/solid";
import { BuildingStorefrontIcon } from "@heroicons/vue/24/outline";
import { XCircleIcon } from "@heroicons/vue/24/solid";
import { invoke } from "@tauri-apps/api/core";
import { GameStatusEnum } from "~/types";
const route = useRoute();
const router = useRouter();
@ -292,11 +490,16 @@ const remoteUrl: string = await invoke("gen_drop_url", {
const bannerUrl = await useObject(game.value.mBannerId);
// Get all available images
const mediaUrls = await Promise.all(game.value.mImageCarousel.map((id) => useObject(id)));
const installFlowOpen = ref(false);
const versionOptions = ref<
undefined | Array<{ versionName: string; platform: string }>
>();
const installDirs = ref<undefined | Array<string>>();
const currentImageIndex = ref(0);
async function installFlow() {
installFlowOpen.value = true;
versionOptions.value = undefined;
@ -376,4 +579,60 @@ async function kill() {
console.error(e);
}
}
function nextImage() {
currentImageIndex.value = (currentImageIndex.value + 1) % mediaUrls.length;
}
function previousImage() {
currentImageIndex.value = (currentImageIndex.value - 1 + mediaUrls.length) % mediaUrls.length;
}
const fullscreenImage = ref<string | null>(null);
</script>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.slide-enter-active,
.slide-leave-active {
transition: all 0.3s ease;
position: absolute;
}
.slide-enter-from {
opacity: 0;
transform: translateX(100%);
}
.slide-leave-to {
opacity: 0;
transform: translateX(-100%);
}
.custom-scrollbar {
scrollbar-width: thin;
scrollbar-color: rgb(82 82 91) transparent;
}
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background-color: rgb(82 82 91);
border-radius: 3px;
}
</style>

View File

@ -1,9 +1,24 @@
<script setup lang="ts">
const props = defineProps<{ libraryDownloadError: boolean }>();
</script>
<template>
<div v-if="libraryDownloadError" class="mx-auto pt-10 text-center text-gray-500">
Library Failed to update
</div>
<div class="h-full flex flex-col items-center justify-center">
<div class="text-center">
<div class="flex flex-col items-center gap-y-4">
<div class="p-4 rounded-xl bg-zinc-800/50 backdrop-blur-sm">
<RocketLaunchIcon class="size-12 text-zinc-600" />
</div>
<div>
<h3 class="text-xl font-display font-semibold text-zinc-100">Select a game</h3>
<p class="mt-1 text-sm text-zinc-500">Choose a game from your library to view details</p>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { RocketLaunchIcon } from '@heroicons/vue/24/outline';
const props = defineProps<{ libraryDownloadError: boolean }>();
</script>

View File

@ -37,6 +37,7 @@ pub struct Game {
m_banner_id: String,
m_cover_id: String,
m_image_library: Vec<String>,
m_image_carousel: Vec<String>,
}
#[derive(serde::Serialize, Clone)]
pub struct GameUpdateEvent {

View File

@ -34,6 +34,7 @@ export type Game = {
mBannerId: string;
mCoverId: string;
mImageLibrary: string[];
mImageCarousel: string[];
};
export enum AppStatus {