mirror of
https://github.com/rommapp/romm.git
synced 2026-02-19 07:50:57 +01:00
working just to letter with pagination
This commit is contained in:
@@ -5,7 +5,7 @@ from datetime import datetime, timezone
|
||||
from io import BytesIO
|
||||
from shutil import rmtree
|
||||
from stat import S_IFREG
|
||||
from typing import Any
|
||||
from typing import Any, TypeVar
|
||||
from urllib.parse import quote
|
||||
from zipfile import ZIP_DEFLATED, ZIP_STORED, ZipFile, ZipInfo
|
||||
|
||||
@@ -50,6 +50,8 @@ from utils.hashing import crc32_to_hex
|
||||
from utils.nginx import FileRedirectResponse, ZipContentLine, ZipResponse
|
||||
from utils.router import APIRouter
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/roms",
|
||||
tags=["roms"],
|
||||
@@ -118,6 +120,10 @@ async def add_rom(request: Request):
|
||||
return Response(status_code=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class CustomLimitOffsetPage(LimitOffsetPage[T]):
|
||||
char_index: dict[str, int]
|
||||
|
||||
|
||||
@protected_route(router.get, "", [Scope.ROMS_READ])
|
||||
def get_roms(
|
||||
request: Request,
|
||||
@@ -139,7 +145,7 @@ def get_roms(
|
||||
selected_status: str | None = None,
|
||||
selected_region: str | None = None,
|
||||
selected_language: str | None = None,
|
||||
) -> LimitOffsetPage[SimpleRomSchema]:
|
||||
) -> CustomLimitOffsetPage[SimpleRomSchema]:
|
||||
"""Get roms endpoint
|
||||
|
||||
Args:
|
||||
@@ -167,12 +173,14 @@ def get_roms(
|
||||
list[RomSchema | SimpleRomSchema]: List of ROMs stored in the database
|
||||
"""
|
||||
|
||||
# Get the base roms query
|
||||
query = db_rom_handler.get_roms_query(
|
||||
user_id=request.user.id,
|
||||
order_by=order_by.lower(),
|
||||
order_dir=order_dir.lower(),
|
||||
)
|
||||
|
||||
# Filter down the query
|
||||
query = db_rom_handler.filter_roms(
|
||||
query=query,
|
||||
user_id=request.user.id,
|
||||
@@ -194,6 +202,10 @@ def get_roms(
|
||||
selected_language=selected_language,
|
||||
)
|
||||
|
||||
# Get the char index for the roms
|
||||
char_index = db_rom_handler.get_char_index(query=query)
|
||||
char_index_dict = {char: index for (char, index) in char_index}
|
||||
|
||||
with sync_session.begin() as session:
|
||||
return paginate(
|
||||
session,
|
||||
@@ -201,6 +213,7 @@ def get_roms(
|
||||
transformer=lambda items: [
|
||||
SimpleRomSchema.from_orm_with_request(i, request) for i in items
|
||||
],
|
||||
additional_data={"char_index": char_index_dict},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import functools
|
||||
from collections.abc import Iterable
|
||||
from typing import Sequence
|
||||
from typing import List, Sequence, Tuple
|
||||
|
||||
from config import ROMM_DB_DRIVER
|
||||
from decorators.database import begin_session
|
||||
from models.collection import Collection, VirtualCollection
|
||||
from models.rom import Rom, RomFile, RomMetadata, RomUser
|
||||
from sqlalchemy import and_, delete, func, or_, select, text, update
|
||||
from sqlalchemy import Row, and_, delete, func, or_, select, text, update
|
||||
from sqlalchemy.orm import Query, Session, selectinload
|
||||
|
||||
from .base_handler import DBBaseHandler
|
||||
@@ -386,6 +386,26 @@ class DBRomsHandler(DBBaseHandler):
|
||||
)
|
||||
return session.scalars(roms).all()
|
||||
|
||||
@begin_session
|
||||
def get_char_index(
|
||||
self, query: Query, session: Session = None
|
||||
) -> List[Row[Tuple[str, int]]]:
|
||||
# Get the row number and first letter for each item
|
||||
subquery = query.add_columns(
|
||||
func.lower(func.substring(Rom.name, 1, 1)).label("letter"),
|
||||
func.row_number().over(order_by=Rom.name).label("position"),
|
||||
).subquery()
|
||||
|
||||
# Get the minimum position for each letter
|
||||
return (
|
||||
session.query(
|
||||
subquery.c.letter, func.min(subquery.c.position - 1).label("position")
|
||||
)
|
||||
.group_by(subquery.c.letter)
|
||||
.order_by(subquery.c.letter)
|
||||
.all()
|
||||
)
|
||||
|
||||
@begin_session
|
||||
@with_details
|
||||
def get_rom_by_fs_name(
|
||||
|
||||
2
frontend/src/__generated__/index.ts
generated
2
frontend/src/__generated__/index.ts
generated
@@ -15,6 +15,7 @@ export type { Body_update_rom_api_roms__id__put } from './models/Body_update_rom
|
||||
export type { Body_update_user_api_users__id__put } from './models/Body_update_user_api_users__id__put';
|
||||
export type { CollectionSchema } from './models/CollectionSchema';
|
||||
export type { ConfigResponse } from './models/ConfigResponse';
|
||||
export type { CustomLimitOffsetPage_SimpleRomSchema_ } from './models/CustomLimitOffsetPage_SimpleRomSchema_';
|
||||
export type { DetailedRomSchema } from './models/DetailedRomSchema';
|
||||
export type { EmulationDict } from './models/EmulationDict';
|
||||
export type { FilesystemDict } from './models/FilesystemDict';
|
||||
@@ -25,7 +26,6 @@ export type { HTTPValidationError } from './models/HTTPValidationError';
|
||||
export type { IGDBAgeRating } from './models/IGDBAgeRating';
|
||||
export type { IGDBMetadataPlatform } from './models/IGDBMetadataPlatform';
|
||||
export type { IGDBRelatedGame } from './models/IGDBRelatedGame';
|
||||
export type { LimitOffsetPage_SimpleRomSchema_ } from './models/LimitOffsetPage_SimpleRomSchema_';
|
||||
export type { MessageResponse } from './models/MessageResponse';
|
||||
export type { MetadataSourcesDict } from './models/MetadataSourcesDict';
|
||||
export type { MobyMetadataPlatform } from './models/MobyMetadataPlatform';
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { SimpleRomSchema } from './SimpleRomSchema';
|
||||
export type LimitOffsetPage_SimpleRomSchema_ = {
|
||||
export type CustomLimitOffsetPage_SimpleRomSchema_ = {
|
||||
items: Array<SimpleRomSchema>;
|
||||
total: (number | null);
|
||||
limit: (number | null);
|
||||
offset: (number | null);
|
||||
char_index: Record<string, number>;
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import FilterTextField from "@/components/Gallery/AppBar/common/FilterTextField.
|
||||
import GalleryViewBtn from "@/components/Gallery/AppBar/common/GalleryViewBtn.vue";
|
||||
import RAvatar from "@/components/common/Collection/RAvatar.vue";
|
||||
import SelectingBtn from "@/components/Gallery/AppBar/common/SelectingBtn.vue";
|
||||
import CharIndexBar from "@/components/Gallery/AppBar/common/CharIndexBar.vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
import storeNavigation from "@/stores/navigation";
|
||||
import storeRoms from "@/stores/roms";
|
||||
@@ -46,6 +47,7 @@ const { currentCollection } = storeToRefs(romsStore);
|
||||
</template>
|
||||
</v-app-bar>
|
||||
|
||||
<char-index-bar />
|
||||
<collection-info-drawer />
|
||||
<filter-drawer />
|
||||
</template>
|
||||
|
||||
@@ -7,6 +7,7 @@ import FilterDrawer from "@/components/Gallery/AppBar/common/FilterDrawer/Base.v
|
||||
import FilterTextField from "@/components/Gallery/AppBar/common/FilterTextField.vue";
|
||||
import GalleryViewBtn from "@/components/Gallery/AppBar/common/GalleryViewBtn.vue";
|
||||
import SelectingBtn from "@/components/Gallery/AppBar/common/SelectingBtn.vue";
|
||||
import CharIndexBar from "@/components/Gallery/AppBar/common/CharIndexBar.vue";
|
||||
import PlatformIcon from "@/components/common/Platform/Icon.vue";
|
||||
import storeNavigation from "@/stores/navigation";
|
||||
import storeRoms from "@/stores/roms";
|
||||
@@ -51,6 +52,7 @@ const { activePlatformInfoDrawer } = storeToRefs(navigationStore);
|
||||
</template>
|
||||
</v-app-bar>
|
||||
|
||||
<char-index-bar />
|
||||
<platform-info-drawer />
|
||||
<filter-drawer hide-platforms />
|
||||
<firmware-drawer />
|
||||
@@ -60,15 +62,18 @@ const { activePlatformInfoDrawer } = storeToRefs(navigationStore);
|
||||
.gallery-app-bar-desktop {
|
||||
width: calc(100% - 76px) !important;
|
||||
}
|
||||
|
||||
.gallery-app-bar-mobile {
|
||||
width: calc(100% - 16px) !important;
|
||||
}
|
||||
|
||||
.platform-icon {
|
||||
transition:
|
||||
filter 0.15s ease-in-out,
|
||||
transform 0.15s ease-in-out;
|
||||
filter: drop-shadow(0px 0px 1px rgba(var(--v-theme-primary)));
|
||||
}
|
||||
|
||||
.platform-icon:hover,
|
||||
.platform-icon.active {
|
||||
filter: drop-shadow(0px 0px 3px rgba(var(--v-theme-primary)));
|
||||
|
||||
@@ -4,6 +4,7 @@ import FilterDrawer from "@/components/Gallery/AppBar/common/FilterDrawer/Base.v
|
||||
import GalleryViewBtn from "@/components/Gallery/AppBar/common/GalleryViewBtn.vue";
|
||||
import SearchTextField from "@/components/Gallery/AppBar/Search/SearchTextField.vue";
|
||||
import SelectingBtn from "@/components/Gallery/AppBar/common/SelectingBtn.vue";
|
||||
import CharIndexBar from "@/components/Gallery/AppBar/common/CharIndexBar.vue";
|
||||
import SearchBtn from "@/components/Gallery/AppBar/Search/SearchBtn.vue";
|
||||
import { useDisplay } from "vuetify";
|
||||
|
||||
@@ -33,6 +34,7 @@ const { xs, smAndDown } = useDisplay();
|
||||
</template>
|
||||
</v-app-bar>
|
||||
|
||||
<char-index-bar />
|
||||
<filter-drawer />
|
||||
</template>
|
||||
|
||||
|
||||
@@ -48,8 +48,9 @@ async function refetchRoms() {
|
||||
// Update URL with search term
|
||||
router.replace({ query: { search: searchTerm.value } });
|
||||
|
||||
romsStore.resetPagination();
|
||||
romsStore
|
||||
.refetchRoms(galleryFilterStore)
|
||||
.fetchRoms(galleryFilterStore, false)
|
||||
.catch((error) => {
|
||||
emitter?.emit("snackbarShow", {
|
||||
msg: `Couldn't fetch roms: ${error}`,
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
<script setup lang="ts">
|
||||
import storeRoms from "@/stores/roms";
|
||||
import storeGalleryFilter from "@/stores/galleryFilter";
|
||||
import type { Events } from "@/types/emitter";
|
||||
import type { Emitter } from "mitt";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { inject, watch } from "vue";
|
||||
|
||||
const romsStore = storeRoms();
|
||||
const galleryFilterStore = storeGalleryFilter();
|
||||
const emitter = inject<Emitter<Events>>("emitter");
|
||||
|
||||
const { characterIndex, selectedCharacter, fetchingRoms } =
|
||||
storeToRefs(romsStore);
|
||||
|
||||
async function fetchRoms() {
|
||||
if (fetchingRoms.value) return;
|
||||
|
||||
emitter?.emit("showLoadingDialog", {
|
||||
loading: true,
|
||||
scrim: false,
|
||||
});
|
||||
|
||||
romsStore
|
||||
.fetchRoms(galleryFilterStore, false)
|
||||
.then(() => {
|
||||
emitter?.emit("showLoadingDialog", {
|
||||
loading: false,
|
||||
scrim: false,
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
emitter?.emit("snackbarShow", {
|
||||
msg: `Couldn't fetch roms: ${error}`,
|
||||
icon: "mdi-close-circle",
|
||||
color: "red",
|
||||
timeout: 4000,
|
||||
});
|
||||
emitter?.emit("showLoadingDialog", {
|
||||
loading: false,
|
||||
scrim: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
watch(
|
||||
selectedCharacter,
|
||||
() => {
|
||||
if (!selectedCharacter.value) return;
|
||||
romsStore.resetPagination();
|
||||
romsStore.fetchOffset = characterIndex.value[selectedCharacter.value];
|
||||
fetchRoms();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-toolbar
|
||||
elevation="0"
|
||||
density="compact"
|
||||
rounded
|
||||
height="100%"
|
||||
class="position-fixed bg-surface mt-4 char-index-toolbar"
|
||||
>
|
||||
<v-tabs v-model="selectedCharacter" bg-color="surface" direction="vertical">
|
||||
<v-tab
|
||||
v-for="char in Object.keys(characterIndex)"
|
||||
:key="char"
|
||||
:value="char"
|
||||
class="py-4"
|
||||
>
|
||||
{{ char }}
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
</v-toolbar>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.char-index-toolbar {
|
||||
right: 8px;
|
||||
z-index: 1010;
|
||||
transform: translateY(0px);
|
||||
height: fit-content;
|
||||
max-height: calc(100vh - 74px);
|
||||
width: 48px;
|
||||
overflow-y: scroll;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
</style>
|
||||
@@ -54,7 +54,8 @@ const emitter = inject<Emitter<Events>>("emitter");
|
||||
emitter?.on("filter", onFilterChange);
|
||||
|
||||
async function onFilterChange() {
|
||||
romsStore.refetchRoms(galleryFilterStore);
|
||||
romsStore.resetPagination();
|
||||
romsStore.fetchRoms(galleryFilterStore, false);
|
||||
emitter?.emit("updateDataTablePages", null);
|
||||
}
|
||||
|
||||
|
||||
@@ -272,6 +272,7 @@ function onDownload() {
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
padding-right: 72px !important;
|
||||
}
|
||||
.sticky-bottom * {
|
||||
pointer-events: auto; /* Re-enables pointer events for all child elements */
|
||||
|
||||
@@ -11,7 +11,7 @@ const { currentView } = storeToRefs(galleryViewStore);
|
||||
<template>
|
||||
<v-row no-gutters>
|
||||
<v-col>
|
||||
<v-row v-if="currentView != 2" no-gutters class="mx-1 mt-3">
|
||||
<v-row v-if="currentView != 2" no-gutters class="mx-1 mt-3 mr-15">
|
||||
<v-col
|
||||
v-for="_ in 60"
|
||||
class="pa-1 align-self-end"
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { DetailedRom, SimpleRom } from "@/stores/roms";
|
||||
import { getDownloadPath, getStatusKeyForText } from "@/utils";
|
||||
import type { AxiosProgressEvent } from "axios";
|
||||
import storeHeartbeat from "@/stores/heartbeat";
|
||||
import { type LimitOffsetPage_SimpleRomSchema_ as GetRomsResponse } from "@/__generated__/models/LimitOffsetPage_SimpleRomSchema_";
|
||||
import { type CustomLimitOffsetPage_SimpleRomSchema_ as GetRomsResponse } from "@/__generated__/models/CustomLimitOffsetPage_SimpleRomSchema_";
|
||||
|
||||
export const romApi = api;
|
||||
|
||||
|
||||
@@ -30,6 +30,8 @@ const defaultRomsState = {
|
||||
fetchLimit: 72,
|
||||
fetchOffset: 0,
|
||||
fetchTotalRoms: 0,
|
||||
characterIndex: {} as Record<string, number>,
|
||||
selectedCharacter: null as string | null,
|
||||
};
|
||||
|
||||
export default defineStore("roms", {
|
||||
@@ -104,7 +106,7 @@ export default defineStore("roms", {
|
||||
setCurrentVirtualCollection(collection: VirtualCollection | null) {
|
||||
this.currentVirtualCollection = collection;
|
||||
},
|
||||
fetchRoms(galleryFilter: GalleryFilterStore) {
|
||||
fetchRoms(galleryFilter: GalleryFilterStore, concat = true) {
|
||||
if (this.fetchingRoms) return Promise.resolve();
|
||||
this.fetchingRoms = true;
|
||||
|
||||
@@ -121,48 +123,13 @@ export default defineStore("roms", {
|
||||
limit: this.fetchLimit,
|
||||
offset: this.fetchOffset,
|
||||
})
|
||||
.then(({ data: { items, offset, total } }) => {
|
||||
.then(({ data: { items, offset, total, char_index } }) => {
|
||||
if (offset !== null) this.fetchOffset = offset + this.fetchLimit;
|
||||
if (total !== null) this.fetchTotalRoms = total;
|
||||
|
||||
// These need to happen in exactly this order
|
||||
this.allRoms = this.allRoms.concat(items);
|
||||
this._reorder();
|
||||
|
||||
resolve(items);
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
})
|
||||
.finally(() => {
|
||||
this.fetchingRoms = false;
|
||||
});
|
||||
});
|
||||
},
|
||||
refetchRoms(galleryFilter: GalleryFilterStore) {
|
||||
if (this.fetchingRoms) return Promise.resolve();
|
||||
this.fetchingRoms = true;
|
||||
this.resetPagination();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
romApi
|
||||
.getRoms({
|
||||
...galleryFilter.$state,
|
||||
platformId:
|
||||
this.currentPlatform?.id ??
|
||||
galleryFilter.selectedPlatform?.id ??
|
||||
null,
|
||||
collectionId: this.currentCollection?.id ?? null,
|
||||
virtualCollectionId: this.currentVirtualCollection?.id ?? null,
|
||||
limit: this.fetchLimit,
|
||||
offset: this.fetchOffset,
|
||||
})
|
||||
.then(({ data: { items, offset, total } }) => {
|
||||
if (offset !== null) this.fetchOffset = offset + this.fetchLimit;
|
||||
if (total !== null) this.fetchTotalRoms = total;
|
||||
|
||||
// These need to happen in exactly this order
|
||||
this.allRoms = items;
|
||||
this.allRoms = concat ? this.allRoms.concat(items) : items;
|
||||
this.characterIndex = char_index;
|
||||
this._reorder();
|
||||
|
||||
resolve(items);
|
||||
|
||||
@@ -287,7 +287,7 @@ onBeforeUnmount(() => {
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="filteredRoms.length > 0">
|
||||
<v-row v-if="currentView != 2" class="mx-1 mt-3" no-gutters>
|
||||
<v-row v-if="currentView != 2" class="mx-1 mt-3 mr-15" no-gutters>
|
||||
<!-- Gallery cards view -->
|
||||
<!-- v-show instead of v-if to avoid recalculate on view change -->
|
||||
<v-col
|
||||
|
||||
@@ -229,7 +229,7 @@ onBeforeUnmount(() => {
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="filteredRoms.length > 0">
|
||||
<v-row v-if="currentView != 2" class="pb-2 mx-1 mt-3" no-gutters>
|
||||
<v-row v-if="currentView != 2" class="mx-1 mt-3 mr-15" no-gutters>
|
||||
<!-- Gallery cards view -->
|
||||
<v-col
|
||||
v-for="rom in filteredRoms"
|
||||
|
||||
@@ -148,7 +148,7 @@ onBeforeUnmount(() => {
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="filteredRoms.length > 0">
|
||||
<v-row v-if="currentView != 2" class="mx-1 mt-3" no-gutters>
|
||||
<v-row v-if="currentView != 2" class="mx-1 mt-3 mr-15" no-gutters>
|
||||
<!-- Gallery cards view -->
|
||||
<v-col
|
||||
v-for="rom in filteredRoms"
|
||||
|
||||
Reference in New Issue
Block a user