{
:size="105"
class="mt-2"
/>
+
{
+ const searchParams = new URLSearchParams();
+
+ Object.keys(params).forEach((key) => {
+ const value = params[key];
+ if (Array.isArray(value)) {
+ // Handle arrays by repeating the parameter name (not adding [])
+ value.forEach((item) => {
+ if (item !== undefined && item !== null) {
+ searchParams.append(key, String(item));
+ }
+ });
+ } else if (value !== undefined && value !== null) {
+ searchParams.append(key, String(value));
+ }
+ });
+
+ return searchParams.toString();
+ },
+ },
+});
const inflightRequests = new Set();
diff --git a/frontend/src/services/api/rom.ts b/frontend/src/services/api/rom.ts
index 2a8a22807..4e5618877 100644
--- a/frontend/src/services/api/rom.ts
+++ b/frontend/src/services/api/rom.ts
@@ -10,7 +10,7 @@ import socket from "@/services/socket";
import storeHeartbeat from "@/stores/heartbeat";
import type { DetailedRom, SimpleRom, SearchRom } from "@/stores/roms";
import storeUpload from "@/stores/upload";
-import { getDownloadPath, getStatusKeyForText } from "@/utils";
+import { getDownloadPath } from "@/utils";
export const romApi = api;
@@ -61,7 +61,7 @@ async function uploadRoms({
}
export interface GetRomsParams {
- platformId?: number | null;
+ platformIds?: number[] | null;
collectionId?: number | null;
virtualCollectionId?: string | null;
smartCollectionId?: number | null;
@@ -70,27 +70,36 @@ export interface GetRomsParams {
offset?: number;
orderBy?: string | null;
orderDir?: string | null;
- filterUnmatched?: boolean;
- filterMatched?: boolean;
- filterFavorites?: boolean;
- filterDuplicates?: boolean;
- filterPlayables?: boolean;
- filterRA?: boolean;
- filterMissing?: boolean;
- filterVerified?: boolean;
+ filterMatched?: boolean | null;
+ filterFavorites?: boolean | null;
+ filterDuplicates?: boolean | null;
+ filterPlayables?: boolean | null;
+ filterRA?: boolean | null;
+ filterMissing?: boolean | null;
+ filterVerified?: boolean | null;
groupByMetaId?: boolean;
- selectedGenre?: string | null;
- selectedFranchise?: string | null;
- selectedCollection?: string | null;
- selectedCompany?: string | null;
- selectedAgeRating?: string | null;
- 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;
+ statusesLogic?: string | null;
}
async function getRoms({
- platformId = null,
+ platformIds = null,
collectionId = null,
virtualCollectionId = null,
smartCollectionId = null,
@@ -99,53 +108,118 @@ async function getRoms({
offset = 0,
orderBy = "name",
orderDir = "asc",
- filterUnmatched = false,
- filterMatched = false,
- filterFavorites = false,
- filterDuplicates = false,
- filterPlayables = false,
+ filterMatched = null,
+ filterFavorites = null,
+ filterDuplicates = null,
+ filterPlayables = null,
filterRA = false,
filterMissing = false,
filterVerified = false,
groupByMetaId = false,
- selectedGenre = null,
- selectedFranchise = null,
- selectedCollection = null,
- selectedCompany = null,
- selectedAgeRating = null,
- 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,
+ statusesLogic = null,
}: GetRomsParams): Promise<{ data: GetRomsResponse }> {
+ const params = {
+ platform_ids:
+ platformIds && platformIds.length > 0 ? platformIds : undefined,
+ 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,
+ genres:
+ selectedGenres && selectedGenres.length > 0 ? selectedGenres : undefined,
+ franchises:
+ selectedFranchises && selectedFranchises.length > 0
+ ? selectedFranchises
+ : undefined,
+ collections:
+ selectedCollections && selectedCollections.length > 0
+ ? selectedCollections
+ : undefined,
+ companies:
+ selectedCompanies && selectedCompanies.length > 0
+ ? selectedCompanies
+ : undefined,
+ age_ratings:
+ selectedAgeRatings && selectedAgeRatings.length > 0
+ ? selectedAgeRatings
+ : undefined,
+ selected_statuses:
+ selectedStatuses && selectedStatuses.length > 0
+ ? selectedStatuses
+ : undefined,
+ regions:
+ selectedRegions && selectedRegions.length > 0
+ ? selectedRegions
+ : undefined,
+ languages:
+ selectedLanguages && selectedLanguages.length > 0
+ ? selectedLanguages
+ : undefined,
+ // 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,
+ statuses_logic:
+ selectedStatuses && selectedStatuses.length > 1
+ ? statusesLogic || "any"
+ : undefined,
+ ...(filterMatched !== null ? { matched: filterMatched } : {}),
+ ...(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,
- selected_genre: selectedGenre,
- selected_franchise: selectedFranchise,
- selected_collection: selectedCollection,
- selected_company: selectedCompany,
- selected_age_rating: selectedAgeRating,
- selected_status: getStatusKeyForText(selectedStatus),
- selected_region: selectedRegion,
- selected_language: selectedLanguage,
- ...(filterUnmatched ? { matched: false } : {}),
- ...(filterMatched ? { matched: true } : {}),
- ...(filterFavorites ? { favorite: true } : {}),
- ...(filterDuplicates ? { duplicate: true } : {}),
- ...(filterPlayables ? { playable: true } : {}),
- ...(filterMissing ? { missing: true } : {}),
- ...(filterRA ? { has_ra: true } : {}),
- ...(filterVerified ? { verified: true } : {}),
- },
+ params,
});
}
@@ -170,6 +244,7 @@ async function getRecentPlayedRoms(): Promise<{ data: GetRomsResponse }> {
order_dir: "desc",
limit: RECENT_PLAYED_ROMS_LIMIT,
with_char_index: false,
+ last_played: true,
},
});
}
diff --git a/frontend/src/services/cache/api.ts b/frontend/src/services/cache/api.ts
index a97cbe92c..8138b17ed 100644
--- a/frontend/src/services/cache/api.ts
+++ b/frontend/src/services/cache/api.ts
@@ -8,7 +8,6 @@ import type {
import type { CustomLimitOffsetPage_SimpleRomSchema_ as GetRomsResponse } from "@/__generated__/models/CustomLimitOffsetPage_SimpleRomSchema_";
import type { GetRomsParams } from "@/services/api/rom";
import cacheService from "@/services/cache";
-import { getStatusKeyForText } from "@/utils";
class CachedApiService {
private createRequestConfig(
@@ -30,7 +29,10 @@ class CachedApiService {
onBackgroundUpdate: (data: GetRomsResponse) => void,
): Promise> {
const config = this.createRequestConfig("GET", "/roms", {
- platform_id: params.platformId,
+ platform_ids:
+ params.platformIds && params.platformIds.length > 0
+ ? params.platformIds
+ : undefined,
collection_id: params.collectionId,
virtual_collection_id: params.virtualCollectionId,
smart_collection_id: params.smartCollectionId,
@@ -40,22 +42,90 @@ class CachedApiService {
order_by: params.orderBy,
order_dir: params.orderDir,
group_by_meta_id: params.groupByMetaId,
- selected_genre: params.selectedGenre,
- selected_franchise: params.selectedFranchise,
- selected_collection: params.selectedCollection,
- selected_company: params.selectedCompany,
- selected_age_rating: params.selectedAgeRating,
- selected_status: getStatusKeyForText(params.selectedStatus ?? null),
- selected_region: params.selectedRegion,
- selected_language: params.selectedLanguage,
- ...(params.filterUnmatched ? { matched: false } : {}),
- ...(params.filterMatched ? { matched: true } : {}),
- ...(params.filterFavorites ? { favorite: true } : {}),
- ...(params.filterDuplicates ? { duplicate: true } : {}),
- ...(params.filterPlayables ? { playable: true } : {}),
- ...(params.filterMissing ? { missing: true } : {}),
- ...(params.filterRA ? { has_ra: true } : {}),
- ...(params.filterVerified ? { verified: true } : {}),
+ genres:
+ params.selectedGenres && params.selectedGenres.length > 0
+ ? params.selectedGenres
+ : undefined,
+ franchises:
+ params.selectedFranchises && params.selectedFranchises.length > 0
+ ? params.selectedFranchises
+ : undefined,
+ collections:
+ params.selectedCollections && params.selectedCollections.length > 0
+ ? params.selectedCollections
+ : undefined,
+ companies:
+ params.selectedCompanies && params.selectedCompanies.length > 0
+ ? params.selectedCompanies
+ : undefined,
+ age_ratings:
+ params.selectedAgeRatings && params.selectedAgeRatings.length > 0
+ ? params.selectedAgeRatings
+ : undefined,
+ selected_statuses:
+ params.selectedStatuses && params.selectedStatuses.length > 0
+ ? params.selectedStatuses
+ : undefined,
+ regions:
+ params.selectedRegions && params.selectedRegions.length > 0
+ ? params.selectedRegions
+ : undefined,
+ languages:
+ params.selectedLanguages && params.selectedLanguages.length > 0
+ ? params.selectedLanguages
+ : undefined,
+ // 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,
+ statuses_logic:
+ params.selectedStatuses && params.selectedStatuses.length > 1
+ ? params.statusesLogic || "any"
+ : undefined,
+ ...(params.filterMatched !== null
+ ? { matched: params.filterMatched }
+ : {}),
+ ...(params.filterFavorites !== null
+ ? { favorite: params.filterFavorites }
+ : {}),
+ ...(params.filterDuplicates !== null
+ ? { duplicate: params.filterDuplicates }
+ : {}),
+ ...(params.filterPlayables !== null
+ ? { playable: params.filterPlayables }
+ : {}),
+ ...(params.filterMissing !== null
+ ? { missing: params.filterMissing }
+ : {}),
+ ...(params.filterRA !== null ? { has_ra: params.filterRA } : {}),
+ ...(params.filterVerified !== null
+ ? { verified: params.filterVerified }
+ : {}),
});
return cacheService.request(config, onBackgroundUpdate);
@@ -82,6 +152,7 @@ class CachedApiService {
order_dir: "desc",
limit: 15,
with_char_index: false,
+ last_played: true,
});
return cacheService.request(config, onBackgroundUpdate);
@@ -107,6 +178,7 @@ class CachedApiService {
order_dir: "desc",
limit: 15,
with_char_index: false,
+ last_played: true,
});
}
diff --git a/frontend/src/stores/galleryFilter.ts b/frontend/src/stores/galleryFilter.ts
index f3152342a..af86b21a4 100644
--- a/frontend/src/stores/galleryFilter.ts
+++ b/frontend/src/stores/galleryFilter.ts
@@ -26,23 +26,32 @@ const defaultFilterState = {
filterRegions: [] as string[],
filterLanguages: [] as string[],
filterStatuses: Object.values(romStatusMap).map((status) => status.text),
- filterUnmatched: false,
- filterMatched: false,
- filterFavorites: false,
- filterDuplicates: false,
- filterPlayables: false,
- filterRA: false,
- filterMissing: false,
- filterVerified: false,
+ filterMatched: null as boolean | null, // null = all, true = matched, false = unmatched
+ filterFavorites: null as boolean | null, // null = all, true = favorites, false = not favorites
+ filterDuplicates: null as boolean | null, // null = all, true = duplicates, false = not duplicates
+ filterPlayables: null as boolean | null, // null = all, true = playables, false = not playables
+ filterRA: null as boolean | null, // null = all, true = has RA, false = no RA
+ 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,
- selectedGenre: null as string | null,
- selectedFranchise: null as string | null,
- selectedCollection: null as string | null,
- selectedCompany: null as string | null,
- selectedAgeRating: null as string | null,
- selectedRegion: null as string | null,
- selectedLanguage: null as string | null,
- selectedStatus: null as string | null,
+ selectedPlatforms: [] as Platform[],
+ selectedGenres: [] as string[],
+ selectedFranchises: [] as string[],
+ selectedCollections: [] as string[],
+ selectedCompanies: [] as string[],
+ selectedAgeRatings: [] as string[],
+ selectedRegions: [] as string[],
+ selectedLanguages: [] as string[],
+ selectedStatuses: [] as string[],
+ // Logic operators for multi-select filters
+ 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,101 +90,253 @@ export default defineStore("galleryFilter", {
? this.filterPlatforms.find((p) => p.id === platform.id) || null
: null;
},
- setSelectedFilterGenre(genre: string) {
- this.selectedGenre = genre;
+ setSelectedFilterPlatforms(platforms: Platform[]) {
+ this.selectedPlatforms = platforms;
+ // Clear single platform selection to avoid conflicts
+ this.selectedPlatform = null;
},
- setSelectedFilterFranchise(franchise: string) {
- this.selectedFranchise = franchise;
+ setSelectedFilterGenres(genres: string[]) {
+ this.selectedGenres = genres;
},
- setSelectedFilterCollection(collection: string) {
- this.selectedCollection = collection;
+ setGenresLogic(logic: "any" | "all") {
+ this.genresLogic = logic;
},
- setSelectedFilterCompany(company: string) {
- this.selectedCompany = company;
+ setSelectedFilterFranchises(franchises: string[]) {
+ this.selectedFranchises = franchises;
},
- setSelectedFilterAgeRating(ageRating: string) {
- this.selectedAgeRating = ageRating;
+ setFranchisesLogic(logic: "any" | "all") {
+ this.franchisesLogic = logic;
},
- setSelectedFilterRegion(region: string) {
- this.selectedRegion = region;
+ setSelectedFilterCollections(collections: string[]) {
+ this.selectedCollections = collections;
},
- setSelectedFilterLanguage(language: string) {
- this.selectedLanguage = language;
+ setCollectionsLogic(logic: "any" | "all") {
+ this.collectionsLogic = logic;
},
- setSelectedFilterStatus(status: string) {
- this.selectedStatus = status;
+ setSelectedFilterCompanies(companies: string[]) {
+ this.selectedCompanies = companies;
},
- setFilterUnmatched(value: boolean) {
- this.filterUnmatched = value;
- this.filterMatched = false;
+ setCompaniesLogic(logic: "any" | "all") {
+ this.companiesLogic = logic;
},
- switchFilterUnmatched() {
- this.filterUnmatched = !this.filterUnmatched;
- this.filterMatched = false;
+ setSelectedFilterAgeRatings(ageRatings: string[]) {
+ this.selectedAgeRatings = ageRatings;
},
- setFilterMatched(value: boolean) {
+ setAgeRatingsLogic(logic: "any" | "all") {
+ this.ageRatingsLogic = logic;
+ },
+ setSelectedFilterRegions(regions: string[]) {
+ this.selectedRegions = regions;
+ },
+ setRegionsLogic(logic: "any" | "all") {
+ this.regionsLogic = logic;
+ },
+ setSelectedFilterLanguages(languages: string[]) {
+ this.selectedLanguages = languages;
+ },
+ setLanguagesLogic(logic: "any" | "all") {
+ this.languagesLogic = logic;
+ },
+ setSelectedFilterStatuses(statuses: string[]) {
+ this.selectedStatuses = statuses;
+ },
+ setStatusesLogic(logic: "any" | "all") {
+ this.statusesLogic = logic;
+ },
+ setFilterMatched(value: boolean | null) {
this.filterMatched = value;
- this.filterUnmatched = false;
+ },
+ setFilterMatchedState(state: "all" | "matched" | "unmatched") {
+ switch (state) {
+ case "matched":
+ this.filterMatched = true;
+ break;
+ case "unmatched":
+ this.filterMatched = false;
+ break;
+ default: // "all"
+ this.filterMatched = null;
+ break;
+ }
},
switchFilterMatched() {
- this.filterMatched = !this.filterMatched;
- this.filterUnmatched = false;
+ if (this.filterMatched === null) {
+ this.filterMatched = true;
+ } else if (this.filterMatched === true) {
+ this.filterMatched = false;
+ } else {
+ this.filterMatched = null;
+ }
},
- setFilterFavorites(value: boolean) {
+ setFilterFavorites(value: boolean | null) {
this.filterFavorites = value;
},
- switchFilterFavorites() {
- this.filterFavorites = !this.filterFavorites;
+ setFilterFavoritesState(state: "all" | "favorites" | "not-favorites") {
+ switch (state) {
+ case "favorites":
+ this.filterFavorites = true;
+ break;
+ case "not-favorites":
+ this.filterFavorites = false;
+ break;
+ default: // "all"
+ this.filterFavorites = null;
+ break;
+ }
},
- setFilterDuplicates(value: boolean) {
+ switchFilterFavorites() {
+ if (this.filterFavorites === null) {
+ this.filterFavorites = true;
+ } else if (this.filterFavorites === true) {
+ this.filterFavorites = false;
+ } else {
+ this.filterFavorites = null;
+ }
+ },
+ setFilterDuplicates(value: boolean | null) {
this.filterDuplicates = value;
},
- switchFilterDuplicates() {
- this.filterDuplicates = !this.filterDuplicates;
+ setFilterDuplicatesState(state: "all" | "duplicates" | "not-duplicates") {
+ switch (state) {
+ case "duplicates":
+ this.filterDuplicates = true;
+ break;
+ case "not-duplicates":
+ this.filterDuplicates = false;
+ break;
+ default: // "all"
+ this.filterDuplicates = null;
+ break;
+ }
},
- setFilterPlayables(value: boolean) {
+ switchFilterDuplicates() {
+ if (this.filterDuplicates === null) {
+ this.filterDuplicates = true;
+ } else if (this.filterDuplicates === true) {
+ this.filterDuplicates = false;
+ } else {
+ this.filterDuplicates = null;
+ }
+ },
+ setFilterPlayables(value: boolean | null) {
this.filterPlayables = value;
},
- switchFilterPlayables() {
- this.filterPlayables = !this.filterPlayables;
+ setFilterPlayablesState(state: "all" | "playables" | "not-playables") {
+ switch (state) {
+ case "playables":
+ this.filterPlayables = true;
+ break;
+ case "not-playables":
+ this.filterPlayables = false;
+ break;
+ default: // "all"
+ this.filterPlayables = null;
+ break;
+ }
},
- setFilterRA(value: boolean) {
+ switchFilterPlayables() {
+ if (this.filterPlayables === null) {
+ this.filterPlayables = true;
+ } else if (this.filterPlayables === true) {
+ this.filterPlayables = false;
+ } else {
+ this.filterPlayables = null;
+ }
+ },
+ setFilterRA(value: boolean | null) {
this.filterRA = value;
},
- switchFilterRA() {
- this.filterRA = !this.filterRA;
+ setFilterRAState(state: "all" | "has-ra" | "no-ra") {
+ switch (state) {
+ case "has-ra":
+ this.filterRA = true;
+ break;
+ case "no-ra":
+ this.filterRA = false;
+ break;
+ default: // "all"
+ this.filterRA = null;
+ break;
+ }
},
- setFilterMissing(value: boolean) {
+ switchFilterRA() {
+ if (this.filterRA === null) {
+ this.filterRA = true;
+ } else if (this.filterRA === true) {
+ this.filterRA = false;
+ } else {
+ this.filterRA = null;
+ }
+ },
+ setFilterMissing(value: boolean | null) {
this.filterMissing = value;
},
- switchFilterMissing() {
- this.filterMissing = !this.filterMissing;
+ setFilterMissingState(state: "all" | "missing" | "not-missing") {
+ switch (state) {
+ case "missing":
+ this.filterMissing = true;
+ break;
+ case "not-missing":
+ this.filterMissing = false;
+ break;
+ default: // "all"
+ this.filterMissing = null;
+ break;
+ }
},
- setFilterVerified(value: boolean) {
+ switchFilterMissing() {
+ if (this.filterMissing === null) {
+ this.filterMissing = true;
+ } else if (this.filterMissing === true) {
+ this.filterMissing = false;
+ } else {
+ this.filterMissing = null;
+ }
+ },
+ setFilterVerified(value: boolean | null) {
this.filterVerified = value;
},
+ setFilterVerifiedState(state: "all" | "verified" | "not-verified") {
+ switch (state) {
+ case "verified":
+ this.filterVerified = true;
+ break;
+ case "not-verified":
+ this.filterVerified = false;
+ break;
+ default: // "all"
+ this.filterVerified = null;
+ break;
+ }
+ },
switchFilterVerified() {
- this.filterVerified = !this.filterVerified;
+ if (this.filterVerified === null) {
+ this.filterVerified = true;
+ } else if (this.filterVerified === true) {
+ this.filterVerified = false;
+ } else {
+ this.filterVerified = null;
+ }
},
isFiltered() {
return Boolean(
- this.filterUnmatched ||
- this.filterMatched ||
- this.filterFavorites ||
- this.filterDuplicates ||
- this.filterPlayables ||
- this.filterRA ||
- this.filterMissing ||
- this.filterVerified ||
- this.selectedPlatform ||
- this.selectedGenre ||
- this.selectedFranchise ||
- this.selectedCollection ||
- this.selectedCompany ||
- this.selectedAgeRating ||
- this.selectedRegion ||
- this.selectedLanguage ||
- this.selectedStatus,
+ this.filterMatched !== null ||
+ this.filterFavorites !== null ||
+ this.filterDuplicates !== null ||
+ this.filterPlayables !== null ||
+ this.filterRA !== null ||
+ this.filterMissing !== null ||
+ this.filterVerified !== null ||
+ this.selectedPlatform ||
+ this.selectedPlatforms.length > 0 ||
+ this.selectedGenres.length > 0 ||
+ this.selectedFranchises.length > 0 ||
+ this.selectedCollections.length > 0 ||
+ this.selectedCompanies.length > 0 ||
+ this.selectedAgeRatings.length > 0 ||
+ this.selectedRegions.length > 0 ||
+ this.selectedLanguages.length > 0 ||
+ this.selectedStatuses.length > 0,
);
},
reset() {
@@ -183,22 +344,31 @@ export default defineStore("galleryFilter", {
},
resetFilters() {
this.selectedPlatform = null;
- this.selectedGenre = null;
- this.selectedFranchise = null;
- this.selectedCollection = null;
- this.selectedCompany = null;
- this.selectedAgeRating = null;
- this.selectedRegion = null;
- this.selectedLanguage = null;
- this.selectedStatus = null;
- this.filterUnmatched = false;
- this.filterMatched = false;
- this.filterFavorites = false;
- this.filterDuplicates = false;
- this.filterPlayables = false;
- this.filterRA = false;
- this.filterMissing = false;
- this.filterVerified = false;
+ this.selectedPlatforms = [];
+ this.selectedGenres = [];
+ this.selectedFranchises = [];
+ this.selectedCollections = [];
+ this.selectedCompanies = [];
+ this.selectedAgeRatings = [];
+ this.selectedRegions = [];
+ this.selectedLanguages = [];
+ this.selectedStatuses = [];
+ this.filterMatched = null;
+ this.filterFavorites = null;
+ this.filterDuplicates = null;
+ this.filterPlayables = null;
+ this.filterRA = null;
+ this.filterMissing = null;
+ this.filterVerified = null;
+ // Reset logic operators to default
+ this.genresLogic = "any";
+ this.franchisesLogic = "any";
+ this.collectionsLogic = "any";
+ this.companiesLogic = "any";
+ this.ageRatingsLogic = "any";
+ this.regionsLogic = "any";
+ this.languagesLogic = "any";
+ this.statusesLogic = "any";
},
},
});
diff --git a/frontend/src/stores/roms.ts b/frontend/src/stores/roms.ts
index ca221352f..ccc5c4091 100644
--- a/frontend/src/stores/roms.ts
+++ b/frontend/src/stores/roms.ts
@@ -96,12 +96,23 @@ export default defineStore("roms", {
},
// Fetching multiple roms
_buildRequestParams(galleryFilter: GalleryFilterStore) {
- return {
- ...galleryFilter.$state,
- platformId:
+ // Determine platform IDs - use multiselect platforms if available, otherwise convert single platform to array
+ let platformIds: number[] | null = null;
+ if (galleryFilter.selectedPlatforms.length > 0) {
+ platformIds = galleryFilter.selectedPlatforms.map((p) => p.id);
+ } else {
+ const singlePlatformId =
this.currentPlatform?.id ??
galleryFilter.selectedPlatform?.id ??
- null,
+ null;
+ if (singlePlatformId) {
+ platformIds = [singlePlatformId];
+ }
+ }
+
+ const params = {
+ searchTerm: galleryFilter.searchTerm,
+ platformIds: platformIds,
collectionId: this.currentCollection?.id ?? null,
virtualCollectionId: this.currentVirtualCollection?.id ?? null,
smartCollectionId: this.currentSmartCollection?.id ?? null,
@@ -110,7 +121,32 @@ export default defineStore("roms", {
orderBy: this.orderBy,
orderDir: this.orderDir,
groupByMetaId: this._shouldGroupRoms() && this.onGalleryView,
+ filterMatched: galleryFilter.filterMatched,
+ filterFavorites: galleryFilter.filterFavorites,
+ filterDuplicates: galleryFilter.filterDuplicates,
+ filterPlayables: galleryFilter.filterPlayables,
+ filterRA: galleryFilter.filterRA,
+ filterMissing: galleryFilter.filterMissing,
+ filterVerified: galleryFilter.filterVerified,
+ selectedGenres: galleryFilter.selectedGenres,
+ selectedFranchises: galleryFilter.selectedFranchises,
+ selectedCollections: galleryFilter.selectedCollections,
+ selectedCompanies: galleryFilter.selectedCompanies,
+ selectedAgeRatings: galleryFilter.selectedAgeRatings,
+ selectedRegions: galleryFilter.selectedRegions,
+ selectedLanguages: galleryFilter.selectedLanguages,
+ selectedStatuses: galleryFilter.selectedStatuses,
+ // Logic operators
+ genresLogic: galleryFilter.genresLogic,
+ franchisesLogic: galleryFilter.franchisesLogic,
+ collectionsLogic: galleryFilter.collectionsLogic,
+ companiesLogic: galleryFilter.companiesLogic,
+ ageRatingsLogic: galleryFilter.ageRatingsLogic,
+ regionsLogic: galleryFilter.regionsLogic,
+ languagesLogic: galleryFilter.languagesLogic,
+ statusesLogic: galleryFilter.statusesLogic,
};
+ return params;
},
_postFetchRoms(response: GetRomsResponse, concat: boolean) {
const { items, offset, total, char_index, rom_id_index } = response;
diff --git a/frontend/src/views/GameDetails.vue b/frontend/src/views/GameDetails.vue
index 74c73e45d..4ffa839d1 100644
--- a/frontend/src/views/GameDetails.vue
+++ b/frontend/src/views/GameDetails.vue
@@ -277,15 +277,14 @@ watch(
-
-
+
+