mirror of
https://github.com/rommapp/romm.git
synced 2026-02-18 00:27:41 +01:00
Merge pull request #894 from rommapp/refactor/gallery_render
Refactor gallery render
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -32,7 +32,7 @@ async function uploadRoms({
|
||||
|
||||
async function getRoms({
|
||||
platformId = null,
|
||||
size = 60,
|
||||
size = 5000,
|
||||
cursor = "",
|
||||
searchTerm = "",
|
||||
orderBy = "name",
|
||||
|
||||
@@ -26,6 +26,7 @@ export default defineStore("roms", {
|
||||
cursor: "" as string | null,
|
||||
searchCursor: "" as string | null,
|
||||
selecting: false,
|
||||
itemsPerBatch: 72,
|
||||
}),
|
||||
|
||||
getters: {
|
||||
|
||||
1
frontend/src/types/emitter.d.ts
vendored
1
frontend/src/types/emitter.d.ts
vendored
@@ -68,6 +68,7 @@ export type Events = {
|
||||
filter: null;
|
||||
filterBarShow: null;
|
||||
filterBarReset: null;
|
||||
updateDataTablePages: null;
|
||||
sortBarShow: null;
|
||||
romUpdated: DetailedRom;
|
||||
};
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user