From 35dfedd22f5c88264e2dacb47ea4b3840101f63a Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Fri, 21 Mar 2025 22:44:47 -0400 Subject: [PATCH] working just to letter with pagination --- backend/endpoints/rom.py | 17 +++- backend/handler/database/roms_handler.py | 24 ++++- frontend/src/__generated__/index.ts | 2 +- ...CustomLimitOffsetPage_SimpleRomSchema_.ts} | 3 +- .../Gallery/AppBar/Collection/Base.vue | 2 + .../Gallery/AppBar/Platform/Base.vue | 5 ++ .../components/Gallery/AppBar/Search/Base.vue | 2 + .../Gallery/AppBar/Search/SearchTextField.vue | 3 +- .../Gallery/AppBar/common/CharIndexBar.vue | 90 +++++++++++++++++++ .../AppBar/common/FilterDrawer/Base.vue | 3 +- .../src/components/Gallery/FabOverlay.vue | 1 + frontend/src/components/Gallery/Skeleton.vue | 2 +- frontend/src/services/api/rom.ts | 2 +- frontend/src/stores/roms.ts | 45 ++-------- frontend/src/views/Gallery/Collection.vue | 2 +- frontend/src/views/Gallery/Platform.vue | 2 +- frontend/src/views/Gallery/Search.vue | 2 +- 17 files changed, 155 insertions(+), 52 deletions(-) rename frontend/src/__generated__/models/{LimitOffsetPage_SimpleRomSchema_.ts => CustomLimitOffsetPage_SimpleRomSchema_.ts} (76%) create mode 100644 frontend/src/components/Gallery/AppBar/common/CharIndexBar.vue diff --git a/backend/endpoints/rom.py b/backend/endpoints/rom.py index b97198183..1b6f3ba90 100644 --- a/backend/endpoints/rom.py +++ b/backend/endpoints/rom.py @@ -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}, ) diff --git a/backend/handler/database/roms_handler.py b/backend/handler/database/roms_handler.py index 5d0efc61a..c4004afe2 100644 --- a/backend/handler/database/roms_handler.py +++ b/backend/handler/database/roms_handler.py @@ -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( diff --git a/frontend/src/__generated__/index.ts b/frontend/src/__generated__/index.ts index d55d0d45f..b02ef4a62 100644 --- a/frontend/src/__generated__/index.ts +++ b/frontend/src/__generated__/index.ts @@ -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'; diff --git a/frontend/src/__generated__/models/LimitOffsetPage_SimpleRomSchema_.ts b/frontend/src/__generated__/models/CustomLimitOffsetPage_SimpleRomSchema_.ts similarity index 76% rename from frontend/src/__generated__/models/LimitOffsetPage_SimpleRomSchema_.ts rename to frontend/src/__generated__/models/CustomLimitOffsetPage_SimpleRomSchema_.ts index 14d1b5802..c75b087ca 100644 --- a/frontend/src/__generated__/models/LimitOffsetPage_SimpleRomSchema_.ts +++ b/frontend/src/__generated__/models/CustomLimitOffsetPage_SimpleRomSchema_.ts @@ -3,10 +3,11 @@ /* tslint:disable */ /* eslint-disable */ import type { SimpleRomSchema } from './SimpleRomSchema'; -export type LimitOffsetPage_SimpleRomSchema_ = { +export type CustomLimitOffsetPage_SimpleRomSchema_ = { items: Array; total: (number | null); limit: (number | null); offset: (number | null); + char_index: Record; }; diff --git a/frontend/src/components/Gallery/AppBar/Collection/Base.vue b/frontend/src/components/Gallery/AppBar/Collection/Base.vue index 787c7a308..7019fbaf2 100644 --- a/frontend/src/components/Gallery/AppBar/Collection/Base.vue +++ b/frontend/src/components/Gallery/AppBar/Collection/Base.vue @@ -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); + diff --git a/frontend/src/components/Gallery/AppBar/Platform/Base.vue b/frontend/src/components/Gallery/AppBar/Platform/Base.vue index 2d07eb01a..719b706bc 100644 --- a/frontend/src/components/Gallery/AppBar/Platform/Base.vue +++ b/frontend/src/components/Gallery/AppBar/Platform/Base.vue @@ -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); + @@ -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))); diff --git a/frontend/src/components/Gallery/AppBar/Search/Base.vue b/frontend/src/components/Gallery/AppBar/Search/Base.vue index ce461b2a6..ad17421c5 100644 --- a/frontend/src/components/Gallery/AppBar/Search/Base.vue +++ b/frontend/src/components/Gallery/AppBar/Search/Base.vue @@ -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(); + diff --git a/frontend/src/components/Gallery/AppBar/Search/SearchTextField.vue b/frontend/src/components/Gallery/AppBar/Search/SearchTextField.vue index 7d11808a8..2e3e03072 100644 --- a/frontend/src/components/Gallery/AppBar/Search/SearchTextField.vue +++ b/frontend/src/components/Gallery/AppBar/Search/SearchTextField.vue @@ -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}`, diff --git a/frontend/src/components/Gallery/AppBar/common/CharIndexBar.vue b/frontend/src/components/Gallery/AppBar/common/CharIndexBar.vue new file mode 100644 index 000000000..fa8c281e0 --- /dev/null +++ b/frontend/src/components/Gallery/AppBar/common/CharIndexBar.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/frontend/src/components/Gallery/AppBar/common/FilterDrawer/Base.vue b/frontend/src/components/Gallery/AppBar/common/FilterDrawer/Base.vue index 071fc6c6b..65cd43204 100644 --- a/frontend/src/components/Gallery/AppBar/common/FilterDrawer/Base.vue +++ b/frontend/src/components/Gallery/AppBar/common/FilterDrawer/Base.vue @@ -54,7 +54,8 @@ const emitter = inject>("emitter"); emitter?.on("filter", onFilterChange); async function onFilterChange() { - romsStore.refetchRoms(galleryFilterStore); + romsStore.resetPagination(); + romsStore.fetchRoms(galleryFilterStore, false); emitter?.emit("updateDataTablePages", null); } diff --git a/frontend/src/components/Gallery/FabOverlay.vue b/frontend/src/components/Gallery/FabOverlay.vue index 96e4bfeef..8bb320475 100644 --- a/frontend/src/components/Gallery/FabOverlay.vue +++ b/frontend/src/components/Gallery/FabOverlay.vue @@ -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 */ diff --git a/frontend/src/components/Gallery/Skeleton.vue b/frontend/src/components/Gallery/Skeleton.vue index c346fcee0..636e522cd 100644 --- a/frontend/src/components/Gallery/Skeleton.vue +++ b/frontend/src/components/Gallery/Skeleton.vue @@ -11,7 +11,7 @@ const { currentView } = storeToRefs(galleryViewStore);