Merge pull request #2297 from rommapp/gallery-loading-states

Use skeleton loaders as loading states for games
This commit is contained in:
Georges-Antoine Assi
2025-08-21 09:29:32 -05:00
committed by GitHub
7 changed files with 135 additions and 14 deletions

View File

@@ -1,10 +1,26 @@
<script setup lang="ts">
import { views } from "@/utils";
import { storeToRefs } from "pinia";
import Skeleton from "@/components/common/Game/Card/Skeleton.vue";
import storeGalleryView from "@/stores/galleryView";
import { RECENT_ROMS_LIMIT } from "@/services/api/rom";
import storeRoms from "@/stores/roms";
const props = withDefaults(
defineProps<{
platformId?: number;
romCount?: number;
}>(),
{
platformId: undefined,
romCount: RECENT_ROMS_LIMIT,
},
);
const galleryViewStore = storeGalleryView();
const romsStore = storeRoms();
const { currentView } = storeToRefs(galleryViewStore);
const { fetchLimit } = storeToRefs(romsStore);
</script>
<template>
@@ -12,7 +28,7 @@ const { currentView } = storeToRefs(galleryViewStore);
<v-col>
<v-row v-if="currentView != 2" no-gutters class="mx-1 mt-3 mr-14">
<v-col
v-for="_ in 60"
v-for="_ in Math.min(props.romCount, fetchLimit)"
class="pa-1 align-self-end"
:cols="views[currentView]['size-cols']"
:sm="views[currentView]['size-sm']"
@@ -20,7 +36,7 @@ const { currentView } = storeToRefs(galleryViewStore);
:lg="views[currentView]['size-lg']"
:xl="views[currentView]['size-xl']"
>
<v-skeleton-loader type="card" />
<skeleton :platformId="props.platformId" />
</v-col>
</v-row>

View File

@@ -1,6 +1,8 @@
<script setup lang="ts">
import RSection from "@/components/common/RSection.vue";
import Skeleton from "@/components/common/Game/Card/Skeleton.vue";
import { views } from "@/utils";
import { RECENT_ROMS_LIMIT } from "@/services/api/rom";
defineProps<{ title: string }>();
</script>
@@ -9,7 +11,7 @@ defineProps<{ title: string }>();
<template #content>
<v-row class="flex-nowrap overflow-x-auto pa-1" no-gutters>
<v-col
v-for="_ in 15"
v-for="_ in RECENT_ROMS_LIMIT"
class="align-self-end pa-1"
:cols="views[0]['size-cols']"
:sm="views[0]['size-sm']"
@@ -17,7 +19,7 @@ defineProps<{ title: string }>();
:lg="views[0]['size-lg']"
:xl="views[0]['size-xl']"
>
<v-skeleton-loader type="card" />
<skeleton type="image" />
</v-col>
</v-row>
</template>

View File

@@ -6,6 +6,7 @@ import Sources from "@/components/common/Game/Card/Sources.vue";
import storePlatforms from "@/stores/platforms";
import PlatformIcon from "@/components/common/Platform/Icon.vue";
import MissingFromFSIcon from "@/components/common/MissingFromFSIcon.vue";
import Skeleton from "@/components/common/Game/Card/Skeleton.vue";
import storeCollections from "@/stores/collections";
import storeGalleryView from "@/stores/galleryView";
import { ROUTES } from "@/plugins/router";
@@ -356,7 +357,15 @@ onBeforeUnmount(() => {
eager
:src="smallCover || fallbackCoverImage"
:aspect-ratio="computedAspectRatio"
></v-img>
>
<template #placeholder>
<skeleton
:platformId="rom.platform_id"
:aspectRatio="computedAspectRatio"
type="image"
/>
</template>
</v-img>
</template>
</v-img>
</v-hover>

View File

@@ -0,0 +1,86 @@
<script setup lang="ts">
import storePlatforms from "@/stores/platforms";
import storeGalleryView from "@/stores/galleryView";
import { computed } from "vue";
const props = withDefaults(
defineProps<{
platformId?: number;
aspectRatio?: string | number;
type?: string;
}>(),
{
platformId: undefined,
aspectRatio: undefined,
type: "image, avatar, chip, chip",
},
);
const platformsStore = storePlatforms();
const galleryViewStore = storeGalleryView();
const computedAspectRatio = computed(() => {
const ratio =
props.aspectRatio ||
platformsStore.getAspectRatio(props.platformId || 0) ||
galleryViewStore.defaultAspectRatioCover;
return parseFloat(ratio.toString());
});
</script>
<template>
<v-skeleton-loader
class="card-skeleton"
:type="type"
:style="{ aspectRatio: computedAspectRatio }"
/>
</template>
<style>
.card-skeleton .v-skeleton-loader__card-avatar {
height: 100%;
}
.card-skeleton .v-skeleton-loader__image {
height: 100%;
border-radius: 4px;
}
.card-skeleton .v-skeleton-loader__avatar {
position: absolute;
bottom: 0;
left: 0;
margin: 4px;
min-height: 32px;
min-width: 32px;
max-height: 32px;
max-width: 32px;
&:after {
animation: none;
}
}
.card-skeleton .v-skeleton-loader__chip {
position: absolute;
font-size: 0.875rem;
padding: 0 12px;
height: 24px;
margin: 4px;
&:after {
animation: none;
}
}
.card-skeleton .v-skeleton-loader__chip:nth-of-type(3) {
bottom: 0;
right: 4px;
}
.card-skeleton .v-skeleton-loader__chip:nth-of-type(4) {
top: 0;
left: 0;
}
</style>

View File

@@ -150,12 +150,15 @@ async function getRoms({
});
}
export const RECENT_ROMS_LIMIT = 15;
export const RECENT_PLAYED_ROMS_LIMIT = 15;
async function getRecentRoms(): Promise<{ data: GetRomsResponse }> {
return api.get("/roms", {
params: {
order_by: "id",
order_dir: "desc",
limit: 15,
limit: RECENT_ROMS_LIMIT,
with_char_index: false,
},
});
@@ -166,7 +169,7 @@ async function getRecentPlayedRoms(): Promise<{ data: GetRomsResponse }> {
params: {
order_by: "last_played",
order_dir: "desc",
limit: 15,
limit: RECENT_PLAYED_ROMS_LIMIT,
with_char_index: false,
},
});

View File

@@ -234,8 +234,10 @@ onBeforeUnmount(() => {
<template>
<template v-if="!noCollectionError">
<gallery-app-bar-collection />
<template v-if="fetchingRoms && filteredRoms.length === 0">
<skeleton />
<template
v-if="currentCollection && fetchingRoms && filteredRoms.length === 0"
>
<skeleton :romCount="currentCollection.rom_count" />
</template>
<template v-else>
<template v-if="filteredRoms.length > 0">

View File

@@ -17,7 +17,7 @@ import type { Emitter } from "mitt";
import { isNull, throttle } from "lodash";
import { storeToRefs } from "pinia";
import { inject, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { onBeforeRouteUpdate, useRoute, useRouter } from "vue-router";
import { onBeforeRouteUpdate, useRoute } from "vue-router";
const route = useRoute();
const galleryViewStore = storeGalleryView();
@@ -36,7 +36,6 @@ const {
fetchTotalRoms,
} = storeToRefs(romsStore);
const noPlatformError = ref(false);
const router = useRouter();
const emitter = inject<Emitter<Events>>("emitter");
const isHovering = ref(false);
const hoveringRomId = ref();
@@ -238,8 +237,13 @@ onBeforeUnmount(() => {
<template>
<template v-if="!noPlatformError">
<gallery-app-bar />
<template v-if="fetchingRoms && filteredRoms.length === 0">
<skeleton />
<template
v-if="currentPlatform && fetchingRoms && filteredRoms.length === 0"
>
<skeleton
:platformId="currentPlatform.id"
:romCount="currentPlatform.rom_count"
/>
</template>
<template v-else>
<template v-if="filteredRoms.length > 0">
@@ -263,7 +267,6 @@ onBeforeUnmount(() => {
}"
>
<game-card
v-if="currentPlatform"
:key="rom.updated_at"
:rom="rom"
titleOnHover