Merge pull request #2904 from rommapp/roms-filter-endpoint

ROMs filter endpoint + refactor
This commit is contained in:
Georges-Antoine Assi
2026-01-16 19:14:37 -05:00
committed by GitHub
17 changed files with 332 additions and 203 deletions

View File

@@ -444,3 +444,16 @@ 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]
collections: list[str]
companies: list[str]
game_modes: list[str]
age_ratings: list[str]
player_counts: list[str]
regions: list[str]
languages: list[str]
platforms: list[int]

View File

@@ -43,6 +43,7 @@ from endpoints.responses import BulkOperationResponse
from endpoints.responses.rom import (
DetailedRomSchema,
RomFileSchema,
RomFiltersDict,
RomUserSchema,
SimpleRomSchema,
UserNoteSchema,
@@ -186,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
@@ -196,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."),
@@ -413,7 +418,7 @@ def get_roms(
] = None,
) -> 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(),
@@ -421,7 +426,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,
@@ -467,6 +472,33 @@ def get_roms(
)
char_index_dict = {char: index for (char, index) in char_index}
filter_values = RomFiltersDict(
genres=[],
franchises=[],
collections=[],
companies=[],
game_modes=[],
age_ratings=[],
player_counts=[],
regions=[],
languages=[],
platforms=[],
)
if with_filter_values:
# We use the unfiltered query so applied filters don't affect the list
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)
# trunk-ignore(mypy/typeddict-item)
filter_values = RomFiltersDict(**query_filters)
# 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
@@ -480,6 +512,7 @@ def get_roms(
additional_data={
"char_index": char_index_dict,
"rom_id_index": rom_id_index,
"filter_values": filter_values,
},
)
@@ -678,6 +711,15 @@ 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()
# trunk-ignore(mypy/typeddict-item)
return RomFiltersDict(**filters)
@protected_route(
router.get,
"/{id}",
@@ -1509,7 +1551,7 @@ async def update_rom_user(
@protected_route(
router.get,
"files/{id}",
"/files/{id}",
[Scope.ROMS_READ],
responses={status.HTTP_404_NOT_FOUND: {}},
)

View File

@@ -23,6 +23,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
@@ -207,12 +208,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
@@ -223,7 +224,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
@@ -236,7 +237,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
@@ -251,7 +252,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}%"),
@@ -259,7 +260,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:
@@ -279,7 +280,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."""
@@ -301,21 +302,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."""
@@ -329,19 +330,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",
@@ -364,7 +365,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,
*,
@@ -375,7 +376,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,
*,
@@ -386,7 +387,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,
*,
@@ -397,7 +398,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,
*,
@@ -408,7 +409,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,
*,
@@ -419,7 +420,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
@@ -441,7 +442,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,
*,
@@ -452,7 +453,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,
*,
@@ -463,7 +464,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,
*,
@@ -518,52 +519,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)
if updated_after:
query = query.filter(Rom.updated_at > updated_after)
@@ -681,14 +682,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:
@@ -699,7 +700,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))
@@ -1232,3 +1233,108 @@ 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))
def _collect_filter_values(
self,
session: Session,
statement: Select,
) -> dict:
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
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 {
"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
def with_filter_values(
self,
query: Query,
session: Session = None, # type: ignore
) -> dict:
"""
Returns the list of filters given the current subset of ROMs in the query
"""
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:
"""
Returns all filter values across all ROM metadata
"""
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)

View File

@@ -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';

View File

@@ -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<SimpleRomSchema>;
@@ -10,5 +11,6 @@ export type CustomLimitOffsetPage_SimpleRomSchema_ = {
offset: number;
char_index: Record<string, number>;
rom_id_index: Array<number>;
filter_values: RomFiltersDict;
};

View File

@@ -0,0 +1,17 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type RomFiltersDict = {
genres: Array<string>;
franchises: Array<string>;
collections: Array<string>;
companies: Array<string>;
game_modes: Array<string>;
age_ratings: Array<string>;
player_counts: Array<string>;
regions: Array<string>;
languages: Array<string>;
platforms: Array<number>;
};

View File

@@ -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,

View File

@@ -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 } from "vue";
import { useI18n } from "vue-i18n";
import { useRouter } from "vue-router";
import { useDisplay } from "vuetify";
@@ -15,10 +15,9 @@ 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 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(
@@ -79,13 +78,16 @@ const {
selectedPlayerCounts,
playerCountsLogic,
} = storeToRefs(galleryFilterStore);
const { allPlatforms } = storeToRefs(platformsStore);
const emitter = inject<Emitter<Events>>("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
@@ -174,16 +176,6 @@ const onFilterChange = debounce(
{ leading: false, trailing: true },
);
// Separate debounced function for search term changes
const onSearchChange = debounce(
async () => {
await fetchSearchFilteredRoms();
setFilters();
},
500,
{ leading: false, trailing: true },
);
emitter?.on("filterRoms", onFilterChange);
const filters = [
@@ -264,111 +256,10 @@ 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<SimpleRom[]>([]);
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
}
onMounted(async () => {
const {
search: urlSearch,
@@ -581,33 +472,10 @@ onMounted(async () => {
romsStore.resetPagination();
}
// Initial fetch of search-filtered ROMs for filter options
await fetchSearchFilteredRoms();
setFilters();
// 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 fetchSearchFilteredRoms();
setFilters();
},
{ immediate: false },
);
});
</script>

View File

@@ -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();
@@ -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", {
@@ -91,10 +95,13 @@ async function fetchRoms() {
}
function cleanupAll() {
romsStore.setLimit(MAX_FETCH_LIMIT);
romsStore.setLimit(10000);
galleryFilterStore.setFilterMissing(true);
romsStore
.fetchRoms({ galleryFilter: galleryFilterStore })
.fetchRoms({
galleryFilter: galleryFilterStore,
platformsStore: platformsStore,
})
.then(() => {
emitter?.emit("showLoadingDialog", {
loading: false,

View File

@@ -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<Events>>("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,
});
}
</script>

View File

@@ -320,6 +320,7 @@ async function fetchRoms() {
const fetchedRoms = await romsStore.fetchRoms({
galleryFilter: galleryFilterStore,
platformsStore: platformsStore,
concat: false,
});

View File

@@ -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";
@@ -246,6 +247,7 @@ async function getRecentRoms(): Promise<{ data: GetRomsResponse }> {
order_dir: "desc",
limit: RECENT_ROMS_LIMIT,
with_char_index: false,
with_filter_values: false,
},
});
}
@@ -257,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,
},
});
@@ -582,6 +585,10 @@ async function getRomNotes({
});
}
async function getRomFilters(): Promise<{ data: RomFiltersDict }> {
return api.get("/roms/filters");
}
export default {
uploadRoms,
getRoms,
@@ -601,4 +608,5 @@ export default {
updateRomNote,
deleteRomNote,
getRomNotes,
getRomFilters,
};

View File

@@ -147,6 +147,7 @@ class CachedApiService {
order_dir: "desc",
limit: 15,
with_char_index: false,
with_filter_values: false,
});
return cacheService.request<GetRomsResponse>(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,
});
}

View File

@@ -14,15 +14,16 @@ 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<typeof storeGalleryFilter>;
type PlatformsStore = ExtractPiniaStoreType<typeof storePlatforms>;
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");
@@ -150,8 +151,14 @@ 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,
platformsStore: PlatformsStore,
concat: boolean,
) {
const { items, offset, total, char_index, rom_id_index, filter_values } =
response;
if (!concat || this.fetchOffset === 0) {
this._allRoms = items;
} else {
@@ -165,12 +172,34 @@ export default defineStore("roms", {
// Set the character index for the current platform
this.characterIndex = char_index;
this.romIdIndex = rom_id_index;
// 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);
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,
platformsStore,
concat = true,
}: {
galleryFilter: GalleryFilterStore;
platformsStore: PlatformsStore;
concat?: boolean;
}): Promise<SimpleRom[]> {
if (this.fetchingRoms) return Promise.resolve([]);
@@ -190,10 +219,20 @@ export default defineStore("roms", {
JSON.stringify(currentParams) !==
JSON.stringify(currentRequestParams);
if (paramsChanged) return;
this._postFetchRoms(response, concat);
this._postFetchRoms(
response,
galleryFilter,
platformsStore,
concat,
);
})
.then((response) => {
this._postFetchRoms(response.data, concat);
this._postFetchRoms(
response.data,
galleryFilter,
platformsStore,
concat,
);
resolve(response.data.items);
})
.catch((error) => {

View File

@@ -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,

View File

@@ -48,7 +48,10 @@ async function fetchRoms() {
});
romsStore
.fetchRoms({ galleryFilter: galleryFilterStore })
.fetchRoms({
galleryFilter: galleryFilterStore,
platformsStore: platformsStore,
})
.then(() => {
emitter?.emit("showLoadingDialog", {
loading: false,

View File

@@ -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}`,
@@ -108,9 +113,6 @@ function fetchRoms() {
color: "red",
timeout: 4000,
});
})
.finally(() => {
galleryFilterStore.activeFilterDrawer = false;
});
}