From 96240a86e9fab08c832d12a534c14945812285fa Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Thu, 15 Jan 2026 11:18:51 -0500 Subject: [PATCH] Add endpoint to fetch rom filters independent of query --- backend/endpoints/responses/rom.py | 11 ++ backend/endpoints/rom.py | 21 ++- backend/handler/database/roms_handler.py | 134 ++++++++++++------ frontend/src/__generated__/index.ts | 1 + .../__generated__/models/RomFiltersDict.ts | 15 ++ .../AppBar/common/FilterDrawer/Base.vue | 124 +++------------- .../LibraryManagement/Config/MissingGames.vue | 4 +- frontend/src/services/api/rom.ts | 6 + frontend/src/stores/roms.ts | 1 - 9 files changed, 162 insertions(+), 155 deletions(-) create mode 100644 frontend/src/__generated__/models/RomFiltersDict.ts diff --git a/backend/endpoints/responses/rom.py b/backend/endpoints/responses/rom.py index 1daf8b840..ccbe430c1 100644 --- a/backend/endpoints/responses/rom.py +++ b/backend/endpoints/responses/rom.py @@ -444,3 +444,14 @@ class DetailedRomSchema(RomSchema): @field_validator("user_screenshots") def sort_user_screenshots(cls, v: list[ScreenshotSchema]) -> list[ScreenshotSchema]: return sorted(v, key=lambda x: x.created_at, reverse=True) + + +class RomFiltersDict(TypedDict): + genres: list[str] + franchises: list[str] + companies: list[str] + game_modes: list[str] + age_ratings: list[str] + player_counts: list[str] + regions: list[str] + languages: list[str] diff --git a/backend/endpoints/rom.py b/backend/endpoints/rom.py index cfffdec02..0d7d2f358 100644 --- a/backend/endpoints/rom.py +++ b/backend/endpoints/rom.py @@ -43,6 +43,7 @@ from endpoints.responses import BulkOperationResponse from endpoints.responses.rom import ( DetailedRomSchema, RomFileSchema, + RomFiltersDict, RomUserSchema, SimpleRomSchema, UserNoteSchema, @@ -671,6 +672,24 @@ def get_rom_by_hash( return DetailedRomSchema.from_orm_with_request(rom, request) +@protected_route(router.get, "/filters", [Scope.ROMS_READ]) +async def get_rom_filters(request: Request) -> RomFiltersDict: + from handler.database import db_rom_handler + + filters = db_rom_handler.get_rom_filters() + + return RomFiltersDict( + genres=filters["genres"], + franchises=filters["franchises"], + companies=filters["companies"], + game_modes=filters["game_modes"], + age_ratings=filters["age_ratings"], + player_counts=filters["player_counts"], + regions=filters["regions"], + languages=filters["languages"], + ) + + @protected_route( router.get, "/{id}", @@ -1502,7 +1521,7 @@ async def update_rom_user( @protected_route( router.get, - "files/{id}", + "/files/{id}", [Scope.ROMS_READ], responses={status.HTTP_404_NOT_FOUND: {}}, ) diff --git a/backend/handler/database/roms_handler.py b/backend/handler/database/roms_handler.py index 1c995616c..25fd82b26 100644 --- a/backend/handler/database/roms_handler.py +++ b/backend/handler/database/roms_handler.py @@ -206,12 +206,12 @@ class DBRomsHandler(DBBaseHandler): def filter_by_platform_id(self, query: Query, platform_id: int): return query.filter(Rom.platform_id == platform_id) - def filter_by_platform_ids( + def _filter_by_platform_ids( self, query: Query, platform_ids: Sequence[int] ) -> Query: return query.filter(Rom.platform_id.in_(platform_ids)) - def filter_by_collection_id( + def _filter_by_collection_id( self, query: Query, session: Session, collection_id: int ): from . import db_collection_handler @@ -222,7 +222,7 @@ class DBRomsHandler(DBBaseHandler): return query.filter(Rom.id.in_(collection.rom_ids)) return query - def filter_by_virtual_collection_id( + def _filter_by_virtual_collection_id( self, query: Query, session: Session, virtual_collection_id: str ): from . import db_collection_handler @@ -235,7 +235,7 @@ class DBRomsHandler(DBBaseHandler): return query.filter(Rom.id.in_(v_collection.rom_ids)) return query - def filter_by_smart_collection_id( + def _filter_by_smart_collection_id( self, query: Query, session: Session, smart_collection_id: int, user_id: int ): from . import db_collection_handler @@ -250,7 +250,7 @@ class DBRomsHandler(DBBaseHandler): return query.filter(Rom.id.in_(smart_collection.rom_ids)) return query - def filter_by_search_term(self, query: Query, search_term: str): + def _filter_by_search_term(self, query: Query, search_term: str): return query.filter( or_( Rom.fs_name.ilike(f"%{search_term}%"), @@ -258,7 +258,7 @@ class DBRomsHandler(DBBaseHandler): ) ) - def filter_by_matched(self, query: Query, value: bool) -> Query: + def _filter_by_matched(self, query: Query, value: bool) -> Query: """Filter based on whether the rom is matched to a metadata provider. Args: @@ -278,7 +278,7 @@ class DBRomsHandler(DBBaseHandler): predicate = not_(predicate) return query.filter(predicate) - def filter_by_favorite( + def _filter_by_favorite( self, query: Query, session: Session, value: bool, user_id: int | None ) -> Query: """Filter based on whether the rom is in the user's favorites collection.""" @@ -300,21 +300,21 @@ class DBRomsHandler(DBBaseHandler): return query return query.filter(false()) - def filter_by_duplicate(self, query: Query, value: bool) -> Query: + def _filter_by_duplicate(self, query: Query, value: bool) -> Query: """Filter based on whether the rom has duplicates.""" predicate = Rom.sibling_roms.any() if not value: predicate = not_(predicate) return query.filter(predicate) - def filter_by_playable(self, query: Query, value: bool) -> Query: + def _filter_by_playable(self, query: Query, value: bool) -> Query: """Filter based on whether the rom is playable on supported platforms.""" predicate = Platform.slug.in_(EJS_SUPPORTED_PLATFORMS) if not value: predicate = not_(predicate) return query.join(Platform).filter(predicate) - def filter_by_last_played( + def _filter_by_last_played( self, query: Query, value: bool, user_id: int | None = None ) -> Query: """Filter based on whether the rom has a last played value for the user.""" @@ -328,19 +328,19 @@ class DBRomsHandler(DBBaseHandler): ) return query.filter(has_last_played) - def filter_by_has_ra(self, query: Query, value: bool) -> Query: + def _filter_by_has_ra(self, query: Query, value: bool) -> Query: predicate = Rom.ra_id.isnot(None) if not value: predicate = not_(predicate) return query.filter(predicate) - def filter_by_missing_from_fs(self, query: Query, value: bool) -> Query: + def _filter_by_missing_from_fs(self, query: Query, value: bool) -> Query: predicate = Rom.missing_from_fs.isnot(False) if not value: predicate = not_(predicate) return query.filter(predicate) - def filter_by_verified(self, query: Query): + def _filter_by_verified(self, query: Query): keys_to_check = [ "tosec_match", "mame_arcade_match", @@ -363,7 +363,7 @@ class DBRomsHandler(DBBaseHandler): or_(*(Rom.hasheous_metadata[key].as_boolean() for key in keys_to_check)) ) - def filter_by_genres( + def _filter_by_genres( self, query: Query, *, @@ -374,7 +374,7 @@ class DBRomsHandler(DBBaseHandler): op = json_array_contains_all if match_all else json_array_contains_any return query.filter(op(RomMetadata.genres, values, session=session)) - def filter_by_franchises( + def _filter_by_franchises( self, query: Query, *, @@ -385,7 +385,7 @@ class DBRomsHandler(DBBaseHandler): op = json_array_contains_all if match_all else json_array_contains_any return query.filter(op(RomMetadata.franchises, values, session=session)) - def filter_by_collections( + def _filter_by_collections( self, query: Query, *, @@ -396,7 +396,7 @@ class DBRomsHandler(DBBaseHandler): op = json_array_contains_all if match_all else json_array_contains_any return query.filter(op(RomMetadata.collections, values, session=session)) - def filter_by_companies( + def _filter_by_companies( self, query: Query, *, @@ -407,7 +407,7 @@ class DBRomsHandler(DBBaseHandler): op = json_array_contains_all if match_all else json_array_contains_any return query.filter(op(RomMetadata.companies, values, session=session)) - def filter_by_age_ratings( + def _filter_by_age_ratings( self, query: Query, *, @@ -418,7 +418,7 @@ class DBRomsHandler(DBBaseHandler): op = json_array_contains_all if match_all else json_array_contains_any return query.filter(op(RomMetadata.age_ratings, values, session=session)) - def filter_by_status(self, query: Query, statuses: Sequence[str]): + def _filter_by_status(self, query: Query, statuses: Sequence[str]): """Filter by one or more user statuses using OR logic.""" if not statuses: return query @@ -440,7 +440,7 @@ class DBRomsHandler(DBBaseHandler): return query.filter(or_(*status_filters), RomUser.hidden.is_(False)) - def filter_by_regions( + def _filter_by_regions( self, query: Query, *, @@ -451,7 +451,7 @@ class DBRomsHandler(DBBaseHandler): op = json_array_contains_all if match_all else json_array_contains_any return query.filter(op(Rom.regions, values, session=session)) - def filter_by_languages( + def _filter_by_languages( self, query: Query, *, @@ -462,7 +462,7 @@ class DBRomsHandler(DBBaseHandler): op = json_array_contains_all if match_all else json_array_contains_any return query.filter(op(Rom.languages, values, session=session)) - def filter_by_player_counts( + def _filter_by_player_counts( self, query: Query, *, @@ -516,52 +516,52 @@ class DBRomsHandler(DBBaseHandler): # Handle platform filtering - platform filtering always uses OR logic since ROMs belong to only one platform if platform_ids: - query = self.filter_by_platform_ids(query, platform_ids) + query = self._filter_by_platform_ids(query, platform_ids) if collection_id: - query = self.filter_by_collection_id(query, session, collection_id) + query = self._filter_by_collection_id(query, session, collection_id) if virtual_collection_id: - query = self.filter_by_virtual_collection_id( + query = self._filter_by_virtual_collection_id( query, session, virtual_collection_id ) if smart_collection_id and user_id: - query = self.filter_by_smart_collection_id( + query = self._filter_by_smart_collection_id( query, session, smart_collection_id, user_id ) if search_term: - query = self.filter_by_search_term(query, search_term) + query = self._filter_by_search_term(query, search_term) if matched is not None: - query = self.filter_by_matched(query, value=matched) + query = self._filter_by_matched(query, value=matched) if favorite is not None: - query = self.filter_by_favorite( + query = self._filter_by_favorite( query, session=session, value=favorite, user_id=user_id ) if duplicate is not None: - query = self.filter_by_duplicate(query, value=duplicate) + query = self._filter_by_duplicate(query, value=duplicate) if last_played is not None: - query = self.filter_by_last_played( + query = self._filter_by_last_played( query, value=last_played, user_id=user_id ) if playable is not None: - query = self.filter_by_playable(query, value=playable) + query = self._filter_by_playable(query, value=playable) if has_ra is not None: - query = self.filter_by_has_ra(query, value=has_ra) + query = self._filter_by_has_ra(query, value=has_ra) if missing is not None: - query = self.filter_by_missing_from_fs(query, value=missing) + query = self._filter_by_missing_from_fs(query, value=missing) # TODO: Correctly support true/false values. if verified: - query = self.filter_by_verified(query) + query = self._filter_by_verified(query) # BEWARE YE WHO ENTERS HERE 💀 if group_by_meta_id: @@ -676,14 +676,14 @@ class DBRomsHandler(DBBaseHandler): # Apply metadata and rom-level filters efficiently filters_to_apply = [ - (genres, genres_logic, self.filter_by_genres), - (franchises, franchises_logic, self.filter_by_franchises), - (collections, collections_logic, self.filter_by_collections), - (companies, companies_logic, self.filter_by_companies), - (age_ratings, age_ratings_logic, self.filter_by_age_ratings), - (regions, regions_logic, self.filter_by_regions), - (languages, languages_logic, self.filter_by_languages), - (player_counts, player_counts_logic, self.filter_by_player_counts), + (genres, genres_logic, self._filter_by_genres), + (franchises, franchises_logic, self._filter_by_franchises), + (collections, collections_logic, self._filter_by_collections), + (companies, companies_logic, self._filter_by_companies), + (age_ratings, age_ratings_logic, self._filter_by_age_ratings), + (regions, regions_logic, self._filter_by_regions), + (languages, languages_logic, self._filter_by_languages), + (player_counts, player_counts_logic, self._filter_by_player_counts), ] for values, logic, filter_func in filters_to_apply: @@ -694,7 +694,7 @@ class DBRomsHandler(DBBaseHandler): # The RomUser table is already joined if user_id is set if statuses and user_id: - query = self.filter_by_status(query, statuses) + query = self._filter_by_status(query, statuses) elif user_id: query = query.filter( or_(RomUser.hidden.is_(False), RomUser.hidden.is_(None)) @@ -1227,3 +1227,49 @@ class DBRomsHandler(DBBaseHandler): # Return the first ROM matching any of the provided hash values return session.scalar(query.outerjoin(Rom.files).filter(or_(*filters)).limit(1)) + + @begin_session + def get_rom_filters( + self, + session: Session = None, # type: ignore + ) -> dict: + statement = select( + RomMetadata.genres, + RomMetadata.franchises, + RomMetadata.companies, + RomMetadata.game_modes, + RomMetadata.age_ratings, + RomMetadata.player_count, + Rom.regions, + Rom.languages, + ) + genres = set() + franchises = set() + companies = set() + game_modes = set() + age_ratings = set() + player_counts = set() + regions = set() + languages = set() + + for row in session.execute(statement): + g, f, c, gm, ar, pc, rg, lg = row + genres.update(g) + franchises.update(f) + companies.update(c) + game_modes.update(gm) + age_ratings.update(ar) + player_counts.update(pc) + regions.update(rg) + languages.update(lg) + + return { + "genres": sorted(genres), + "franchises": sorted(franchises), + "companies": sorted(companies), + "game_modes": sorted(game_modes), + "age_ratings": sorted(age_ratings), + "player_counts": sorted(player_counts), + "regions": sorted(regions), + "languages": sorted(languages), + } diff --git a/frontend/src/__generated__/index.ts b/frontend/src/__generated__/index.ts index 247fbd188..baabd29be 100644 --- a/frontend/src/__generated__/index.ts +++ b/frontend/src/__generated__/index.ts @@ -62,6 +62,7 @@ export type { RAUserGameProgression } from './models/RAUserGameProgression'; export type { Role } from './models/Role'; export type { RomFileCategory } from './models/RomFileCategory'; export type { RomFileSchema } from './models/RomFileSchema'; +export type { RomFiltersDict } from './models/RomFiltersDict'; export type { RomFlashpointMetadata } from './models/RomFlashpointMetadata'; export type { RomGamelistMetadata } from './models/RomGamelistMetadata'; export type { RomHasheousMetadata } from './models/RomHasheousMetadata'; diff --git a/frontend/src/__generated__/models/RomFiltersDict.ts b/frontend/src/__generated__/models/RomFiltersDict.ts new file mode 100644 index 000000000..7dac4b10f --- /dev/null +++ b/frontend/src/__generated__/models/RomFiltersDict.ts @@ -0,0 +1,15 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type RomFiltersDict = { + genres: Array; + franchises: Array; + companies: Array; + game_modes: Array; + age_ratings: Array; + player_counts: Array; + regions: Array; + languages: Array; +}; + diff --git a/frontend/src/components/Gallery/AppBar/common/FilterDrawer/Base.vue b/frontend/src/components/Gallery/AppBar/common/FilterDrawer/Base.vue index 0410437f0..8f9732281 100644 --- a/frontend/src/components/Gallery/AppBar/common/FilterDrawer/Base.vue +++ b/frontend/src/components/Gallery/AppBar/common/FilterDrawer/Base.vue @@ -2,7 +2,7 @@ import { debounce } from "lodash"; import type { Emitter } from "mitt"; import { storeToRefs } from "pinia"; -import { inject, nextTick, onMounted, ref, watch } from "vue"; +import { inject, nextTick, onMounted, watch } from "vue"; import { useI18n } from "vue-i18n"; import { useRouter } from "vue-router"; import { useDisplay } from "vuetify"; @@ -15,10 +15,10 @@ import FilterPlatformBtn from "@/components/Gallery/AppBar/common/FilterDrawer/F import FilterPlayablesBtn from "@/components/Gallery/AppBar/common/FilterDrawer/FilterPlayablesBtn.vue"; import FilterRaBtn from "@/components/Gallery/AppBar/common/FilterDrawer/FilterRaBtn.vue"; import FilterVerifiedBtn from "@/components/Gallery/AppBar/common/FilterDrawer/FilterVerifiedBtn.vue"; -import cachedApiService from "@/services/cache/api"; +import romApi from "@/services/api/rom"; import storeGalleryFilter from "@/stores/galleryFilter"; import storePlatforms from "@/stores/platforms"; -import storeRoms, { type SimpleRom } from "@/stores/roms"; +import storeRoms from "@/stores/roms"; import type { Events } from "@/types/emitter"; withDefaults( @@ -177,8 +177,7 @@ const onFilterChange = debounce( // Separate debounced function for search term changes const onSearchChange = debounce( async () => { - await fetchSearchFilteredRoms(); - setFilters(); + await fetchSearchFilters(); }, 500, { leading: false, trailing: true }, @@ -264,109 +263,22 @@ const filters = [ function resetFilters() { galleryFilterStore.resetFilters(); nextTick(async () => { - await fetchSearchFilteredRoms(); - setFilters(); emitter?.emit("filterRoms", null); }); } -// Store search-filtered ROMs for populating filter options -let searchFilteredRoms = ref([]); +async function fetchSearchFilters() { + const { data } = await romApi.getRomFilters(); -async function fetchSearchFilteredRoms() { - try { - const params = { - searchTerm: searchTerm.value, - platformIds: romsStore.currentPlatform - ? [romsStore.currentPlatform.id] - : null, - collectionId: romsStore.currentCollection?.id ?? null, - virtualCollectionId: romsStore.currentVirtualCollection?.id ?? null, - smartCollectionId: romsStore.currentSmartCollection?.id ?? null, - limit: 10000, // Get enough ROMs to populate filters - offset: 0, - orderBy: romsStore.orderBy, - orderDir: romsStore.orderDir, - // Exclude all other filters to get comprehensive filter options - filterMatched: null, - filterFavorites: null, - filterDuplicates: null, - filterPlayables: null, - filterRA: null, - filterMissing: null, - filterVerified: null, - // Exclude all multi-value filters to get all possible options - selectedGenres: null, - selectedFranchises: null, - selectedCollections: null, - selectedCompanies: null, - selectedAgeRatings: null, - selectedRegions: null, - selectedLanguages: null, - selectedStatuses: null, - }; - - // Fetch ROMs with only search term applied (and current platform/collection context) - const response = await cachedApiService.getRoms(params, () => {}); // No background update callback needed - - searchFilteredRoms.value = response.data.items; - } catch (error) { - console.error("Failed to fetch search-filtered ROMs:", error); - // Fall back to current filtered ROMs if search-only fetch fails - searchFilteredRoms.value = romsStore.filteredRoms; - } -} - -function setFilters() { - const romsForFilters = - searchFilteredRoms.value.length > 0 - ? searchFilteredRoms.value - : romsStore.filteredRoms; - - galleryFilterStore.setFilterPlatforms([ - ...new Set( - romsForFilters - .flatMap((rom) => platformsStore.get(rom.platform_id)) - .filter((platform) => !!platform) - .sort(), - ), - ]); - galleryFilterStore.setFilterGenres([ - ...new Set(romsForFilters.flatMap((rom) => rom.metadatum.genres).sort()), - ]); - galleryFilterStore.setFilterFranchises([ - ...new Set( - romsForFilters.flatMap((rom) => rom.metadatum.franchises).sort(), - ), - ]); - galleryFilterStore.setFilterCompanies([ - ...new Set(romsForFilters.flatMap((rom) => rom.metadatum.companies).sort()), - ]); - galleryFilterStore.setFilterCollections([ - ...new Set( - romsForFilters.flatMap((rom) => rom.metadatum.collections).sort(), - ), - ]); - galleryFilterStore.setFilterAgeRatings([ - ...new Set( - romsForFilters.flatMap((rom) => rom.metadatum.age_ratings).sort(), - ), - ]); - galleryFilterStore.setFilterRegions([ - ...new Set(romsForFilters.flatMap((rom) => rom.regions).sort()), - ]); - galleryFilterStore.setFilterLanguages([ - ...new Set(romsForFilters.flatMap((rom) => rom.languages).sort()), - ]); - galleryFilterStore.setFilterPlayerCounts([ - ...new Set( - romsForFilters - .map((rom) => rom.metadatum.player_count) - .filter((playerCount): playerCount is string => !!playerCount) - .sort(), - ), - ]); - // Note: filterStatuses is static and doesn't need to be set dynamically + galleryFilterStore.setFilterPlatforms([]); + galleryFilterStore.setFilterGenres(data.genres); + galleryFilterStore.setFilterFranchises(data.franchises); + galleryFilterStore.setFilterCompanies(data.companies); + galleryFilterStore.setFilterCollections([]); + galleryFilterStore.setFilterAgeRatings(data.age_ratings); + galleryFilterStore.setFilterRegions(data.regions); + galleryFilterStore.setFilterLanguages(data.languages); + galleryFilterStore.setFilterPlayerCounts(data.player_counts); } onMounted(async () => { @@ -582,8 +494,7 @@ onMounted(async () => { } // Initial fetch of search-filtered ROMs for filter options - await fetchSearchFilteredRoms(); - setFilters(); + await fetchSearchFilters(); // Fire off search if URL state prepopulated if (freshSearch || galleryFilterStore.isFiltered()) { @@ -603,8 +514,7 @@ onMounted(async () => { watch( () => allPlatforms.value, async () => { - await fetchSearchFilteredRoms(); - setFilters(); + await fetchSearchFilters(); }, { immediate: false }, ); diff --git a/frontend/src/components/Settings/LibraryManagement/Config/MissingGames.vue b/frontend/src/components/Settings/LibraryManagement/Config/MissingGames.vue index c1da149b5..1fa8ff14b 100644 --- a/frontend/src/components/Settings/LibraryManagement/Config/MissingGames.vue +++ b/frontend/src/components/Settings/LibraryManagement/Config/MissingGames.vue @@ -13,7 +13,7 @@ import PlatformIcon from "@/components/common/Platform/PlatformIcon.vue"; import storeGalleryFilter from "@/stores/galleryFilter"; import storeGalleryView from "@/stores/galleryView"; import storePlatforms from "@/stores/platforms"; -import storeRoms, { MAX_FETCH_LIMIT } from "@/stores/roms"; +import storeRoms from "@/stores/roms"; import type { Events } from "@/types/emitter"; const { t } = useI18n(); @@ -91,7 +91,7 @@ async function fetchRoms() { } function cleanupAll() { - romsStore.setLimit(MAX_FETCH_LIMIT); + romsStore.setLimit(10000); galleryFilterStore.setFilterMissing(true); romsStore .fetchRoms({ galleryFilter: galleryFilterStore }) diff --git a/frontend/src/services/api/rom.ts b/frontend/src/services/api/rom.ts index 552675265..ad5d2b750 100644 --- a/frontend/src/services/api/rom.ts +++ b/frontend/src/services/api/rom.ts @@ -4,6 +4,7 @@ import type { ManualMetadata, RomUserSchema, UserNoteSchema, + RomFiltersDict, } from "@/__generated__"; import { type CustomLimitOffsetPage_SimpleRomSchema_ as GetRomsResponse } from "@/__generated__/models/CustomLimitOffsetPage_SimpleRomSchema_"; import api from "@/services/api"; @@ -582,6 +583,10 @@ async function getRomNotes({ }); } +async function getRomFilters(): Promise<{ data: RomFiltersDict }> { + return api.get("/roms/filters"); +} + export default { uploadRoms, getRoms, @@ -601,4 +606,5 @@ export default { updateRomNote, deleteRomNote, getRomNotes, + getRomFilters, }; diff --git a/frontend/src/stores/roms.ts b/frontend/src/stores/roms.ts index 7c077e8e5..0a283c5ee 100644 --- a/frontend/src/stores/roms.ts +++ b/frontend/src/stores/roms.ts @@ -22,7 +22,6 @@ type GalleryFilterStore = ExtractPiniaStoreType; export type SimpleRom = SimpleRomSchema; export type SearchRom = SearchRomSchema; export type DetailedRom = DetailedRomSchema; -export const MAX_FETCH_LIMIT = 10000; const orderByStorage = useLocalStorage("roms.orderBy", "name"); const orderDirStorage = useLocalStorage("roms.orderDir", "asc");