From 96240a86e9fab08c832d12a534c14945812285fa Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Thu, 15 Jan 2026 11:18:51 -0500 Subject: [PATCH 01/10] 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"); From 81b9eff8c7b3cdd8583a4aa7b57a0de4cfef9167 Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Thu, 15 Jan 2026 11:51:59 -0500 Subject: [PATCH 02/10] Add filter values to roms query --- backend/endpoints/rom.py | 28 ++++++++++ backend/handler/database/roms_handler.py | 55 +++++++++++++++++++ .../CustomLimitOffsetPage_SimpleRomSchema_.ts | 2 + .../AppBar/common/FilterDrawer/Base.vue | 21 ++++--- frontend/src/services/api/rom.ts | 2 + frontend/src/services/cache/api.ts | 4 ++ frontend/src/stores/roms.ts | 25 +++++++-- 7 files changed, 122 insertions(+), 15 deletions(-) diff --git a/backend/endpoints/rom.py b/backend/endpoints/rom.py index 0d7d2f358..7425dc655 100644 --- a/backend/endpoints/rom.py +++ b/backend/endpoints/rom.py @@ -187,6 +187,7 @@ class CustomLimitOffsetParams(LimitOffsetParams): class CustomLimitOffsetPage[T: BaseModel](LimitOffsetPage[T]): char_index: dict[str, int] rom_id_index: list[int] + filter_values: RomFiltersDict __params_type__ = CustomLimitOffsetParams @@ -197,6 +198,9 @@ def get_roms( bool, Query(description="Whether to get the char index."), ] = True, + with_filter_values: Annotated[ + bool, Query(description="Whether to return filter values.") + ] = True, search_term: Annotated[ str | None, Query(description="Search term to filter roms."), @@ -461,6 +465,29 @@ def get_roms( ) char_index_dict = {char: index for (char, index) in char_index} + filter_values = RomFiltersDict( + genres=[], + franchises=[], + companies=[], + game_modes=[], + age_ratings=[], + player_counts=[], + regions=[], + languages=[], + ) + if with_filter_values: + query_filters = db_rom_handler.with_filter_values(query=query) + filter_values = RomFiltersDict( + genres=query_filters["genres"], + franchises=query_filters["franchises"], + companies=query_filters["companies"], + game_modes=query_filters["game_modes"], + age_ratings=query_filters["age_ratings"], + player_counts=query_filters["player_counts"], + regions=query_filters["regions"], + languages=query_filters["languages"], + ) + # Get all ROM IDs in order for the additional data with sync_session.begin() as session: rom_id_index = session.scalars(query.with_only_columns(Rom.id)).all() # type: ignore @@ -474,6 +501,7 @@ def get_roms( additional_data={ "char_index": char_index_dict, "rom_id_index": rom_id_index, + "filter_values": filter_values, }, ) diff --git a/backend/handler/database/roms_handler.py b/backend/handler/database/roms_handler.py index 25fd82b26..eeb91e19d 100644 --- a/backend/handler/database/roms_handler.py +++ b/backend/handler/database/roms_handler.py @@ -1273,3 +1273,58 @@ class DBRomsHandler(DBBaseHandler): "regions": sorted(regions), "languages": sorted(languages), } + + @begin_session + def with_filter_values( + self, + query: Query, + session: Session = None, # type: ignore + ) -> dict: + ids_subq = query.with_only_columns(Rom.id).scalar_subquery() # type: ignore + + statement = ( + select( + RomMetadata.genres, + RomMetadata.franchises, + RomMetadata.companies, + RomMetadata.game_modes, + RomMetadata.age_ratings, + RomMetadata.player_count, + Rom.regions, + Rom.languages, + ) + .select_from(Rom) + .join(RomMetadata, Rom.id == RomMetadata.rom_id) + .where(Rom.id.in_(ids_subq)) + ) + + 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__/models/CustomLimitOffsetPage_SimpleRomSchema_.ts b/frontend/src/__generated__/models/CustomLimitOffsetPage_SimpleRomSchema_.ts index dcd5c0ae6..baea947bf 100644 --- a/frontend/src/__generated__/models/CustomLimitOffsetPage_SimpleRomSchema_.ts +++ b/frontend/src/__generated__/models/CustomLimitOffsetPage_SimpleRomSchema_.ts @@ -2,6 +2,7 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ +import type { RomFiltersDict } from './RomFiltersDict'; import type { SimpleRomSchema } from './SimpleRomSchema'; export type CustomLimitOffsetPage_SimpleRomSchema_ = { items: Array; @@ -10,5 +11,6 @@ export type CustomLimitOffsetPage_SimpleRomSchema_ = { offset: number; char_index: Record; rom_id_index: Array; + filter_values: RomFiltersDict; }; diff --git a/frontend/src/components/Gallery/AppBar/common/FilterDrawer/Base.vue b/frontend/src/components/Gallery/AppBar/common/FilterDrawer/Base.vue index 8f9732281..984c61817 100644 --- a/frontend/src/components/Gallery/AppBar/common/FilterDrawer/Base.vue +++ b/frontend/src/components/Gallery/AppBar/common/FilterDrawer/Base.vue @@ -268,17 +268,16 @@ function resetFilters() { } async function fetchSearchFilters() { - const { data } = await romApi.getRomFilters(); - - 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); + // const { data } = await romApi.getRomFilters(); + // 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 () => { diff --git a/frontend/src/services/api/rom.ts b/frontend/src/services/api/rom.ts index ad5d2b750..7fcde968e 100644 --- a/frontend/src/services/api/rom.ts +++ b/frontend/src/services/api/rom.ts @@ -247,6 +247,7 @@ async function getRecentRoms(): Promise<{ data: GetRomsResponse }> { order_dir: "desc", limit: RECENT_ROMS_LIMIT, with_char_index: false, + with_filter_values: false, }, }); } @@ -258,6 +259,7 @@ async function getRecentPlayedRoms(): Promise<{ data: GetRomsResponse }> { order_dir: "desc", limit: RECENT_PLAYED_ROMS_LIMIT, with_char_index: false, + with_filter_values: false, last_played: true, }, }); diff --git a/frontend/src/services/cache/api.ts b/frontend/src/services/cache/api.ts index 40c51743d..f302bcffb 100644 --- a/frontend/src/services/cache/api.ts +++ b/frontend/src/services/cache/api.ts @@ -147,6 +147,7 @@ class CachedApiService { order_dir: "desc", limit: 15, with_char_index: false, + with_filter_values: false, }); return cacheService.request(config, onBackgroundUpdate); @@ -160,6 +161,7 @@ class CachedApiService { order_dir: "desc", limit: 15, with_char_index: false, + with_filter_values: false, last_played: true, }); @@ -177,6 +179,7 @@ class CachedApiService { order_dir: "desc", limit: 15, with_char_index: false, + with_filter_values: false, }); } @@ -186,6 +189,7 @@ class CachedApiService { order_dir: "desc", limit: 15, with_char_index: false, + with_filter_values: false, last_played: true, }); } diff --git a/frontend/src/stores/roms.ts b/frontend/src/stores/roms.ts index 0a283c5ee..e6d64e405 100644 --- a/frontend/src/stores/roms.ts +++ b/frontend/src/stores/roms.ts @@ -149,8 +149,13 @@ export default defineStore("roms", { }; return params; }, - _postFetchRoms(response: GetRomsResponse, concat: boolean) { - const { items, offset, total, char_index, rom_id_index } = response; + _postFetchRoms( + response: GetRomsResponse, + galleryFilter: GalleryFilterStore, + concat: boolean, + ) { + const { items, offset, total, char_index, rom_id_index, filter_values } = + response; if (!concat || this.fetchOffset === 0) { this._allRoms = items; } else { @@ -164,6 +169,18 @@ export default defineStore("roms", { // Set the character index for the current platform this.characterIndex = char_index; this.romIdIndex = rom_id_index; + + if (filter_values) { + galleryFilter.setFilterPlatforms([]); + galleryFilter.setFilterCollections([]); + galleryFilter.setFilterGenres(filter_values.genres); + galleryFilter.setFilterFranchises(filter_values.franchises); + galleryFilter.setFilterCompanies(filter_values.companies); + galleryFilter.setFilterAgeRatings(filter_values.age_ratings); + galleryFilter.setFilterRegions(filter_values.regions); + galleryFilter.setFilterLanguages(filter_values.languages); + galleryFilter.setFilterPlayerCounts(filter_values.player_counts); + } }, async fetchRoms({ galleryFilter, @@ -189,10 +206,10 @@ export default defineStore("roms", { JSON.stringify(currentParams) !== JSON.stringify(currentRequestParams); if (paramsChanged) return; - this._postFetchRoms(response, concat); + this._postFetchRoms(response, galleryFilter, concat); }) .then((response) => { - this._postFetchRoms(response.data, concat); + this._postFetchRoms(response.data, galleryFilter, concat); resolve(response.data.items); }) .catch((error) => { From b0cf2e9338bef0d94816acf8c725f33e0d01bcdc Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Thu, 15 Jan 2026 12:10:13 -0500 Subject: [PATCH 03/10] set filter platforms on platforms fetch --- backend/endpoints/responses/rom.py | 1 + backend/endpoints/rom.py | 3 + backend/handler/database/roms_handler.py | 123 ++++++++++-------- .../__generated__/models/RomFiltersDict.ts | 1 + .../AppBar/common/FilterDrawer/Base.vue | 47 +------ frontend/src/console/Layout.vue | 4 +- frontend/src/layouts/Main.vue | 6 +- frontend/src/stores/platforms.ts | 10 +- frontend/src/stores/roms.ts | 3 +- 9 files changed, 89 insertions(+), 109 deletions(-) diff --git a/backend/endpoints/responses/rom.py b/backend/endpoints/responses/rom.py index ccbe430c1..29d90caa5 100644 --- a/backend/endpoints/responses/rom.py +++ b/backend/endpoints/responses/rom.py @@ -449,6 +449,7 @@ class DetailedRomSchema(RomSchema): class RomFiltersDict(TypedDict): genres: list[str] franchises: list[str] + collections: list[str] companies: list[str] game_modes: list[str] age_ratings: list[str] diff --git a/backend/endpoints/rom.py b/backend/endpoints/rom.py index 7425dc655..c8236bf8a 100644 --- a/backend/endpoints/rom.py +++ b/backend/endpoints/rom.py @@ -468,6 +468,7 @@ def get_roms( filter_values = RomFiltersDict( genres=[], franchises=[], + collections=[], companies=[], game_modes=[], age_ratings=[], @@ -480,6 +481,7 @@ def get_roms( filter_values = RomFiltersDict( genres=query_filters["genres"], franchises=query_filters["franchises"], + collections=query_filters["collections"], companies=query_filters["companies"], game_modes=query_filters["game_modes"], age_ratings=query_filters["age_ratings"], @@ -709,6 +711,7 @@ async def get_rom_filters(request: Request) -> RomFiltersDict: return RomFiltersDict( genres=filters["genres"], franchises=filters["franchises"], + collections=filters["collections"], companies=filters["companies"], game_modes=filters["game_modes"], age_ratings=filters["age_ratings"], diff --git a/backend/handler/database/roms_handler.py b/backend/handler/database/roms_handler.py index eeb91e19d..776d1f59a 100644 --- a/backend/handler/database/roms_handler.py +++ b/backend/handler/database/roms_handler.py @@ -835,6 +835,65 @@ class DBRomsHandler(DBBaseHandler): .all() ) + @begin_session + def with_filter_values( + self, + query: Query, + session: Session = None, # type: ignore + ) -> dict: + ids_subq = query.with_only_columns(Rom.id).scalar_subquery() # type: ignore + + statement = ( + select( + RomMetadata.genres, + RomMetadata.franchises, + RomMetadata.collections, + RomMetadata.companies, + RomMetadata.game_modes, + RomMetadata.age_ratings, + RomMetadata.player_count, + Rom.regions, + Rom.languages, + ) + .select_from(Rom) + .join(RomMetadata, Rom.id == RomMetadata.rom_id) + .where(Rom.id.in_(ids_subq)) + ) + + genres = set() + franchises = set() + collections = set() + companies = set() + game_modes = set() + age_ratings = set() + player_counts = set() + regions = set() + languages = set() + + for row in session.execute(statement): + g, f, cl, co, gm, ar, pc, rg, lg = row + genres.update(g) + franchises.update(f) + collections.update(cl) + companies.update(co) + 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), + "collections": sorted(collections), + "companies": sorted(companies), + "game_modes": sorted(game_modes), + "age_ratings": sorted(age_ratings), + "player_counts": sorted(player_counts), + "regions": sorted(regions), + "languages": sorted(languages), + } + @begin_session @with_details def get_roms_by_fs_name( @@ -1236,6 +1295,7 @@ class DBRomsHandler(DBBaseHandler): statement = select( RomMetadata.genres, RomMetadata.franchises, + RomMetadata.collections, RomMetadata.companies, RomMetadata.game_modes, RomMetadata.age_ratings, @@ -1243,8 +1303,10 @@ class DBRomsHandler(DBBaseHandler): Rom.regions, Rom.languages, ) + genres = set() franchises = set() + collections = set() companies = set() game_modes = set() age_ratings = set() @@ -1253,65 +1315,11 @@ class DBRomsHandler(DBBaseHandler): languages = set() for row in session.execute(statement): - g, f, c, gm, ar, pc, rg, lg = row + g, f, cl, co, 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), - } - - @begin_session - def with_filter_values( - self, - query: Query, - session: Session = None, # type: ignore - ) -> dict: - ids_subq = query.with_only_columns(Rom.id).scalar_subquery() # type: ignore - - statement = ( - select( - RomMetadata.genres, - RomMetadata.franchises, - RomMetadata.companies, - RomMetadata.game_modes, - RomMetadata.age_ratings, - RomMetadata.player_count, - Rom.regions, - Rom.languages, - ) - .select_from(Rom) - .join(RomMetadata, Rom.id == RomMetadata.rom_id) - .where(Rom.id.in_(ids_subq)) - ) - - 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) + collections.update(cl) + companies.update(co) game_modes.update(gm) age_ratings.update(ar) player_counts.update(pc) @@ -1321,6 +1329,7 @@ class DBRomsHandler(DBBaseHandler): return { "genres": sorted(genres), "franchises": sorted(franchises), + "collections": sorted(collections), "companies": sorted(companies), "game_modes": sorted(game_modes), "age_ratings": sorted(age_ratings), diff --git a/frontend/src/__generated__/models/RomFiltersDict.ts b/frontend/src/__generated__/models/RomFiltersDict.ts index 7dac4b10f..dfeeb3bc9 100644 --- a/frontend/src/__generated__/models/RomFiltersDict.ts +++ b/frontend/src/__generated__/models/RomFiltersDict.ts @@ -5,6 +5,7 @@ export type RomFiltersDict = { genres: Array; franchises: Array; + collections: Array; companies: Array; game_modes: Array; age_ratings: Array; diff --git a/frontend/src/components/Gallery/AppBar/common/FilterDrawer/Base.vue b/frontend/src/components/Gallery/AppBar/common/FilterDrawer/Base.vue index 984c61817..5338702a4 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, watch } from "vue"; +import { inject, nextTick, onMounted } from "vue"; import { useI18n } from "vue-i18n"; import { useRouter } from "vue-router"; import { useDisplay } from "vuetify"; @@ -15,7 +15,6 @@ 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 romApi from "@/services/api/rom"; import storeGalleryFilter from "@/stores/galleryFilter"; import storePlatforms from "@/stores/platforms"; import storeRoms from "@/stores/roms"; @@ -79,7 +78,6 @@ const { selectedPlayerCounts, playerCountsLogic, } = storeToRefs(galleryFilterStore); -const { allPlatforms } = storeToRefs(platformsStore); const emitter = inject>("emitter"); const onFilterChange = debounce( @@ -174,15 +172,6 @@ const onFilterChange = debounce( { leading: false, trailing: true }, ); -// Separate debounced function for search term changes -const onSearchChange = debounce( - async () => { - await fetchSearchFilters(); - }, - 500, - { leading: false, trailing: true }, -); - emitter?.on("filterRoms", onFilterChange); const filters = [ @@ -267,19 +256,6 @@ function resetFilters() { }); } -async function fetchSearchFilters() { - // const { data } = await romApi.getRomFilters(); - // 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 () => { const { search: urlSearch, @@ -492,31 +468,10 @@ onMounted(async () => { romsStore.resetPagination(); } - // Initial fetch of search-filtered ROMs for filter options - await fetchSearchFilters(); - // Fire off search if URL state prepopulated if (freshSearch || galleryFilterStore.isFiltered()) { emitter?.emit("filterRoms", null); } - - // Watch for search term changes to update filter options - watch( - () => searchTerm.value, - async () => { - await onSearchChange(); - }, - { immediate: false }, - ); - - // Watch for platform changes to update filter options - watch( - () => allPlatforms.value, - async () => { - await fetchSearchFilters(); - }, - { immediate: false }, - ); }); diff --git a/frontend/src/console/Layout.vue b/frontend/src/console/Layout.vue index bb4754435..be5787aa9 100644 --- a/frontend/src/console/Layout.vue +++ b/frontend/src/console/Layout.vue @@ -9,6 +9,7 @@ import { attachGamepad } from "@/console/input/gamepad"; import { attachKeyboard } from "@/console/input/keyboard"; import { ROUTES } from "@/plugins/router"; import storeCollections from "@/stores/collections"; +import storeGalleryFilter from "@/stores/galleryFilter"; import storeNavigation from "@/stores/navigation"; import storePlatforms from "@/stores/platforms"; @@ -20,6 +21,7 @@ provide(InputBusSymbol, bus); const navigationStore = storeNavigation(); const platformsStore = storePlatforms(); const collectionsStore = storeCollections(); +const galleryFilterStore = storeGalleryFilter(); const showVirtualCollections = useLocalStorage( "settings.showVirtualCollections", @@ -70,7 +72,7 @@ let detachKeyboard: (() => void) | null = null; let detachGamepad: (() => void) | null = null; onBeforeMount(() => { - platformsStore.fetchPlatforms(); + platformsStore.fetchPlatforms({ galleryFilter: galleryFilterStore }); collectionsStore.fetchCollections(); collectionsStore.fetchSmartCollections(); if (showVirtualCollections) { diff --git a/frontend/src/layouts/Main.vue b/frontend/src/layouts/Main.vue index e453e8d4c..37970e9f5 100644 --- a/frontend/src/layouts/Main.vue +++ b/frontend/src/layouts/Main.vue @@ -23,6 +23,7 @@ import NewVersionDialog from "@/components/common/NewVersionDialog.vue"; import Notification from "@/components/common/Notifications/Notification.vue"; import UploadProgress from "@/components/common/Notifications/UploadProgress.vue"; import storeCollections from "@/stores/collections"; +import storeGalleryFilter from "@/stores/galleryFilter"; import storeNavigation from "@/stores/navigation"; import storePlatforms from "@/stores/platforms"; import type { Events } from "@/types/emitter"; @@ -30,10 +31,11 @@ import type { Events } from "@/types/emitter"; const navigationStore = storeNavigation(); const platformsStore = storePlatforms(); const collectionsStore = storeCollections(); +const galleryFilterStore = storeGalleryFilter(); const emitter = inject>("emitter"); emitter?.on("refreshDrawer", async () => { - platformsStore.fetchPlatforms(); + platformsStore.fetchPlatforms({ galleryFilter: galleryFilterStore }); }); const showVirtualCollections = useLocalStorage( @@ -56,7 +58,7 @@ function unhackNavbar() { onBeforeMount(async () => { document.addEventListener("network-quiesced", unhackNavbar); - platformsStore.fetchPlatforms(); + platformsStore.fetchPlatforms({ galleryFilter: galleryFilterStore }); collectionsStore.fetchCollections(); collectionsStore.fetchSmartCollections(); if (showVirtualCollections) { diff --git a/frontend/src/stores/platforms.ts b/frontend/src/stores/platforms.ts index a24735515..5915b56f3 100644 --- a/frontend/src/stores/platforms.ts +++ b/frontend/src/stores/platforms.ts @@ -2,8 +2,11 @@ import { uniqBy } from "lodash"; import { defineStore } from "pinia"; import type { PlatformSchema } from "@/__generated__"; import platformApi from "@/services/api/platform"; +import storeGalleryFilter from "@/stores/galleryFilter"; +import type { ExtractPiniaStoreType } from "@/types"; export type Platform = PlatformSchema; +type GalleryFilterStore = ExtractPiniaStoreType; export default defineStore("platforms", { state: () => ({ @@ -35,7 +38,11 @@ export default defineStore("platforms", { return a.name.localeCompare(b.name); }); }, - fetchPlatforms(): Promise { + fetchPlatforms({ + galleryFilter, + }: { + galleryFilter: GalleryFilterStore; + }): Promise { if (this.fetchingPlatforms) return Promise.resolve([]); this.fetchingPlatforms = true; @@ -44,6 +51,7 @@ export default defineStore("platforms", { .getPlatforms() .then(({ data: platforms }) => { this.allPlatforms = platforms; + galleryFilter.setFilterPlatforms(platforms); resolve(platforms); }) .catch((error) => { diff --git a/frontend/src/stores/roms.ts b/frontend/src/stores/roms.ts index e6d64e405..9af4f11da 100644 --- a/frontend/src/stores/roms.ts +++ b/frontend/src/stores/roms.ts @@ -171,8 +171,7 @@ export default defineStore("roms", { this.romIdIndex = rom_id_index; if (filter_values) { - galleryFilter.setFilterPlatforms([]); - galleryFilter.setFilterCollections([]); + galleryFilter.setFilterCollections(filter_values.collections); galleryFilter.setFilterGenres(filter_values.genres); galleryFilter.setFilterFranchises(filter_values.franchises); galleryFilter.setFilterCompanies(filter_values.companies); From 354922cf4104844671834b1f479f7559128a43a5 Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Thu, 15 Jan 2026 12:43:35 -0500 Subject: [PATCH 04/10] correct set platforms in roms fetch --- backend/endpoints/responses/rom.py | 1 + backend/endpoints/rom.py | 17 +++++++++--- backend/handler/database/roms_handler.py | 12 +++++++-- .../__generated__/models/RomFiltersDict.ts | 1 + .../Gallery/AppBar/common/CharIndexBar.vue | 8 +++++- .../AppBar/common/FilterDrawer/Base.vue | 6 ++++- .../LibraryManagement/Config/MissingGames.vue | 11 ++++++-- .../components/common/Game/VirtualTable.vue | 7 ++++- frontend/src/console/Layout.vue | 4 +-- frontend/src/console/views/GamesList.vue | 1 + frontend/src/layouts/Main.vue | 6 ++--- frontend/src/stores/platforms.ts | 7 +---- frontend/src/stores/roms.ts | 26 +++++++++++++++++-- .../Gallery/Collection/BaseCollection.vue | 7 ++++- frontend/src/views/Gallery/Platform.vue | 5 +++- frontend/src/views/Gallery/Search.vue | 7 ++++- 16 files changed, 98 insertions(+), 28 deletions(-) diff --git a/backend/endpoints/responses/rom.py b/backend/endpoints/responses/rom.py index 29d90caa5..143551787 100644 --- a/backend/endpoints/responses/rom.py +++ b/backend/endpoints/responses/rom.py @@ -456,3 +456,4 @@ class RomFiltersDict(TypedDict): player_counts: list[str] regions: list[str] languages: list[str] + platforms: list[int] diff --git a/backend/endpoints/rom.py b/backend/endpoints/rom.py index c8236bf8a..b99dc9438 100644 --- a/backend/endpoints/rom.py +++ b/backend/endpoints/rom.py @@ -412,7 +412,7 @@ def get_roms( ] = "asc", ) -> CustomLimitOffsetPage[SimpleRomSchema]: """Retrieve roms.""" - query, order_by_attr = db_rom_handler.get_roms_query( + unfiltered_query, order_by_attr = db_rom_handler.get_roms_query( user_id=request.user.id, order_by=order_by.lower(), order_dir=order_dir.lower(), @@ -420,7 +420,7 @@ def get_roms( # Filter down the query query = db_rom_handler.filter_roms( - query=query, + query=unfiltered_query, user_id=request.user.id, platform_ids=platform_ids, collection_id=collection_id, @@ -475,9 +475,18 @@ def get_roms( player_counts=[], regions=[], languages=[], + platforms=[], ) if with_filter_values: - query_filters = db_rom_handler.with_filter_values(query=query) + filter_query = db_rom_handler.filter_roms( + query=unfiltered_query, + user_id=request.user.id, + collection_id=collection_id, + virtual_collection_id=virtual_collection_id, + smart_collection_id=smart_collection_id, + search_term=search_term, + ) + query_filters = db_rom_handler.with_filter_values(query=filter_query) filter_values = RomFiltersDict( genres=query_filters["genres"], franchises=query_filters["franchises"], @@ -488,6 +497,7 @@ def get_roms( player_counts=query_filters["player_counts"], regions=query_filters["regions"], languages=query_filters["languages"], + platforms=query_filters["platforms"], ) # Get all ROM IDs in order for the additional data @@ -718,6 +728,7 @@ async def get_rom_filters(request: Request) -> RomFiltersDict: player_counts=filters["player_counts"], regions=filters["regions"], languages=filters["languages"], + platforms=filters["platforms"], ) diff --git a/backend/handler/database/roms_handler.py b/backend/handler/database/roms_handler.py index 776d1f59a..0e1dfb3e8 100644 --- a/backend/handler/database/roms_handler.py +++ b/backend/handler/database/roms_handler.py @@ -854,6 +854,7 @@ class DBRomsHandler(DBBaseHandler): RomMetadata.player_count, Rom.regions, Rom.languages, + Rom.platform_id, ) .select_from(Rom) .join(RomMetadata, Rom.id == RomMetadata.rom_id) @@ -869,9 +870,10 @@ class DBRomsHandler(DBBaseHandler): player_counts = set() regions = set() languages = set() + platforms = set() for row in session.execute(statement): - g, f, cl, co, gm, ar, pc, rg, lg = row + g, f, cl, co, gm, ar, pc, rg, lg, pid = row genres.update(g) franchises.update(f) collections.update(cl) @@ -881,6 +883,7 @@ class DBRomsHandler(DBBaseHandler): player_counts.update(pc) regions.update(rg) languages.update(lg) + platforms.add(pid) return { "genres": sorted(genres), @@ -892,6 +895,7 @@ class DBRomsHandler(DBBaseHandler): "player_counts": sorted(player_counts), "regions": sorted(regions), "languages": sorted(languages), + "platforms": sorted(platforms), } @begin_session @@ -1302,6 +1306,7 @@ class DBRomsHandler(DBBaseHandler): RomMetadata.player_count, Rom.regions, Rom.languages, + Rom.platform_id, ) genres = set() @@ -1313,9 +1318,10 @@ class DBRomsHandler(DBBaseHandler): player_counts = set() regions = set() languages = set() + platforms = set() for row in session.execute(statement): - g, f, cl, co, gm, ar, pc, rg, lg = row + g, f, cl, co, gm, ar, pc, rg, lg, pid = row genres.update(g) franchises.update(f) collections.update(cl) @@ -1325,6 +1331,7 @@ class DBRomsHandler(DBBaseHandler): player_counts.update(pc) regions.update(rg) languages.update(lg) + platforms.add(pid) return { "genres": sorted(genres), @@ -1336,4 +1343,5 @@ class DBRomsHandler(DBBaseHandler): "player_counts": sorted(player_counts), "regions": sorted(regions), "languages": sorted(languages), + "platforms": sorted(platforms), } diff --git a/frontend/src/__generated__/models/RomFiltersDict.ts b/frontend/src/__generated__/models/RomFiltersDict.ts index dfeeb3bc9..16ccfccea 100644 --- a/frontend/src/__generated__/models/RomFiltersDict.ts +++ b/frontend/src/__generated__/models/RomFiltersDict.ts @@ -12,5 +12,6 @@ export type RomFiltersDict = { player_counts: Array; regions: Array; languages: Array; + platforms: Array; }; diff --git a/frontend/src/components/Gallery/AppBar/common/CharIndexBar.vue b/frontend/src/components/Gallery/AppBar/common/CharIndexBar.vue index d10694bf8..b474c6380 100644 --- a/frontend/src/components/Gallery/AppBar/common/CharIndexBar.vue +++ b/frontend/src/components/Gallery/AppBar/common/CharIndexBar.vue @@ -5,11 +5,13 @@ import { inject, watch, computed } from "vue"; import { useDisplay } from "vuetify"; import storeGalleryFilter from "@/stores/galleryFilter"; import storeGalleryView from "@/stores/galleryView"; +import storePlatforms from "@/stores/platforms"; import storeRoms from "@/stores/roms"; import type { Events } from "@/types/emitter"; const { smAndDown } = useDisplay(); const romsStore = storeRoms(); +const platformsStore = storePlatforms(); const galleryFilterStore = storeGalleryFilter(); const galleryViewStore = storeGalleryView(); const { selectedRoms } = storeToRefs(romsStore); @@ -29,7 +31,11 @@ async function fetchRoms() { }); romsStore - .fetchRoms({ galleryFilter: galleryFilterStore, concat: false }) + .fetchRoms({ + galleryFilter: galleryFilterStore, + platformsStore: platformsStore, + concat: false, + }) .then(() => { emitter?.emit("showLoadingDialog", { loading: false, diff --git a/frontend/src/components/Gallery/AppBar/common/FilterDrawer/Base.vue b/frontend/src/components/Gallery/AppBar/common/FilterDrawer/Base.vue index 5338702a4..bd2b7af25 100644 --- a/frontend/src/components/Gallery/AppBar/common/FilterDrawer/Base.vue +++ b/frontend/src/components/Gallery/AppBar/common/FilterDrawer/Base.vue @@ -83,7 +83,11 @@ const emitter = inject>("emitter"); const onFilterChange = debounce( () => { romsStore.resetPagination(); - romsStore.fetchRoms({ galleryFilter: galleryFilterStore, concat: false }); + romsStore.fetchRoms({ + galleryFilter: galleryFilterStore, + platformsStore: platformsStore, + concat: false, + }); const url = new URL(window.location.href); // Update URL with filters diff --git a/frontend/src/components/Settings/LibraryManagement/Config/MissingGames.vue b/frontend/src/components/Settings/LibraryManagement/Config/MissingGames.vue index 1fa8ff14b..6dc35db29 100644 --- a/frontend/src/components/Settings/LibraryManagement/Config/MissingGames.vue +++ b/frontend/src/components/Settings/LibraryManagement/Config/MissingGames.vue @@ -45,6 +45,7 @@ const onFilterChange = debounce( galleryFilterStore.setFilterMissing(true); romsStore.fetchRoms({ galleryFilter: galleryFilterStore, + platformsStore: platformsStore, concat: false, }); @@ -74,7 +75,10 @@ async function fetchRoms() { galleryFilterStore.setFilterMissing(true); romsStore - .fetchRoms({ galleryFilter: galleryFilterStore }) + .fetchRoms({ + galleryFilter: galleryFilterStore, + platformsStore: platformsStore, + }) .catch((error) => { console.error("Error fetching missing games:", error); emitter?.emit("snackbarShow", { @@ -94,7 +98,10 @@ function cleanupAll() { romsStore.setLimit(10000); galleryFilterStore.setFilterMissing(true); romsStore - .fetchRoms({ galleryFilter: galleryFilterStore }) + .fetchRoms({ + galleryFilter: galleryFilterStore, + platformsStore: platformsStore, + }) .then(() => { emitter?.emit("showLoadingDialog", { loading: false, diff --git a/frontend/src/components/common/Game/VirtualTable.vue b/frontend/src/components/common/Game/VirtualTable.vue index 334e96f45..1a14903de 100644 --- a/frontend/src/components/common/Game/VirtualTable.vue +++ b/frontend/src/components/common/Game/VirtualTable.vue @@ -16,6 +16,7 @@ import storeAuth from "@/stores/auth"; import storeCollections from "@/stores/collections"; import storeDownload from "@/stores/download"; import storeGalleryFilter from "@/stores/galleryFilter"; +import storePlatforms from "@/stores/platforms"; import storeRoms, { type SimpleRom } from "@/stores/roms"; import type { Events } from "@/types/emitter"; import { @@ -44,6 +45,7 @@ const { filteredRoms, selectedRoms, fetchingRoms, fetchTotalRoms } = const auth = storeAuth(); const galleryFilterStore = storeGalleryFilter(); const collectionsStore = storeCollections(); +const platformsStore = storePlatforms(); const emitter = inject>("emitter"); const HEADERS = [ @@ -134,7 +136,10 @@ function updateOptions({ sortBy }: { sortBy: SortBy }) { romsStore.resetPagination(); romsStore.setOrderBy(key); romsStore.setOrderDir(order); - romsStore.fetchRoms({ galleryFilter: galleryFilterStore }); + romsStore.fetchRoms({ + galleryFilter: galleryFilterStore, + platformsStore: platformsStore, + }); } diff --git a/frontend/src/console/Layout.vue b/frontend/src/console/Layout.vue index be5787aa9..bb4754435 100644 --- a/frontend/src/console/Layout.vue +++ b/frontend/src/console/Layout.vue @@ -9,7 +9,6 @@ import { attachGamepad } from "@/console/input/gamepad"; import { attachKeyboard } from "@/console/input/keyboard"; import { ROUTES } from "@/plugins/router"; import storeCollections from "@/stores/collections"; -import storeGalleryFilter from "@/stores/galleryFilter"; import storeNavigation from "@/stores/navigation"; import storePlatforms from "@/stores/platforms"; @@ -21,7 +20,6 @@ provide(InputBusSymbol, bus); const navigationStore = storeNavigation(); const platformsStore = storePlatforms(); const collectionsStore = storeCollections(); -const galleryFilterStore = storeGalleryFilter(); const showVirtualCollections = useLocalStorage( "settings.showVirtualCollections", @@ -72,7 +70,7 @@ let detachKeyboard: (() => void) | null = null; let detachGamepad: (() => void) | null = null; onBeforeMount(() => { - platformsStore.fetchPlatforms({ galleryFilter: galleryFilterStore }); + platformsStore.fetchPlatforms(); collectionsStore.fetchCollections(); collectionsStore.fetchSmartCollections(); if (showVirtualCollections) { diff --git a/frontend/src/console/views/GamesList.vue b/frontend/src/console/views/GamesList.vue index ae30864d0..abccb18f5 100644 --- a/frontend/src/console/views/GamesList.vue +++ b/frontend/src/console/views/GamesList.vue @@ -320,6 +320,7 @@ async function fetchRoms() { const fetchedRoms = await romsStore.fetchRoms({ galleryFilter: galleryFilterStore, + platformsStore: platformsStore, concat: false, }); diff --git a/frontend/src/layouts/Main.vue b/frontend/src/layouts/Main.vue index 37970e9f5..e453e8d4c 100644 --- a/frontend/src/layouts/Main.vue +++ b/frontend/src/layouts/Main.vue @@ -23,7 +23,6 @@ import NewVersionDialog from "@/components/common/NewVersionDialog.vue"; import Notification from "@/components/common/Notifications/Notification.vue"; import UploadProgress from "@/components/common/Notifications/UploadProgress.vue"; import storeCollections from "@/stores/collections"; -import storeGalleryFilter from "@/stores/galleryFilter"; import storeNavigation from "@/stores/navigation"; import storePlatforms from "@/stores/platforms"; import type { Events } from "@/types/emitter"; @@ -31,11 +30,10 @@ import type { Events } from "@/types/emitter"; const navigationStore = storeNavigation(); const platformsStore = storePlatforms(); const collectionsStore = storeCollections(); -const galleryFilterStore = storeGalleryFilter(); const emitter = inject>("emitter"); emitter?.on("refreshDrawer", async () => { - platformsStore.fetchPlatforms({ galleryFilter: galleryFilterStore }); + platformsStore.fetchPlatforms(); }); const showVirtualCollections = useLocalStorage( @@ -58,7 +56,7 @@ function unhackNavbar() { onBeforeMount(async () => { document.addEventListener("network-quiesced", unhackNavbar); - platformsStore.fetchPlatforms({ galleryFilter: galleryFilterStore }); + platformsStore.fetchPlatforms(); collectionsStore.fetchCollections(); collectionsStore.fetchSmartCollections(); if (showVirtualCollections) { diff --git a/frontend/src/stores/platforms.ts b/frontend/src/stores/platforms.ts index 5915b56f3..6c028c0b8 100644 --- a/frontend/src/stores/platforms.ts +++ b/frontend/src/stores/platforms.ts @@ -38,11 +38,7 @@ export default defineStore("platforms", { return a.name.localeCompare(b.name); }); }, - fetchPlatforms({ - galleryFilter, - }: { - galleryFilter: GalleryFilterStore; - }): Promise { + fetchPlatforms(): Promise { if (this.fetchingPlatforms) return Promise.resolve([]); this.fetchingPlatforms = true; @@ -51,7 +47,6 @@ export default defineStore("platforms", { .getPlatforms() .then(({ data: platforms }) => { this.allPlatforms = platforms; - galleryFilter.setFilterPlatforms(platforms); resolve(platforms); }) .catch((error) => { diff --git a/frontend/src/stores/roms.ts b/frontend/src/stores/roms.ts index 9af4f11da..680e6cb55 100644 --- a/frontend/src/stores/roms.ts +++ b/frontend/src/stores/roms.ts @@ -14,10 +14,12 @@ import { type SmartCollection, } from "@/stores/collections"; import storeGalleryFilter from "@/stores/galleryFilter"; +import storePlatforms from "@/stores/platforms"; import { type Platform } from "@/stores/platforms"; import type { ExtractPiniaStoreType } from "@/types"; type GalleryFilterStore = ExtractPiniaStoreType; +type PlatformsStore = ExtractPiniaStoreType; export type SimpleRom = SimpleRomSchema; export type SearchRom = SearchRomSchema; @@ -152,6 +154,7 @@ export default defineStore("roms", { _postFetchRoms( response: GetRomsResponse, galleryFilter: GalleryFilterStore, + platformsStore: PlatformsStore, concat: boolean, ) { const { items, offset, total, char_index, rom_id_index, filter_values } = @@ -170,6 +173,13 @@ export default defineStore("roms", { this.characterIndex = char_index; this.romIdIndex = rom_id_index; + // Set the list of platforms in the filter + galleryFilter.setFilterPlatforms( + platformsStore.allPlatforms.filter((p) => + filter_values.platforms.includes(p.id), + ), + ); + if (filter_values) { galleryFilter.setFilterCollections(filter_values.collections); galleryFilter.setFilterGenres(filter_values.genres); @@ -183,9 +193,11 @@ export default defineStore("roms", { }, async fetchRoms({ galleryFilter, + platformsStore, concat = true, }: { galleryFilter: GalleryFilterStore; + platformsStore: PlatformsStore; concat?: boolean; }): Promise { if (this.fetchingRoms) return Promise.resolve([]); @@ -205,10 +217,20 @@ export default defineStore("roms", { JSON.stringify(currentParams) !== JSON.stringify(currentRequestParams); if (paramsChanged) return; - this._postFetchRoms(response, galleryFilter, concat); + this._postFetchRoms( + response, + galleryFilter, + platformsStore, + concat, + ); }) .then((response) => { - this._postFetchRoms(response.data, galleryFilter, concat); + this._postFetchRoms( + response.data, + galleryFilter, + platformsStore, + concat, + ); resolve(response.data.items); }) .catch((error) => { diff --git a/frontend/src/views/Gallery/Collection/BaseCollection.vue b/frontend/src/views/Gallery/Collection/BaseCollection.vue index d535a0175..272a38407 100644 --- a/frontend/src/views/Gallery/Collection/BaseCollection.vue +++ b/frontend/src/views/Gallery/Collection/BaseCollection.vue @@ -15,6 +15,7 @@ import GameTable from "@/components/common/Game/VirtualTable.vue"; import { type CollectionType } from "@/stores/collections"; import storeGalleryFilter from "@/stores/galleryFilter"; import storeGalleryView from "@/stores/galleryView"; +import storePlatforms from "@/stores/platforms"; import storeRoms, { type SimpleRom } from "@/stores/roms"; import type { Events } from "@/types/emitter"; import { views } from "@/utils"; @@ -29,6 +30,7 @@ const galleryViewStore = storeGalleryView(); const galleryFilterStore = storeGalleryFilter(); const { scrolledToTop, currentView } = storeToRefs(galleryViewStore); const romsStore = storeRoms(); +const platformsStore = storePlatforms(); const { filteredRoms, selectedRoms, @@ -53,7 +55,10 @@ async function fetchRoms() { }); romsStore - .fetchRoms({ galleryFilter: galleryFilterStore }) + .fetchRoms({ + galleryFilter: galleryFilterStore, + platformsStore: platformsStore, + }) .then(() => { emitter?.emit("showLoadingDialog", { loading: false, diff --git a/frontend/src/views/Gallery/Platform.vue b/frontend/src/views/Gallery/Platform.vue index 9d8815842..75d1757ea 100644 --- a/frontend/src/views/Gallery/Platform.vue +++ b/frontend/src/views/Gallery/Platform.vue @@ -48,7 +48,10 @@ async function fetchRoms() { }); romsStore - .fetchRoms({ galleryFilter: galleryFilterStore }) + .fetchRoms({ + galleryFilter: galleryFilterStore, + platformsStore: platformsStore, + }) .then(() => { emitter?.emit("showLoadingDialog", { loading: false, diff --git a/frontend/src/views/Gallery/Search.vue b/frontend/src/views/Gallery/Search.vue index f2bb39a70..38d7e7717 100644 --- a/frontend/src/views/Gallery/Search.vue +++ b/frontend/src/views/Gallery/Search.vue @@ -13,6 +13,7 @@ import GameCard from "@/components/common/Game/Card/Base.vue"; import GameTable from "@/components/common/Game/VirtualTable.vue"; import storeGalleryFilter from "@/stores/galleryFilter"; import storeGalleryView from "@/stores/galleryView"; +import storePlatforms from "@/stores/platforms"; import storeRoms, { type SimpleRom } from "@/stores/roms"; import type { Events } from "@/types/emitter"; import { views } from "@/utils"; @@ -22,6 +23,7 @@ const { scrolledToTop, currentView } = storeToRefs(galleryViewStore); const galleryFilterStore = storeGalleryFilter(); const { searchTerm } = storeToRefs(galleryFilterStore); const romsStore = storeRoms(); +const platformsStore = storePlatforms(); const { filteredRoms, selectedRoms, @@ -100,7 +102,10 @@ function onGameTouchEnd() { function fetchRoms() { romsStore - .fetchRoms({ galleryFilter: galleryFilterStore }) + .fetchRoms({ + galleryFilter: galleryFilterStore, + platformsStore: platformsStore, + }) .catch((error) => { emitter?.emit("snackbarShow", { msg: `Couldn't fetch roms: ${error}`, From 2c0325cae127b566e5b41a84e10da13783ab793f Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Thu, 15 Jan 2026 12:53:01 -0500 Subject: [PATCH 05/10] changes form bot review --- backend/handler/database/roms_handler.py | 131 +++++++++-------------- frontend/src/stores/platforms.ts | 3 - 2 files changed, 52 insertions(+), 82 deletions(-) diff --git a/backend/handler/database/roms_handler.py b/backend/handler/database/roms_handler.py index 0e1dfb3e8..7bc1e9d02 100644 --- a/backend/handler/database/roms_handler.py +++ b/backend/handler/database/roms_handler.py @@ -22,6 +22,7 @@ from sqlalchemy import ( ) from sqlalchemy.orm import Query, Session, joinedload, noload, selectinload from sqlalchemy.sql.elements import KeyedColumnElement +from sqlalchemy.sql.selectable import Select from config import ROMM_DB_DRIVER from decorators.database import begin_session @@ -835,69 +836,6 @@ class DBRomsHandler(DBBaseHandler): .all() ) - @begin_session - def with_filter_values( - self, - query: Query, - session: Session = None, # type: ignore - ) -> dict: - ids_subq = query.with_only_columns(Rom.id).scalar_subquery() # type: ignore - - statement = ( - select( - RomMetadata.genres, - RomMetadata.franchises, - RomMetadata.collections, - RomMetadata.companies, - RomMetadata.game_modes, - RomMetadata.age_ratings, - RomMetadata.player_count, - Rom.regions, - Rom.languages, - Rom.platform_id, - ) - .select_from(Rom) - .join(RomMetadata, Rom.id == RomMetadata.rom_id) - .where(Rom.id.in_(ids_subq)) - ) - - genres = set() - franchises = set() - collections = set() - companies = set() - game_modes = set() - age_ratings = set() - player_counts = set() - regions = set() - languages = set() - platforms = set() - - for row in session.execute(statement): - g, f, cl, co, gm, ar, pc, rg, lg, pid = row - genres.update(g) - franchises.update(f) - collections.update(cl) - companies.update(co) - game_modes.update(gm) - age_ratings.update(ar) - player_counts.update(pc) - regions.update(rg) - languages.update(lg) - platforms.add(pid) - - return { - "genres": sorted(genres), - "franchises": sorted(franchises), - "collections": sorted(collections), - "companies": sorted(companies), - "game_modes": sorted(game_modes), - "age_ratings": sorted(age_ratings), - "player_counts": sorted(player_counts), - "regions": sorted(regions), - "languages": sorted(languages), - "platforms": sorted(platforms), - } - @begin_session @with_details def get_roms_by_fs_name( @@ -1291,24 +1229,11 @@ 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( + def _collect_filter_values( self, - session: Session = None, # type: ignore + session: Session, + statement: Select, ) -> dict: - statement = select( - RomMetadata.genres, - RomMetadata.franchises, - RomMetadata.collections, - RomMetadata.companies, - RomMetadata.game_modes, - RomMetadata.age_ratings, - RomMetadata.player_count, - Rom.regions, - Rom.languages, - Rom.platform_id, - ) - genres = set() franchises = set() collections = set() @@ -1345,3 +1270,51 @@ class DBRomsHandler(DBBaseHandler): "languages": sorted(languages), "platforms": sorted(platforms), } + + @begin_session + def with_filter_values( + self, + query: Query, + session: Session = None, # type: ignore + ) -> dict: + ids_subq = query.with_only_columns(Rom.id).scalar_subquery() # type: ignore + + statement = ( + select( + RomMetadata.genres, + RomMetadata.franchises, + RomMetadata.collections, + RomMetadata.companies, + RomMetadata.game_modes, + RomMetadata.age_ratings, + RomMetadata.player_count, + Rom.regions, + Rom.languages, + Rom.platform_id, + ) + .select_from(Rom) + .join(RomMetadata, Rom.id == RomMetadata.rom_id) + .where(Rom.id.in_(ids_subq)) + ) + + return self._collect_filter_values(session, statement) + + @begin_session + def get_rom_filters( + self, + session: Session = None, # type: ignore + ) -> dict: + statement = select( + RomMetadata.genres, + RomMetadata.franchises, + RomMetadata.collections, + RomMetadata.companies, + RomMetadata.game_modes, + RomMetadata.age_ratings, + RomMetadata.player_count, + Rom.regions, + Rom.languages, + Rom.platform_id, + ) + + return self._collect_filter_values(session, statement) diff --git a/frontend/src/stores/platforms.ts b/frontend/src/stores/platforms.ts index 6c028c0b8..a24735515 100644 --- a/frontend/src/stores/platforms.ts +++ b/frontend/src/stores/platforms.ts @@ -2,11 +2,8 @@ import { uniqBy } from "lodash"; import { defineStore } from "pinia"; import type { PlatformSchema } from "@/__generated__"; import platformApi from "@/services/api/platform"; -import storeGalleryFilter from "@/stores/galleryFilter"; -import type { ExtractPiniaStoreType } from "@/types"; export type Platform = PlatformSchema; -type GalleryFilterStore = ExtractPiniaStoreType; export default defineStore("platforms", { state: () => ({ From 5240bb2f71b04ba97d83a7f7ec3f888496a0cb58 Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Thu, 15 Jan 2026 13:15:29 -0500 Subject: [PATCH 06/10] more changes from bot review --- backend/endpoints/rom.py | 27 +++--------------------- backend/handler/database/roms_handler.py | 27 ++++++++++++++++-------- 2 files changed, 21 insertions(+), 33 deletions(-) diff --git a/backend/endpoints/rom.py b/backend/endpoints/rom.py index b99dc9438..79e2c93fb 100644 --- a/backend/endpoints/rom.py +++ b/backend/endpoints/rom.py @@ -481,24 +481,14 @@ def get_roms( filter_query = db_rom_handler.filter_roms( query=unfiltered_query, user_id=request.user.id, + platform_ids=platform_ids, collection_id=collection_id, virtual_collection_id=virtual_collection_id, smart_collection_id=smart_collection_id, search_term=search_term, ) query_filters = db_rom_handler.with_filter_values(query=filter_query) - filter_values = RomFiltersDict( - genres=query_filters["genres"], - franchises=query_filters["franchises"], - collections=query_filters["collections"], - companies=query_filters["companies"], - game_modes=query_filters["game_modes"], - age_ratings=query_filters["age_ratings"], - player_counts=query_filters["player_counts"], - regions=query_filters["regions"], - languages=query_filters["languages"], - platforms=query_filters["platforms"], - ) + filter_values = RomFiltersDict(**query_filters) # Get all ROM IDs in order for the additional data with sync_session.begin() as session: @@ -718,18 +708,7 @@ async def get_rom_filters(request: Request) -> RomFiltersDict: filters = db_rom_handler.get_rom_filters() - return RomFiltersDict( - genres=filters["genres"], - franchises=filters["franchises"], - collections=filters["collections"], - companies=filters["companies"], - game_modes=filters["game_modes"], - age_ratings=filters["age_ratings"], - player_counts=filters["player_counts"], - regions=filters["regions"], - languages=filters["languages"], - platforms=filters["platforms"], - ) + return RomFiltersDict(**filters) @protected_route( diff --git a/backend/handler/database/roms_handler.py b/backend/handler/database/roms_handler.py index 7bc1e9d02..2ab124b0f 100644 --- a/backend/handler/database/roms_handler.py +++ b/backend/handler/database/roms_handler.py @@ -1247,15 +1247,24 @@ class DBRomsHandler(DBBaseHandler): for row in session.execute(statement): g, f, cl, co, gm, ar, pc, rg, lg, pid = row - genres.update(g) - franchises.update(f) - collections.update(cl) - companies.update(co) - game_modes.update(gm) - age_ratings.update(ar) - player_counts.update(pc) - regions.update(rg) - languages.update(lg) + if g: + genres.update(g) + if f: + franchises.update(f) + if cl: + collections.update(cl) + if co: + companies.update(co) + if gm: + game_modes.update(gm) + if ar: + age_ratings.update(ar) + if pc: + player_counts.add(pc) + if rg: + regions.update(rg) + if lg: + languages.update(lg) platforms.add(pid) return { From 804547cc66f33c4476d9de3be69b889a03a55479 Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Thu, 15 Jan 2026 13:21:45 -0500 Subject: [PATCH 07/10] Only set the list of platforms on first fetch --- frontend/src/stores/roms.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/frontend/src/stores/roms.ts b/frontend/src/stores/roms.ts index 680e6cb55..d0e641c3f 100644 --- a/frontend/src/stores/roms.ts +++ b/frontend/src/stores/roms.ts @@ -173,12 +173,14 @@ export default defineStore("roms", { this.characterIndex = char_index; this.romIdIndex = rom_id_index; - // Set the list of platforms in the filter - galleryFilter.setFilterPlatforms( - platformsStore.allPlatforms.filter((p) => - filter_values.platforms.includes(p.id), - ), - ); + // Only set the list of platforms on first fetch + if (galleryFilter.filterPlatforms.length === 0) { + galleryFilter.setFilterPlatforms( + platformsStore.allPlatforms.filter((p) => + filter_values.platforms.includes(p.id), + ), + ); + } if (filter_values) { galleryFilter.setFilterCollections(filter_values.collections); From 7485639b413d256a9b7f233c16d8efa57aef48f3 Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Thu, 15 Jan 2026 17:47:06 -0500 Subject: [PATCH 08/10] keep panel open after filtering --- backend/endpoints/rom.py | 3 ++- .../src/components/Gallery/AppBar/common/FilterDrawer/Base.vue | 2 +- frontend/src/views/Gallery/Search.vue | 3 --- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/backend/endpoints/rom.py b/backend/endpoints/rom.py index 79e2c93fb..b206e9951 100644 --- a/backend/endpoints/rom.py +++ b/backend/endpoints/rom.py @@ -488,6 +488,7 @@ def get_roms( search_term=search_term, ) query_filters = db_rom_handler.with_filter_values(query=filter_query) + # trunk-ignore(mypy/typeddict-item) filter_values = RomFiltersDict(**query_filters) # Get all ROM IDs in order for the additional data @@ -707,7 +708,7 @@ async def get_rom_filters(request: Request) -> RomFiltersDict: from handler.database import db_rom_handler filters = db_rom_handler.get_rom_filters() - + # trunk-ignore(mypy/typeddict-item) return RomFiltersDict(**filters) diff --git a/frontend/src/components/Gallery/AppBar/common/FilterDrawer/Base.vue b/frontend/src/components/Gallery/AppBar/common/FilterDrawer/Base.vue index bd2b7af25..855b43e73 100644 --- a/frontend/src/components/Gallery/AppBar/common/FilterDrawer/Base.vue +++ b/frontend/src/components/Gallery/AppBar/common/FilterDrawer/Base.vue @@ -482,7 +482,7 @@ onMounted(async () => {