Enhance multi-value filter functionality with logic operators for genres, franchises, collections, companies, age ratings, regions, and languages. Update API and frontend components to support new filter states and logic. Refactor gallery filter store and API service to accommodate multi-value selections and maintain backward compatibility with single-value filters. Improve UI for filter options in the gallery app, including logic toggle buttons for better user interaction.

This commit is contained in:
zurdi
2025-11-27 19:19:09 +00:00
parent c261d73776
commit 85e4f0a433
8 changed files with 728 additions and 107 deletions

View File

@@ -349,6 +349,56 @@ def get_roms(
str,
Query(description="Order direction, either 'asc' or 'desc'."),
] = "asc",
# Logic operators for multi-value filters
genres_logic: Annotated[
str,
Query(
description="Logic operator for genres filter: 'any' (OR) or 'all' (AND).",
alias="genres_logic",
),
] = "any",
franchises_logic: Annotated[
str,
Query(
description="Logic operator for franchises filter: 'any' (OR) or 'all' (AND).",
alias="franchises_logic",
),
] = "any",
collections_logic: Annotated[
str,
Query(
description="Logic operator for collections filter: 'any' (OR) or 'all' (AND).",
alias="collections_logic",
),
] = "any",
companies_logic: Annotated[
str,
Query(
description="Logic operator for companies filter: 'any' (OR) or 'all' (AND).",
alias="companies_logic",
),
] = "any",
age_ratings_logic: Annotated[
str,
Query(
description="Logic operator for age ratings filter: 'any' (OR) or 'all' (AND).",
alias="age_ratings_logic",
),
] = "any",
regions_logic: Annotated[
str,
Query(
description="Logic operator for regions filter: 'any' (OR) or 'all' (AND).",
alias="regions_logic",
),
] = "any",
languages_logic: Annotated[
str,
Query(
description="Logic operator for languages filter: 'any' (OR) or 'all' (AND).",
alias="languages_logic",
),
] = "any",
) -> CustomLimitOffsetPage[SimpleRomSchema]:
"""Retrieve roms."""
@@ -420,6 +470,14 @@ def get_roms(
selected_status=selected_status,
regions=regions,
languages=languages,
# Logic operators
genres_logic=genres_logic,
franchises_logic=franchises_logic,
collections_logic=collections_logic,
companies_logic=companies_logic,
age_ratings_logic=age_ratings_logic,
regions_logic=regions_logic,
languages_logic=languages_logic,
group_by_meta_id=group_by_meta_id,
)

View File

@@ -436,6 +436,14 @@ class DBRomsHandler(DBBaseHandler):
selected_status: str | None = None,
regions: Sequence[str] | None = None,
languages: Sequence[str] | None = None,
# Logic operators for multi-value filters
genres_logic: str = "any",
franchises_logic: str = "any",
collections_logic: str = "any",
companies_logic: str = "any",
age_ratings_logic: str = "any",
regions_logic: str = "any",
languages_logic: str = "any",
user_id: int | None = None,
session: Session = None,
) -> Query[Rom]:
@@ -597,25 +605,53 @@ class DBRomsHandler(DBBaseHandler):
# Apply metadata filters efficiently
if genres:
query = self.filter_by_genres(query, session=session, values=genres)
query = self.filter_by_genres(
query, session=session, values=genres, match_all=(genres_logic == "all")
)
if franchises:
query = self.filter_by_franchises(query, session=session, values=franchises)
query = self.filter_by_franchises(
query,
session=session,
values=franchises,
match_all=(franchises_logic == "all"),
)
if collections:
query = self.filter_by_collections(
query, session=session, values=collections
query,
session=session,
values=collections,
match_all=(collections_logic == "all"),
)
if companies:
query = self.filter_by_companies(query, session=session, values=companies)
query = self.filter_by_companies(
query,
session=session,
values=companies,
match_all=(companies_logic == "all"),
)
if age_ratings:
query = self.filter_by_age_ratings(
query, session=session, values=age_ratings
query,
session=session,
values=age_ratings,
match_all=(age_ratings_logic == "all"),
)
# Apply rom-level filters
if regions:
query = self.filter_by_regions(query, session=session, values=regions)
query = self.filter_by_regions(
query,
session=session,
values=regions,
match_all=(regions_logic == "all"),
)
if languages:
query = self.filter_by_languages(query, session=session, values=languages)
query = self.filter_by_languages(
query,
session=session,
values=languages,
match_all=(languages_logic == "all"),
)
# The RomUser table is already joined if user_id is set
if selected_status and user_id:

View File

@@ -53,21 +53,37 @@ const {
filterVerified,
selectedGenre,
filterGenres,
selectedGenres,
genresLogic,
selectedFranchise,
filterFranchises,
selectedFranchises,
franchisesLogic,
selectedCollection,
filterCollections,
selectedCollections,
collectionsLogic,
selectedCompany,
filterCompanies,
selectedCompanies,
companiesLogic,
selectedAgeRating,
filterAgeRatings,
selectedAgeRatings,
ageRatingsLogic,
selectedStatus,
filterStatuses,
selectedStatuses,
statusesLogic,
selectedPlatform,
selectedRegion,
filterRegions,
selectedRegions,
regionsLogic,
selectedLanguage,
filterLanguages,
selectedLanguages,
languagesLogic,
} = storeToRefs(galleryFilterStore);
const { filteredRoms } = storeToRefs(romsStore);
const { allPlatforms } = storeToRefs(platformsStore);
@@ -94,13 +110,58 @@ const onFilterChange = debounce(
? String(selectedPlatform.value.id)
: null,
genre: selectedGenre.value,
genres:
selectedGenres.value.length > 0 ? selectedGenres.value.join(",") : null,
genresLogic: selectedGenres.value.length > 1 ? genresLogic.value : null,
franchise: selectedFranchise.value,
franchises:
selectedFranchises.value.length > 0
? selectedFranchises.value.join(",")
: null,
franchisesLogic:
selectedFranchises.value.length > 1 ? franchisesLogic.value : null,
collection: selectedCollection.value,
collections:
selectedCollections.value.length > 0
? selectedCollections.value.join(",")
: null,
collectionsLogic:
selectedCollections.value.length > 1 ? collectionsLogic.value : null,
company: selectedCompany.value,
companies:
selectedCompanies.value.length > 0
? selectedCompanies.value.join(",")
: null,
companiesLogic:
selectedCompanies.value.length > 1 ? companiesLogic.value : null,
ageRating: selectedAgeRating.value,
ageRatings:
selectedAgeRatings.value.length > 0
? selectedAgeRatings.value.join(",")
: null,
ageRatingsLogic:
selectedAgeRatings.value.length > 1 ? ageRatingsLogic.value : null,
region: selectedRegion.value,
regions:
selectedRegions.value.length > 0
? selectedRegions.value.join(",")
: null,
regionsLogic:
selectedRegions.value.length > 1 ? regionsLogic.value : null,
language: selectedLanguage.value,
languages:
selectedLanguages.value.length > 0
? selectedLanguages.value.join(",")
: null,
languagesLogic:
selectedLanguages.value.length > 1 ? languagesLogic.value : null,
status: selectedStatus.value,
statuses:
selectedStatuses.value.length > 0
? selectedStatuses.value.join(",")
: null,
statusesLogic:
selectedStatuses.value.length > 1 ? statusesLogic.value : null,
}).forEach(([key, value]) => {
if (value) {
url.searchParams.set(key, value);
@@ -131,43 +192,67 @@ emitter?.on("filterRoms", onFilterChange);
const filters = [
{
label: t("platform.genre"),
selected: selectedGenre,
selected: selectedGenres,
items: filterGenres,
logic: genresLogic,
setLogic: (logic: "any" | "all") =>
galleryFilterStore.setGenresLogic(logic),
},
{
label: t("platform.franchise"),
selected: selectedFranchise,
selected: selectedFranchises,
items: filterFranchises,
logic: franchisesLogic,
setLogic: (logic: "any" | "all") =>
galleryFilterStore.setFranchisesLogic(logic),
},
{
label: t("platform.collection"),
selected: selectedCollection,
selected: selectedCollections,
items: filterCollections,
logic: collectionsLogic,
setLogic: (logic: "any" | "all") =>
galleryFilterStore.setCollectionsLogic(logic),
},
{
label: t("platform.company"),
selected: selectedCompany,
selected: selectedCompanies,
items: filterCompanies,
logic: companiesLogic,
setLogic: (logic: "any" | "all") =>
galleryFilterStore.setCompaniesLogic(logic),
},
{
label: t("platform.age-rating"),
selected: selectedAgeRating,
selected: selectedAgeRatings,
items: filterAgeRatings,
logic: ageRatingsLogic,
setLogic: (logic: "any" | "all") =>
galleryFilterStore.setAgeRatingsLogic(logic),
},
{
label: t("platform.region"),
selected: selectedRegion,
selected: selectedRegions,
items: filterRegions,
logic: regionsLogic,
setLogic: (logic: "any" | "all") =>
galleryFilterStore.setRegionsLogic(logic),
},
{
label: t("platform.language"),
selected: selectedLanguage,
selected: selectedLanguages,
items: filterLanguages,
logic: languagesLogic,
setLogic: (logic: "any" | "all") =>
galleryFilterStore.setLanguagesLogic(logic),
},
{
label: t("platform.status"),
selected: selectedStatus,
selected: selectedStatuses,
items: filterStatuses,
logic: statusesLogic,
setLogic: (logic: "any" | "all") =>
galleryFilterStore.setStatusesLogic(logic),
},
];
@@ -185,38 +270,47 @@ let searchFilteredRoms = ref<SimpleRom[]>([]);
async function fetchSearchFilteredRoms() {
try {
const params = {
searchTerm: searchTerm.value,
platformId: romsStore.currentPlatform?.id ?? null,
collectionId: romsStore.currentCollection?.id ?? null,
virtualCollectionId: romsStore.currentVirtualCollection?.id ?? null,
smartCollectionId: romsStore.currentSmartCollection?.id ?? null,
limit: romsStore.fetchLimit,
offset: romsStore.fetchOffset,
orderBy: romsStore.orderBy,
orderDir: romsStore.orderDir,
// Exclude all other filters
filterUnmatched: false,
filterMatched: false,
filterFavorites: null,
filterDuplicates: null,
filterPlayables: null,
filterRA: null,
filterMissing: null,
filterVerified: null,
// Single value filters - exclude
selectedGenre: null,
selectedFranchise: null,
selectedCollection: null,
selectedCompany: null,
selectedAgeRating: null,
selectedRegion: null,
selectedLanguage: null,
selectedStatus: null,
// Multi-value filters - exclude
selectedGenres: null,
selectedFranchises: null,
selectedCollections: null,
selectedCompanies: null,
selectedAgeRatings: null,
selectedRegions: null,
selectedLanguages: null,
};
// Fetch ROMs with only search term applied (and current platform/collection context)
const response = await cachedApiService.getRoms(
{
searchTerm: searchTerm.value,
platformId: romsStore.currentPlatform?.id ?? null,
collectionId: romsStore.currentCollection?.id ?? null,
virtualCollectionId: romsStore.currentVirtualCollection?.id ?? null,
smartCollectionId: romsStore.currentSmartCollection?.id ?? null,
limit: romsStore.fetchLimit,
offset: romsStore.fetchOffset,
orderBy: romsStore.orderBy,
orderDir: romsStore.orderDir,
// Exclude all other filters
filterUnmatched: false,
filterMatched: false,
filterFavorites: null,
filterDuplicates: null,
filterPlayables: null,
filterRA: null,
filterMissing: null,
filterVerified: null,
selectedGenre: null,
selectedFranchise: null,
selectedCollection: null,
selectedCompany: null,
selectedAgeRating: null,
selectedRegion: null,
selectedLanguage: null,
selectedStatus: null,
},
() => {}, // No background update callback needed
);
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);
@@ -280,6 +374,7 @@ onMounted(async () => {
filterVerified: urlFilteredVerified,
filterRA: urlFilteredRa,
platform: urlPlatform,
// Single value URL params (backward compatibility)
genre: urlGenre,
franchise: urlFranchise,
collection: urlCollection,
@@ -288,6 +383,23 @@ onMounted(async () => {
region: urlRegion,
language: urlLanguage,
status: urlStatus,
// Multi-value URL params
genres: urlGenres,
genresLogic: urlGenresLogic,
franchises: urlFranchises,
franchisesLogic: urlFranchisesLogic,
collections: urlCollections,
collectionsLogic: urlCollectionsLogic,
companies: urlCompanies,
companiesLogic: urlCompaniesLogic,
ageRatings: urlAgeRatings,
ageRatingsLogic: urlAgeRatingsLogic,
regions: urlRegions,
regionsLogic: urlRegionsLogic,
languages: urlLanguages,
languagesLogic: urlLanguagesLogic,
statuses: urlStatuses,
statusesLogic: urlStatusesLogic,
} = router.currentRoute.value.query;
// Check for query params to set filters
@@ -319,29 +431,102 @@ onMounted(async () => {
const platform = platformsStore.get(Number(urlPlatform));
if (platform) galleryFilterStore.setSelectedFilterPlatform(platform);
}
if (urlGenre !== undefined) {
galleryFilterStore.setSelectedFilterGenre(urlGenre as string);
// Check for query params to set multi-value filters (prioritize over single values)
if (urlGenres !== undefined) {
const genres = (urlGenres as string).split(",").filter((g) => g.trim());
galleryFilterStore.setSelectedFilterGenres(genres);
if (urlGenresLogic !== undefined) {
galleryFilterStore.setGenresLogic(urlGenresLogic as "any" | "all");
}
} else if (urlGenre !== undefined) {
// Backward compatibility: if single genre is set, convert to multiselect
galleryFilterStore.setSelectedFilterGenres([urlGenre as string]);
}
if (urlFranchise !== undefined) {
galleryFilterStore.setSelectedFilterFranchise(urlFranchise as string);
if (urlFranchises !== undefined) {
const franchises = (urlFranchises as string)
.split(",")
.filter((f) => f.trim());
galleryFilterStore.setSelectedFilterFranchises(franchises);
if (urlFranchisesLogic !== undefined) {
galleryFilterStore.setFranchisesLogic(
urlFranchisesLogic as "any" | "all",
);
}
} else if (urlFranchise !== undefined) {
galleryFilterStore.setSelectedFilterFranchises([urlFranchise as string]);
}
if (urlCollection !== undefined) {
galleryFilterStore.setSelectedFilterCollection(urlCollection as string);
if (urlCollections !== undefined) {
const collections = (urlCollections as string)
.split(",")
.filter((c) => c.trim());
galleryFilterStore.setSelectedFilterCollections(collections);
if (urlCollectionsLogic !== undefined) {
galleryFilterStore.setCollectionsLogic(
urlCollectionsLogic as "any" | "all",
);
}
} else if (urlCollection !== undefined) {
galleryFilterStore.setSelectedFilterCollections([urlCollection as string]);
}
if (urlCompany !== undefined) {
galleryFilterStore.setSelectedFilterCompany(urlCompany as string);
if (urlCompanies !== undefined) {
const companies = (urlCompanies as string)
.split(",")
.filter((c) => c.trim());
galleryFilterStore.setSelectedFilterCompanies(companies);
if (urlCompaniesLogic !== undefined) {
galleryFilterStore.setCompaniesLogic(urlCompaniesLogic as "any" | "all");
}
} else if (urlCompany !== undefined) {
galleryFilterStore.setSelectedFilterCompanies([urlCompany as string]);
}
if (urlAgeRating !== undefined) {
galleryFilterStore.setSelectedFilterAgeRating(urlAgeRating as string);
if (urlAgeRatings !== undefined) {
const ageRatings = (urlAgeRatings as string)
.split(",")
.filter((a) => a.trim());
galleryFilterStore.setSelectedFilterAgeRatings(ageRatings);
if (urlAgeRatingsLogic !== undefined) {
galleryFilterStore.setAgeRatingsLogic(
urlAgeRatingsLogic as "any" | "all",
);
}
} else if (urlAgeRating !== undefined) {
galleryFilterStore.setSelectedFilterAgeRatings([urlAgeRating as string]);
}
if (urlRegion !== undefined) {
galleryFilterStore.setSelectedFilterRegion(urlRegion as string);
if (urlRegions !== undefined) {
const regions = (urlRegions as string).split(",").filter((r) => r.trim());
galleryFilterStore.setSelectedFilterRegions(regions);
if (urlRegionsLogic !== undefined) {
galleryFilterStore.setRegionsLogic(urlRegionsLogic as "any" | "all");
}
} else if (urlRegion !== undefined) {
galleryFilterStore.setSelectedFilterRegions([urlRegion as string]);
}
if (urlLanguage !== undefined) {
galleryFilterStore.setSelectedFilterLanguage(urlLanguage as string);
if (urlLanguages !== undefined) {
const languages = (urlLanguages as string)
.split(",")
.filter((l) => l.trim());
galleryFilterStore.setSelectedFilterLanguages(languages);
if (urlLanguagesLogic !== undefined) {
galleryFilterStore.setLanguagesLogic(urlLanguagesLogic as "any" | "all");
}
} else if (urlLanguage !== undefined) {
galleryFilterStore.setSelectedFilterLanguages([urlLanguage as string]);
}
if (urlStatus !== undefined) {
galleryFilterStore.setSelectedFilterStatus(urlStatus as string);
if (urlStatuses !== undefined) {
const statuses = (urlStatuses as string).split(",").filter((s) => s.trim());
galleryFilterStore.setSelectedFilterStatuses(statuses);
if (urlStatusesLogic !== undefined) {
galleryFilterStore.setStatusesLogic(urlStatusesLogic as "any" | "all");
}
} else if (urlStatus !== undefined) {
galleryFilterStore.setSelectedFilterStatuses([urlStatus as string]);
}
// Check if search term is set in the URL (empty string is ok)
@@ -429,12 +614,16 @@ onMounted(async () => {
v-for="filter in filters"
:key="filter.label"
:tabindex="activeFilterDrawer ? 0 : -1"
class="py-2"
>
<v-select
v-model="filter.selected.value"
:tabindex="activeFilterDrawer ? 0 : -1"
hide-details
clearable
multiple
chips
closable-chips
:label="filter.label"
variant="outlined"
density="comfortable"
@@ -443,6 +632,24 @@ onMounted(async () => {
nextTick(() => emitter?.emit('filterRoms', null))
"
/>
<!-- AND/OR Logic Toggle -->
<v-btn-toggle
v-if="filter.selected.value.length > 1"
:model-value="filter.logic.value"
mandatory
variant="outlined"
density="compact"
class="mt-2"
@update:model-value="
(value) => {
filter.setLogic(value);
nextTick(() => emitter?.emit('filterRoms', null));
}
"
>
<v-btn value="any" size="small">{{ t("common.filter.any") }}</v-btn>
<v-btn value="all" size="small">{{ t("common.filter.all") }}</v-btn>
</v-btn-toggle>
</v-list-item>
<v-list-item
class="justify-center d-flex"

View File

@@ -57,5 +57,9 @@
"username-length": "Username must be between 3 and 255 characters",
"virtual-collection": "Autogenerated collection",
"virtual-collections": "Autogenerated collections",
"warning": "WARNING:"
"warning": "WARNING:",
"filter": {
"any": "Any",
"all": "All"
}
}

View File

@@ -79,6 +79,7 @@ export interface GetRomsParams {
filterMissing?: boolean | null;
filterVerified?: boolean | null;
groupByMetaId?: boolean;
// Single value filters (for backward compatibility)
selectedGenre?: string | null;
selectedFranchise?: string | null;
selectedCollection?: string | null;
@@ -87,6 +88,23 @@ export interface GetRomsParams {
selectedStatus?: string | null;
selectedRegion?: string | null;
selectedLanguage?: string | null;
// Multi-value filters
selectedGenres?: string[] | null;
selectedFranchises?: string[] | null;
selectedCollections?: string[] | null;
selectedCompanies?: string[] | null;
selectedAgeRatings?: string[] | null;
selectedRegions?: string[] | null;
selectedLanguages?: string[] | null;
selectedStatuses?: string[] | null;
// Logic operators for multi-value filters
genresLogic?: string | null;
franchisesLogic?: string | null;
collectionsLogic?: string | null;
companiesLogic?: string | null;
ageRatingsLogic?: string | null;
regionsLogic?: string | null;
languagesLogic?: string | null;
}
async function getRoms({
@@ -116,36 +134,93 @@ async function getRoms({
selectedStatus = null,
selectedRegion = null,
selectedLanguage = null,
selectedGenres = null,
selectedFranchises = null,
selectedCollections = null,
selectedCompanies = null,
selectedAgeRatings = null,
selectedRegions = null,
selectedLanguages = null,
selectedStatuses = null,
// Logic operators
genresLogic = null,
franchisesLogic = null,
collectionsLogic = null,
companiesLogic = null,
ageRatingsLogic = null,
regionsLogic = null,
languagesLogic = null,
}: GetRomsParams): Promise<{ data: GetRomsResponse }> {
// Helper function to handle single vs multi-value parameters
const getFilterArray = (
single: string | null,
multi: string[] | null,
): string[] | undefined => {
if (multi && multi.length > 0) return multi;
if (single) return [single];
return undefined;
};
const params = {
platform_id: platformId,
collection_id: collectionId,
virtual_collection_id: virtualCollectionId,
smart_collection_id: smartCollectionId,
search_term: searchTerm,
limit: limit,
offset: offset,
order_by: orderBy,
order_dir: orderDir,
group_by_meta_id: groupByMetaId,
genre: getFilterArray(selectedGenre, selectedGenres),
franchise: getFilterArray(selectedFranchise, selectedFranchises),
collection: getFilterArray(selectedCollection, selectedCollections),
company: getFilterArray(selectedCompany, selectedCompanies),
age_rating: getFilterArray(selectedAgeRating, selectedAgeRatings),
selected_status: getStatusKeyForText(selectedStatus),
region: getFilterArray(selectedRegion, selectedRegions),
language: getFilterArray(selectedLanguage, selectedLanguages),
// Logic operators
genres_logic:
selectedGenres && selectedGenres.length > 1
? genresLogic || "any"
: undefined,
franchises_logic:
selectedFranchises && selectedFranchises.length > 1
? franchisesLogic || "any"
: undefined,
collections_logic:
selectedCollections && selectedCollections.length > 1
? collectionsLogic || "any"
: undefined,
companies_logic:
selectedCompanies && selectedCompanies.length > 1
? companiesLogic || "any"
: undefined,
age_ratings_logic:
selectedAgeRatings && selectedAgeRatings.length > 1
? ageRatingsLogic || "any"
: undefined,
regions_logic:
selectedRegions && selectedRegions.length > 1
? regionsLogic || "any"
: undefined,
languages_logic:
selectedLanguages && selectedLanguages.length > 1
? languagesLogic || "any"
: undefined,
...(filterUnmatched ? { matched: false } : {}),
...(filterMatched ? { matched: true } : {}),
...(filterFavorites !== null ? { favorite: filterFavorites } : {}),
...(filterDuplicates !== null ? { duplicate: filterDuplicates } : {}),
...(filterPlayables !== null ? { playable: filterPlayables } : {}),
...(filterMissing !== null ? { missing: filterMissing } : {}),
...(filterRA !== null ? { has_ra: filterRA } : {}),
...(filterVerified !== null ? { verified: filterVerified } : {}),
};
return api.get(`/roms`, {
params: {
platform_id: platformId,
collection_id: collectionId,
virtual_collection_id: virtualCollectionId,
smart_collection_id: smartCollectionId,
search_term: searchTerm,
limit: limit,
offset: offset,
order_by: orderBy,
order_dir: orderDir,
group_by_meta_id: groupByMetaId,
genre: selectedGenre ? [selectedGenre] : undefined,
franchise: selectedFranchise ? [selectedFranchise] : undefined,
collection: selectedCollection ? [selectedCollection] : undefined,
company: selectedCompany ? [selectedCompany] : undefined,
age_rating: selectedAgeRating ? [selectedAgeRating] : undefined,
selected_status: getStatusKeyForText(selectedStatus),
region: selectedRegion ? [selectedRegion] : undefined,
language: selectedLanguage ? [selectedLanguage] : undefined,
...(filterUnmatched ? { matched: false } : {}),
...(filterMatched ? { matched: true } : {}),
...(filterFavorites !== null ? { favorite: filterFavorites } : {}),
...(filterDuplicates !== null ? { duplicate: filterDuplicates } : {}),
...(filterPlayables !== null ? { playable: filterPlayables } : {}),
...(filterMissing !== null ? { missing: filterMissing } : {}),
...(filterRA !== null ? { has_ra: filterRA } : {}),
...(filterVerified !== null ? { verified: filterVerified } : {}),
},
params,
});
}

View File

@@ -11,6 +11,22 @@ import cacheService from "@/services/cache";
import { getStatusKeyForText } from "@/utils";
class CachedApiService {
// Helper function to handle single vs multi-value parameters
private getFilterArray(
single: string | null | undefined,
multi: string[] | null | undefined,
): string[] | undefined {
const result = (() => {
if (multi && multi.length > 0) return multi;
if (single) return [single];
return undefined;
})();
// Only log non-empty results to reduce console noise
if (result) {
console.log("CachedApiService getFilterArray - result:", result);
}
return result;
}
private createRequestConfig(
method: Method,
url: string,
@@ -40,20 +56,61 @@ class CachedApiService {
order_by: params.orderBy,
order_dir: params.orderDir,
group_by_meta_id: params.groupByMetaId,
genre: params.selectedGenre ? [params.selectedGenre] : undefined,
franchise: params.selectedFranchise
? [params.selectedFranchise]
: undefined,
collection: params.selectedCollection
? [params.selectedCollection]
: undefined,
company: params.selectedCompany ? [params.selectedCompany] : undefined,
age_rating: params.selectedAgeRating
? [params.selectedAgeRating]
: undefined,
genre: this.getFilterArray(params.selectedGenre, params.selectedGenres),
franchise: this.getFilterArray(
params.selectedFranchise,
params.selectedFranchises,
),
collection: this.getFilterArray(
params.selectedCollection,
params.selectedCollections,
),
company: this.getFilterArray(
params.selectedCompany,
params.selectedCompanies,
),
age_rating: this.getFilterArray(
params.selectedAgeRating,
params.selectedAgeRatings,
),
selected_status: getStatusKeyForText(params.selectedStatus ?? null),
region: params.selectedRegion ? [params.selectedRegion] : undefined,
language: params.selectedLanguage ? [params.selectedLanguage] : undefined,
region: this.getFilterArray(
params.selectedRegion,
params.selectedRegions,
),
language: this.getFilterArray(
params.selectedLanguage,
params.selectedLanguages,
),
// Logic operators
genres_logic:
params.selectedGenres && params.selectedGenres.length > 1
? params.genresLogic || "any"
: undefined,
franchises_logic:
params.selectedFranchises && params.selectedFranchises.length > 1
? params.franchisesLogic || "any"
: undefined,
collections_logic:
params.selectedCollections && params.selectedCollections.length > 1
? params.collectionsLogic || "any"
: undefined,
companies_logic:
params.selectedCompanies && params.selectedCompanies.length > 1
? params.companiesLogic || "any"
: undefined,
age_ratings_logic:
params.selectedAgeRatings && params.selectedAgeRatings.length > 1
? params.ageRatingsLogic || "any"
: undefined,
regions_logic:
params.selectedRegions && params.selectedRegions.length > 1
? params.regionsLogic || "any"
: undefined,
languages_logic:
params.selectedLanguages && params.selectedLanguages.length > 1
? params.languagesLogic || "any"
: undefined,
...(params.filterUnmatched ? { matched: false } : {}),
...(params.filterMatched ? { matched: true } : {}),
...(params.filterFavorites !== null

View File

@@ -35,14 +35,33 @@ const defaultFilterState = {
filterMissing: null as boolean | null, // null = all, true = missing, false = not missing
filterVerified: null as boolean | null, // null = all, true = verified, false = not verified
selectedPlatform: null as Platform | null,
selectedPlatforms: [] as Platform[],
selectedGenre: null as string | null,
selectedGenres: [] as string[],
selectedFranchise: null as string | null,
selectedFranchises: [] as string[],
selectedCollection: null as string | null,
selectedCollections: [] as string[],
selectedCompany: null as string | null,
selectedCompanies: [] as string[],
selectedAgeRating: null as string | null,
selectedAgeRatings: [] as string[],
selectedRegion: null as string | null,
selectedRegions: [] as string[],
selectedLanguage: null as string | null,
selectedLanguages: [] as string[],
selectedStatus: null as string | null,
selectedStatuses: [] as string[],
// Logic operators for multi-select filters
platformsLogic: "any" as "any" | "all",
genresLogic: "any" as "any" | "all",
franchisesLogic: "any" as "any" | "all",
collectionsLogic: "any" as "any" | "all",
companiesLogic: "any" as "any" | "all",
ageRatingsLogic: "any" as "any" | "all",
regionsLogic: "any" as "any" | "all",
languagesLogic: "any" as "any" | "all",
statusesLogic: "any" as "any" | "all",
};
export default defineStore("galleryFilter", {
@@ -81,30 +100,84 @@ export default defineStore("galleryFilter", {
? this.filterPlatforms.find((p) => p.id === platform.id) || null
: null;
},
setSelectedFilterPlatforms(platforms: Platform[]) {
this.selectedPlatforms = platforms;
},
setPlatformsLogic(logic: "any" | "all") {
this.platformsLogic = logic;
},
setSelectedFilterGenre(genre: string) {
this.selectedGenre = genre;
},
setSelectedFilterGenres(genres: string[]) {
this.selectedGenres = genres;
},
setGenresLogic(logic: "any" | "all") {
this.genresLogic = logic;
},
setSelectedFilterFranchise(franchise: string) {
this.selectedFranchise = franchise;
},
setSelectedFilterFranchises(franchises: string[]) {
this.selectedFranchises = franchises;
},
setFranchisesLogic(logic: "any" | "all") {
this.franchisesLogic = logic;
},
setSelectedFilterCollection(collection: string) {
this.selectedCollection = collection;
},
setSelectedFilterCollections(collections: string[]) {
this.selectedCollections = collections;
},
setCollectionsLogic(logic: "any" | "all") {
this.collectionsLogic = logic;
},
setSelectedFilterCompany(company: string) {
this.selectedCompany = company;
},
setSelectedFilterCompanies(companies: string[]) {
this.selectedCompanies = companies;
},
setCompaniesLogic(logic: "any" | "all") {
this.companiesLogic = logic;
},
setSelectedFilterAgeRating(ageRating: string) {
this.selectedAgeRating = ageRating;
},
setSelectedFilterAgeRatings(ageRatings: string[]) {
this.selectedAgeRatings = ageRatings;
},
setAgeRatingsLogic(logic: "any" | "all") {
this.ageRatingsLogic = logic;
},
setSelectedFilterRegion(region: string) {
this.selectedRegion = region;
},
setSelectedFilterRegions(regions: string[]) {
this.selectedRegions = regions;
},
setRegionsLogic(logic: "any" | "all") {
this.regionsLogic = logic;
},
setSelectedFilterLanguage(language: string) {
this.selectedLanguage = language;
},
setSelectedFilterLanguages(languages: string[]) {
this.selectedLanguages = languages;
},
setLanguagesLogic(logic: "any" | "all") {
this.languagesLogic = logic;
},
setSelectedFilterStatus(status: string) {
this.selectedStatus = status;
},
setSelectedFilterStatuses(statuses: string[]) {
this.selectedStatuses = statuses;
},
setStatusesLogic(logic: "any" | "all") {
this.statusesLogic = logic;
},
setFilterUnmatched(value: boolean) {
this.filterUnmatched = value;
this.filterMatched = false;
@@ -282,14 +355,23 @@ export default defineStore("galleryFilter", {
this.filterMissing !== null ||
this.filterVerified !== null ||
this.selectedPlatform ||
this.selectedPlatforms.length > 0 ||
this.selectedGenre ||
this.selectedGenres.length > 0 ||
this.selectedFranchise ||
this.selectedFranchises.length > 0 ||
this.selectedCollection ||
this.selectedCollections.length > 0 ||
this.selectedCompany ||
this.selectedCompanies.length > 0 ||
this.selectedAgeRating ||
this.selectedAgeRatings.length > 0 ||
this.selectedRegion ||
this.selectedRegions.length > 0 ||
this.selectedLanguage ||
this.selectedStatus,
this.selectedLanguages.length > 0 ||
this.selectedStatus ||
this.selectedStatuses.length > 0,
);
},
reset() {
@@ -297,14 +379,23 @@ export default defineStore("galleryFilter", {
},
resetFilters() {
this.selectedPlatform = null;
this.selectedPlatforms = [];
this.selectedGenre = null;
this.selectedGenres = [];
this.selectedFranchise = null;
this.selectedFranchises = [];
this.selectedCollection = null;
this.selectedCollections = [];
this.selectedCompany = null;
this.selectedCompanies = [];
this.selectedAgeRating = null;
this.selectedAgeRatings = [];
this.selectedRegion = null;
this.selectedRegions = [];
this.selectedLanguage = null;
this.selectedLanguages = [];
this.selectedStatus = null;
this.selectedStatuses = [];
this.filterUnmatched = false;
this.filterMatched = false;
this.filterFavorites = null;
@@ -313,6 +404,16 @@ export default defineStore("galleryFilter", {
this.filterRA = null;
this.filterMissing = null;
this.filterVerified = null;
// Reset logic operators to default
this.platformsLogic = "any";
this.genresLogic = "any";
this.franchisesLogic = "any";
this.collectionsLogic = "any";
this.companiesLogic = "any";
this.ageRatingsLogic = "any";
this.regionsLogic = "any";
this.languagesLogic = "any";
this.statusesLogic = "any";
},
},
});

View File

@@ -96,8 +96,8 @@ export default defineStore("roms", {
},
// Fetching multiple roms
_buildRequestParams(galleryFilter: GalleryFilterStore) {
return {
...galleryFilter.$state,
const params = {
searchTerm: galleryFilter.searchTerm,
platformId:
this.currentPlatform?.id ??
galleryFilter.selectedPlatform?.id ??
@@ -110,7 +110,90 @@ export default defineStore("roms", {
orderBy: this.orderBy,
orderDir: this.orderDir,
groupByMetaId: this._shouldGroupRoms() && this.onGalleryView,
filterUnmatched: galleryFilter.filterUnmatched,
filterMatched: galleryFilter.filterMatched,
filterFavorites: galleryFilter.filterFavorites,
filterDuplicates: galleryFilter.filterDuplicates,
filterPlayables: galleryFilter.filterPlayables,
filterRA: galleryFilter.filterRA,
filterMissing: galleryFilter.filterMissing,
filterVerified: galleryFilter.filterVerified,
// Single value filters (backward compatibility - only use if multiselect is empty)
selectedGenre:
galleryFilter.selectedGenres.length > 0
? null
: galleryFilter.selectedGenre,
selectedFranchise:
galleryFilter.selectedFranchises.length > 0
? null
: galleryFilter.selectedFranchise,
selectedCollection:
galleryFilter.selectedCollections.length > 0
? null
: galleryFilter.selectedCollection,
selectedCompany:
galleryFilter.selectedCompanies.length > 0
? null
: galleryFilter.selectedCompany,
selectedAgeRating:
galleryFilter.selectedAgeRatings.length > 0
? null
: galleryFilter.selectedAgeRating,
selectedStatus:
galleryFilter.selectedStatuses.length > 0
? null
: galleryFilter.selectedStatus,
selectedRegion:
galleryFilter.selectedRegions.length > 0
? null
: galleryFilter.selectedRegion,
selectedLanguage:
galleryFilter.selectedLanguages.length > 0
? null
: galleryFilter.selectedLanguage,
// Multi-value filters
selectedGenres:
galleryFilter.selectedGenres.length > 0
? galleryFilter.selectedGenres
: null,
selectedFranchises:
galleryFilter.selectedFranchises.length > 0
? galleryFilter.selectedFranchises
: null,
selectedCollections:
galleryFilter.selectedCollections.length > 0
? galleryFilter.selectedCollections
: null,
selectedCompanies:
galleryFilter.selectedCompanies.length > 0
? galleryFilter.selectedCompanies
: null,
selectedAgeRatings:
galleryFilter.selectedAgeRatings.length > 0
? galleryFilter.selectedAgeRatings
: null,
selectedRegions:
galleryFilter.selectedRegions.length > 0
? galleryFilter.selectedRegions
: null,
selectedLanguages:
galleryFilter.selectedLanguages.length > 0
? galleryFilter.selectedLanguages
: null,
selectedStatuses:
galleryFilter.selectedStatuses.length > 0
? galleryFilter.selectedStatuses
: null,
// Logic operators
genresLogic: galleryFilter.genresLogic,
franchisesLogic: galleryFilter.franchisesLogic,
collectionsLogic: galleryFilter.collectionsLogic,
companiesLogic: galleryFilter.companiesLogic,
ageRatingsLogic: galleryFilter.ageRatingsLogic,
regionsLogic: galleryFilter.regionsLogic,
languagesLogic: galleryFilter.languagesLogic,
};
return params;
},
_postFetchRoms(response: GetRomsResponse, concat: boolean) {
const { items, offset, total, char_index, rom_id_index } = response;