Merge pull request #894 from rommapp/refactor/gallery_render

Refactor gallery render
This commit is contained in:
Zurdi
2024-05-28 19:48:18 +02:00
committed by GitHub
10 changed files with 179 additions and 84 deletions

View File

@@ -29,10 +29,10 @@ onBeforeMount(async () => {
"https://api.github.com/repos/rommapp/romm/releases/latest"
);
const json = await response.json();
GITHUB_VERSION.value = json.name;
GITHUB_VERSION.value = json.tag_name;
latestVersionDismissed.value =
VERSION === "development" ||
json.name === localStorage.getItem("dismissedVersion");
json.tag_name === localStorage.getItem("dismissedVersion");
});
async function logout() {
@@ -108,7 +108,7 @@ async function logout() {
<v-col class="py-1">
<span
>New version available
<span class="text-romm-accent-1">{{ GITHUB_VERSION }}</span></span
<span class="text-romm-accent-1">v{{ GITHUB_VERSION }}</span></span
>
</v-col>
</v-row>

View File

@@ -27,7 +27,7 @@ const {
<div v-if="showFilterBar">
<v-row no-gutters class="pa-1">
<filter-unmatched-btn />
<v-select
<v-autocomplete
hide-details
clearable
label="Genre"
@@ -37,8 +37,8 @@ const {
@update:model-value="emitter?.emit('filter', null);"
v-model="selectedGenre"
:items="galleryFilterStore.filterGenres"
></v-select>
<v-select
></v-autocomplete>
<v-autocomplete
hide-details
clearable
label="Franchise"
@@ -48,8 +48,8 @@ const {
@update:model-value="emitter?.emit('filter', null)"
v-model="selectedFranchise"
:items="galleryFilterStore.filterFranchises"
></v-select>
<v-select
></v-autocomplete>
<v-autocomplete
hide-details
clearable
label="Collection"
@@ -59,8 +59,8 @@ const {
@update:model-value="emitter?.emit('filter', null)"
v-model="selectedCollection"
:items="galleryFilterStore.filterCollections"
></v-select>
<v-select
></v-autocomplete>
<v-autocomplete
hide-details
clearable
label="Company"
@@ -70,7 +70,7 @@ const {
@update:model-value="emitter?.emit('filter', null)"
v-model="selectedCompany"
:items="galleryFilterStore.filterCompanies"
></v-select>
></v-autocomplete>
</v-row>
<v-divider
:thickness="2"

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import LazyImage from "@/components/LazyImage.vue";
import storeDownload from "@/stores/download";
import storeGalleryView from "@/stores/galleryView";
import storeRoms, { type SimpleRom } from "@/stores/roms";
@@ -82,9 +83,10 @@ function onTouchEnd() {
absolute
/>
<v-hover v-slot="{ isHovering, props }" open-delay="800">
<img
<lazy-image
:key="rom.id"
v-bind="props"
:placeholder="`/assets/default/cover/big_${theme.global.name.value}_missing_cover.png`"
:src="
!rom.igdb_id && !rom.moby_id && !rom.has_cover
? `/assets/default/cover/big_${theme.global.name.value}_unmatched.png`
@@ -140,7 +142,7 @@ function onTouchEnd() {
</v-chip>
</v-row>
</div>
</img>
</lazy-image>
</v-hover>
</router-link>
</template>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref } from "vue";
import { ref, inject, onMounted, watch } from "vue";
import { useRouter } from "vue-router";
import AdminMenu from "@/components/Game/AdminMenu/Base.vue";
@@ -7,6 +7,8 @@ import romApi from "@/services/api/rom";
import storeAuth from "@/stores/auth";
import storeDownload from "@/stores/download";
import storeRoms from "@/stores/roms";
import type { Events } from "@/types/emitter";
import type { Emitter } from "mitt";
import {
formatBytes,
languageToEmoji,
@@ -62,21 +64,39 @@ const HEADERS = [
{ title: "", align: "end", key: "actions", sortable: false },
] as const;
const PER_PAGE_OPTIONS = [
{ value: -1, title: "$vuetify.dataFooter.itemsPerPageAll" },
] as const;
const PER_PAGE_OPTIONS = [10, 25, 50, 100];
const emitter = inject<Emitter<Events>>("emitter");
emitter?.on("updateDataTablePages", updateDataTablePages);
// Props
const router = useRouter();
const downloadStore = storeDownload();
const romsStore = storeRoms();
const auth = storeAuth();
const romsPerPage = ref(-1);
const page = ref(1);
const storedRomsPerPage = parseInt(localStorage.getItem("romsPerPage") ?? "");
const romsPerPage = ref(isNaN(storedRomsPerPage) ? 25 : storedRomsPerPage);
const pageCount = ref(0);
// Functions
function rowClick(_: Event, row: any) {
router.push({ name: "rom", params: { rom: row.item.id } });
}
function updateDataTablePages() {
pageCount.value = Math.ceil(
romsStore.filteredRoms.length / romsPerPage.value
);
}
watch(romsPerPage, async () => {
localStorage.setItem("romsPerPage", romsPerPage.value.toString());
updateDataTablePages();
});
onMounted(() => {
updateDataTablePages();
});
</script>
<template>
@@ -91,6 +111,7 @@ function rowClick(_: Event, row: any) {
@click:row="rowClick"
show-select
v-model="romsStore._selectedIDs"
v-model:page="page"
>
<template v-slot:item.path_cover_s="{ item }">
<v-avatar :rounded="0">
@@ -100,7 +121,7 @@ function rowClick(_: Event, row: any) {
:indeterminate="true"
absolute
/>
<v-img
<img
:src="
!item.igdb_id && !item.has_cover
? `/assets/default/cover/small_${theme.global.name.value}_unmatched.png`
@@ -108,17 +129,20 @@ function rowClick(_: Event, row: any) {
? `/assets/default/cover/small_${theme.global.name.value}_missing_cover.png`
: `/assets/romm/resources/${item.path_cover_s}`
"
:lazy-src="
!item.igdb_id && !item.has_cover
? `/assets/default/cover/small_${theme.global.name.value}_unmatched.png`
: !item.has_cover
? `/assets/default/cover/small_${theme.global.name.value}_missing_cover.png`
: `/assets/romm/resources/${item.path_cover_s}`
"
min-height="150"
/>
</v-avatar>
</template>
<template v-slot:item.name="{ item }">
<span>
{{ item.name }}
</span>
</template>
<template v-slot:item.file_name="{ item }">
<span>
{{ item.file_name }}
</span>
</template>
<template v-slot:item.file_size_bytes="{ item }">
<span>
{{ formatBytes(item.file_size_bytes) }}
@@ -171,5 +195,31 @@ function rowClick(_: Event, row: any) {
<admin-menu :rom="item" />
</v-menu>
</template>
<template v-slot:bottom>
<v-divider class="border-opacity-25" />
<v-row no-gutters class="pt-2 px-6 align-center">
<v-col cols="11" class="px-6">
<v-pagination
class="mr-6"
rounded="0"
:show-first-last-page="true"
active-color="romm-accent-1"
v-model="page"
:length="pageCount"
></v-pagination>
</v-col>
<v-col>
<v-select
label="Roms per page"
density="compact"
variant="outlined"
:items="PER_PAGE_OPTIONS"
v-model="romsPerPage"
hide-details
/>
</v-col>
</v-row>
</template>
</v-data-table>
</template>

View File

@@ -32,7 +32,7 @@ async function uploadRoms({
async function getRoms({
platformId = null,
size = 60,
size = 5000,
cursor = "",
searchTerm = "",
orderBy = "name",

View File

@@ -26,6 +26,7 @@ export default defineStore("roms", {
cursor: "" as string | null,
searchCursor: "" as string | null,
selecting: false,
itemsPerBatch: 72,
}),
getters: {

View File

@@ -68,6 +68,7 @@ export type Events = {
filter: null;
filterBarShow: null;
filterBarReset: null;
updateDataTablePages: null;
sortBarShow: null;
romUpdated: DetailedRom;
};

View File

@@ -48,14 +48,6 @@ export const views: Record<
export const defaultAvatarPath = "/assets/default/user.png";
export function toTop() {
window.scrollTo({
top: 0,
left: 0,
behavior: "smooth",
});
}
export function normalizeString(s: string) {
return s
.toLowerCase()

View File

@@ -42,6 +42,7 @@ emitter?.on("showEmulation", () => {
showEmulation.value = !showEmulation.value;
tab.value = showEmulation.value ? "emulation" : "details";
});
const noRomError = ref(false);
async function fetchDetails() {
if (!route.params.rom) return;
@@ -52,6 +53,7 @@ async function fetchDetails() {
rom.value = response.data;
})
.catch((error) => {
console.log(error);
emitter?.emit("snackbarShow", {
msg: error.response.data.detail,
@@ -69,6 +71,7 @@ async function fetchDetails() {
platform.value = response.data;
})
.catch((error) => {
noRomError.value = true;
console.log(error);
emitter?.emit("snackbarShow", {
msg: error.response.data.detail,
@@ -269,6 +272,15 @@ watch(
</template>
</v-row>
</template>
<template v-if="noRomError">
<v-empty-state
headline="Whoops, 404"
title="Game not found"
text="The game you were looking for does not exist"
icon="mdi-disc-alert"
></v-empty-state>
</template>
</template>
<style scoped>

View File

@@ -11,7 +11,7 @@ import storeRoms from "@/stores/roms";
import storePlatforms from "@/stores/platforms";
import type { Events } from "@/types/emitter";
import type { RomSelectEvent } from "@/types/rom";
import { normalizeString, toTop, views } from "@/utils";
import { normalizeString, views } from "@/utils";
import type { Emitter } from "mitt";
import { storeToRefs } from "pinia";
import { inject, onBeforeUnmount, onMounted, ref } from "vue";
@@ -34,7 +34,10 @@ const {
cursor,
searchCursor,
platformID,
itemsPerBatch,
} = storeToRefs(romsStore);
const itemsShown = ref(itemsPerBatch.value);
const noPlatformError = ref(false);
// Event listeners bus
const emitter = inject<Emitter<Events>>("emitter");
@@ -89,7 +92,9 @@ async function fetchRoms() {
color: "red",
timeout: 4000,
});
console.error(`Couldn't fetch roms for platform ID ${platformID.value}: ${error}`);
console.error(
`Couldn't fetch roms for platform ID ${platformID.value}: ${error}`
);
})
.finally(() => {
gettingRoms.value = false;
@@ -108,6 +113,7 @@ async function onFilterChange() {
return;
}
await fetchRoms();
emitter?.emit("updateDataTablePages", null);
}
function selectRom({ event, index, selected }: RomSelectEvent) {
@@ -168,6 +174,16 @@ function resetGallery() {
romsStore.reset();
scrolledToTop.value = true;
galleryFilterStore.reset();
itemsShown.value = itemsPerBatch.value;
}
function scrollToTop() {
window.scrollTo({
top: 0,
left: 0,
behavior: "smooth",
});
scrolledToTop.value = true;
}
function onScroll() {
@@ -175,11 +191,12 @@ function onScroll() {
const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
scrolledToTop.value = scrollTop === 0;
if (!cursor.value && !searchCursor.value) return;
// if (!cursor.value && !searchCursor.value) return;
const scrollOffset = 60;
const scrollOffset = 1000;
if (scrollTop + clientHeight + scrollOffset >= scrollHeight) {
await fetchRoms();
// await fetchRoms();
itemsShown.value = itemsShown.value + itemsPerBatch.value;
setFilters();
}
}, 100);
@@ -188,13 +205,20 @@ function onScroll() {
onMounted(async () => {
const storedPlatformID = romsStore.platformID;
const platformID = Number(route.params.platform);
romsStore.setPlatformID(platformID);
const platform = platforms.get(platformID);
if (!platform) {
const { data } = await platformApi.getPlatform(platformID)
platforms.add(data);
// const { data } =
await platformApi
.getPlatform(platformID)
.then((data) => {
platforms.add(data.data);
})
.catch((error) => {
noPlatformError.value = true;
});
}
// If platform is different, reset store and fetch roms
@@ -227,13 +251,13 @@ onBeforeRouteUpdate(async (to, _) => {
// Triggers when change query param of the same route
// Reset store if switching to another platform
resetGallery();
const platformID = Number(to.params.platform);
romsStore.setPlatformID(platformID);
const platform = platforms.get(platformID);
if (!platform) {
const { data } = await platformApi.getPlatform(platformID)
const { data } = await platformApi.getPlatform(platformID);
platforms.add(data);
}
@@ -248,6 +272,7 @@ onBeforeRouteUpdate(async (to, _) => {
<template v-if="filteredRoms.length > 0">
<v-row class="pa-1" no-gutters>
<!-- Gallery cards view -->
<!-- v-show instead of v-if to avoid recalculate -->
<v-col
class="pa-1"
v-if="galleryViewStore.current != 2"
@@ -257,7 +282,7 @@ onBeforeRouteUpdate(async (to, _) => {
:md="views[galleryViewStore.current]['size-md']"
:lg="views[galleryViewStore.current]['size-lg']"
:xl="views[galleryViewStore.current]['size-xl']"
v-for="rom in filteredRoms"
v-for="rom in filteredRoms.slice(0, itemsShown)"
:key="rom.id"
>
<game-card
@@ -276,50 +301,62 @@ onBeforeRouteUpdate(async (to, _) => {
</v-row>
</template>
<template v-if="noPlatformError">
<v-empty-state
headline="Whoops, 404"
title="Platform not found"
text="The platform you were looking for does not exist"
icon="mdi-controller-off"
></v-empty-state>
</template>
<v-layout-item
class="text-end"
v-show="!scrolledToTop"
class="text-end pr-2"
:model-value="true"
position="bottom"
size="88"
size="65"
>
<div class="ma-4">
<v-scroll-y-reverse-transition>
<v-btn
id="scrollToTop"
v-show="!scrolledToTop"
color="primary"
elevation="8"
icon
class="mr-2"
size="large"
@click="toTop()"
><v-icon color="romm-accent-1">mdi-chevron-up</v-icon></v-btn
<v-row no-gutters>
<v-col>
<v-scroll-y-reverse-transition>
<v-btn
id="scrollToTop"
color="primary"
elevation="8"
icon
class="ml-2"
size="large"
@click="scrollToTop()"
><v-icon color="romm-accent-1">mdi-chevron-up</v-icon></v-btn
>
</v-scroll-y-reverse-transition>
<v-menu
location="top"
v-model="fabMenu"
:transition="
fabMenu ? 'scroll-y-reverse-transition' : 'scroll-y-transition'
"
>
</v-scroll-y-reverse-transition>
<v-menu
location="top"
v-model="fabMenu"
:transition="
fabMenu ? 'scroll-y-reverse-transition' : 'scroll-y-transition'
"
>
<template v-slot:activator="{ props }">
<v-fab-transition>
<v-btn
v-show="romsStore._selectedIDs.length > 0"
color="romm-accent-1"
v-bind="props"
elevation="8"
icon
size="large"
>{{ romsStore._selectedIDs.length }}</v-btn
>
</v-fab-transition>
</template>
<template v-slot:activator="{ props }">
<v-fab-transition>
<v-btn
v-show="romsStore._selectedIDs.length > 0"
color="romm-accent-1"
v-bind="props"
elevation="8"
class="ml-2"
icon
size="large"
>{{ romsStore._selectedIDs.length }}</v-btn
>
</v-fab-transition>
</template>
<fab-menu />
</v-menu>
</div>
<fab-menu />
</v-menu>
</v-col>
</v-row>
</v-layout-item>
</template>