From 7f5130c9b8406b188ce3b8fb56b9740adab99290 Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Thu, 22 Jan 2026 13:47:55 -0500 Subject: [PATCH 1/2] Add NONE support for filter logic operators --- backend/endpoints/rom.py | 18 ++--- backend/handler/database/roms_handler.py | 36 +++++++-- .../AppBar/common/FilterDrawer/Base.vue | 79 ++++++++++++------- .../Dialog/CreateSmartCollection.vue | 34 ++++---- frontend/src/services/api/rom.ts | 18 ++--- frontend/src/services/cache/api.ts | 18 ++--- frontend/src/stores/galleryFilter.ts | 38 ++++----- 7 files changed, 145 insertions(+), 96 deletions(-) diff --git a/backend/endpoints/rom.py b/backend/endpoints/rom.py index 54b13bbf3..25c498b43 100644 --- a/backend/endpoints/rom.py +++ b/backend/endpoints/rom.py @@ -351,55 +351,55 @@ def get_roms( genres_logic: Annotated[ str, Query( - description="Logic operator for genres filter: 'any' (OR) or 'all' (AND).", + description="Logic operator for genres filter: 'any' (OR), 'all' (AND) or 'none' (NOT).", ), ] = "any", franchises_logic: Annotated[ str, Query( - description="Logic operator for franchises filter: 'any' (OR) or 'all' (AND).", + description="Logic operator for franchises filter: 'any' (OR), 'all' (AND) or 'none' (NOT).", ), ] = "any", collections_logic: Annotated[ str, Query( - description="Logic operator for collections filter: 'any' (OR) or 'all' (AND).", + description="Logic operator for collections filter: 'any' (OR), 'all' (AND) or 'none' (NOT).", ), ] = "any", companies_logic: Annotated[ str, Query( - description="Logic operator for companies filter: 'any' (OR) or 'all' (AND).", + description="Logic operator for companies filter: 'any' (OR), 'all' (AND) or 'none' (NOT).", ), ] = "any", age_ratings_logic: Annotated[ str, Query( - description="Logic operator for age ratings filter: 'any' (OR) or 'all' (AND).", + description="Logic operator for age ratings filter: 'any' (OR), 'all' (AND) or 'none' (NOT).", ), ] = "any", regions_logic: Annotated[ str, Query( - description="Logic operator for regions filter: 'any' (OR) or 'all' (AND).", + description="Logic operator for regions filter: 'any' (OR), 'all' (AND) or 'none' (NOT).", ), ] = "any", languages_logic: Annotated[ str, Query( - description="Logic operator for languages filter: 'any' (OR) or 'all' (AND).", + description="Logic operator for languages filter: 'any' (OR), 'all' (AND) or 'none' (NOT).", ), ] = "any", statuses_logic: Annotated[ str, Query( - description="Logic operator for statuses filter: 'any' (OR) or 'all' (AND).", + description="Logic operator for statuses filter: 'any' (OR), 'all' (AND) or 'none' (NOT).", ), ] = "any", player_counts_logic: Annotated[ str, Query( - description="Logic operator for player counts filter: 'any' (OR) or 'all' (AND).", + description="Logic operator for player counts filter: 'any' (OR), 'all' (AND) or 'none' (NOT).", ), ] = "any", order_by: Annotated[ diff --git a/backend/handler/database/roms_handler.py b/backend/handler/database/roms_handler.py index 99dee6b06..01b31befe 100644 --- a/backend/handler/database/roms_handler.py +++ b/backend/handler/database/roms_handler.py @@ -372,9 +372,12 @@ class DBRomsHandler(DBBaseHandler): session: Session, values: Sequence[str], match_all: bool = False, + match_none: bool = False, ) -> Query: + print("GENRES", match_all, match_none) op = json_array_contains_all if match_all else json_array_contains_any - return query.filter(op(RomMetadata.genres, values, session=session)) + condition = op(RomMetadata.genres, values, session=session) + return query.filter(~condition) if match_none else query.filter(condition) def _filter_by_franchises( self, @@ -383,9 +386,11 @@ class DBRomsHandler(DBBaseHandler): session: Session, values: Sequence[str], match_all: bool = False, + match_none: bool = False, ) -> Query: op = json_array_contains_all if match_all else json_array_contains_any - return query.filter(op(RomMetadata.franchises, values, session=session)) + condition = op(RomMetadata.franchises, values, session=session) + return query.filter(~condition) if match_none else query.filter(condition) def _filter_by_collections( self, @@ -394,9 +399,11 @@ class DBRomsHandler(DBBaseHandler): session: Session, values: Sequence[str], match_all: bool = False, + match_none: bool = False, ) -> Query: op = json_array_contains_all if match_all else json_array_contains_any - return query.filter(op(RomMetadata.collections, values, session=session)) + condition = op(RomMetadata.collections, values, session=session) + return query.filter(~condition) if match_none else query.filter(condition) def _filter_by_companies( self, @@ -405,9 +412,11 @@ class DBRomsHandler(DBBaseHandler): session: Session, values: Sequence[str], match_all: bool = False, + match_none: bool = False, ) -> Query: op = json_array_contains_all if match_all else json_array_contains_any - return query.filter(op(RomMetadata.companies, values, session=session)) + condition = op(RomMetadata.companies, values, session=session) + return query.filter(~condition) if match_none else query.filter(condition) def _filter_by_age_ratings( self, @@ -416,9 +425,11 @@ class DBRomsHandler(DBBaseHandler): session: Session, values: Sequence[str], match_all: bool = False, + match_none: bool = False, ) -> Query: op = json_array_contains_all if match_all else json_array_contains_any - return query.filter(op(RomMetadata.age_ratings, values, session=session)) + condition = op(RomMetadata.age_ratings, values, session=session) + return query.filter(~condition) if match_none else query.filter(condition) def _filter_by_status(self, query: Query, statuses: Sequence[str]): """Filter by one or more user statuses using OR logic.""" @@ -449,9 +460,11 @@ class DBRomsHandler(DBBaseHandler): session: Session, values: Sequence[str], match_all: bool = False, + match_none: bool = False, ) -> Query: op = json_array_contains_all if match_all else json_array_contains_any - return query.filter(op(Rom.regions, values, session=session)) + condition = op(Rom.regions, values, session=session) + return query.filter(~condition) if match_none else query.filter(condition) def _filter_by_languages( self, @@ -460,9 +473,11 @@ class DBRomsHandler(DBBaseHandler): session: Session, values: Sequence[str], match_all: bool = False, + match_none: bool = False, ) -> Query: op = json_array_contains_all if match_all else json_array_contains_any - return query.filter(op(Rom.languages, values, session=session)) + condition = op(Rom.languages, values, session=session) + return query.filter(~condition) if match_none else query.filter(condition) def _filter_by_player_counts( self, @@ -471,6 +486,7 @@ class DBRomsHandler(DBBaseHandler): session: Session, values: Sequence[str], match_all: bool = False, + match_none: bool = False, ) -> Query: return query.filter(RomMetadata.player_count.in_(values)) @@ -695,7 +711,11 @@ class DBRomsHandler(DBBaseHandler): for values, logic, filter_func in filters_to_apply: if values: query = filter_func( - query, session=session, values=values, match_all=(logic == "all") + query, + session=session, + values=values, + match_all=(logic == "all"), + match_none=(logic == "none"), ) # The RomUser table is already joined if user_id is set diff --git a/frontend/src/components/Gallery/AppBar/common/FilterDrawer/Base.vue b/frontend/src/components/Gallery/AppBar/common/FilterDrawer/Base.vue index bd2b7af25..484176d36 100644 --- a/frontend/src/components/Gallery/AppBar/common/FilterDrawer/Base.vue +++ b/frontend/src/components/Gallery/AppBar/common/FilterDrawer/Base.vue @@ -15,7 +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 storeGalleryFilter from "@/stores/galleryFilter"; +import storeGalleryFilter, { + type FilterLogicOperator, +} from "@/stores/galleryFilter"; import storePlatforms from "@/stores/platforms"; import storeRoms from "@/stores/roms"; import type { Events } from "@/types/emitter"; @@ -112,49 +114,49 @@ const onFilterChange = debounce( : null, genres: selectedGenres.value.length > 0 ? selectedGenres.value.join(",") : null, - genresLogic: selectedGenres.value.length > 1 ? genresLogic.value : null, + genresLogic: selectedGenres.value.length > 0 ? genresLogic.value : null, franchises: selectedFranchises.value.length > 0 ? selectedFranchises.value.join(",") : null, franchisesLogic: - selectedFranchises.value.length > 1 ? franchisesLogic.value : null, + selectedFranchises.value.length > 0 ? franchisesLogic.value : null, collections: selectedCollections.value.length > 0 ? selectedCollections.value.join(",") : null, collectionsLogic: - selectedCollections.value.length > 1 ? collectionsLogic.value : null, + selectedCollections.value.length > 0 ? collectionsLogic.value : null, companies: selectedCompanies.value.length > 0 ? selectedCompanies.value.join(",") : null, companiesLogic: - selectedCompanies.value.length > 1 ? companiesLogic.value : null, + selectedCompanies.value.length > 0 ? companiesLogic.value : null, ageRatings: selectedAgeRatings.value.length > 0 ? selectedAgeRatings.value.join(",") : null, ageRatingsLogic: - selectedAgeRatings.value.length > 1 ? ageRatingsLogic.value : null, + selectedAgeRatings.value.length > 0 ? ageRatingsLogic.value : null, regions: selectedRegions.value.length > 0 ? selectedRegions.value.join(",") : null, regionsLogic: - selectedRegions.value.length > 1 ? regionsLogic.value : null, + selectedRegions.value.length > 0 ? regionsLogic.value : null, languages: selectedLanguages.value.length > 0 ? selectedLanguages.value.join(",") : null, languagesLogic: - selectedLanguages.value.length > 1 ? languagesLogic.value : null, + selectedLanguages.value.length > 0 ? languagesLogic.value : null, statuses: selectedStatuses.value.length > 0 ? selectedStatuses.value.join(",") : null, statusesLogic: - selectedStatuses.value.length > 1 ? statusesLogic.value : null, + selectedStatuses.value.length > 0 ? statusesLogic.value : null, playerCounts: selectedPlayerCounts.value.length > 0 ? selectedPlayerCounts.value.join(",") @@ -184,7 +186,7 @@ const filters = [ selected: selectedGenres, items: filterGenres, logic: genresLogic, - setLogic: (logic: "any" | "all") => + setLogic: (logic: FilterLogicOperator) => galleryFilterStore.setGenresLogic(logic), }, { @@ -192,7 +194,7 @@ const filters = [ selected: selectedFranchises, items: filterFranchises, logic: franchisesLogic, - setLogic: (logic: "any" | "all") => + setLogic: (logic: FilterLogicOperator) => galleryFilterStore.setFranchisesLogic(logic), }, { @@ -200,7 +202,7 @@ const filters = [ selected: selectedCollections, items: filterCollections, logic: collectionsLogic, - setLogic: (logic: "any" | "all") => + setLogic: (logic: FilterLogicOperator) => galleryFilterStore.setCollectionsLogic(logic), }, { @@ -208,7 +210,7 @@ const filters = [ selected: selectedCompanies, items: filterCompanies, logic: companiesLogic, - setLogic: (logic: "any" | "all") => + setLogic: (logic: FilterLogicOperator) => galleryFilterStore.setCompaniesLogic(logic), }, { @@ -216,7 +218,7 @@ const filters = [ selected: selectedAgeRatings, items: filterAgeRatings, logic: ageRatingsLogic, - setLogic: (logic: "any" | "all") => + setLogic: (logic: FilterLogicOperator) => galleryFilterStore.setAgeRatingsLogic(logic), }, { @@ -224,7 +226,7 @@ const filters = [ selected: selectedRegions, items: filterRegions, logic: regionsLogic, - setLogic: (logic: "any" | "all") => + setLogic: (logic: FilterLogicOperator) => galleryFilterStore.setRegionsLogic(logic), }, { @@ -232,7 +234,7 @@ const filters = [ selected: selectedLanguages, items: filterLanguages, logic: languagesLogic, - setLogic: (logic: "any" | "all") => + setLogic: (logic: FilterLogicOperator) => galleryFilterStore.setLanguagesLogic(logic), }, { @@ -240,7 +242,7 @@ const filters = [ selected: selectedPlayerCounts, items: filterPlayerCounts, logic: playerCountsLogic, - setLogic: (logic: "any" | "all") => + setLogic: (logic: FilterLogicOperator) => galleryFilterStore.setPlayerCountsLogic(logic), }, { @@ -248,7 +250,7 @@ const filters = [ selected: selectedStatuses, items: filterStatuses, logic: statusesLogic, - setLogic: (logic: "any" | "all") => + setLogic: (logic: FilterLogicOperator) => galleryFilterStore.setStatusesLogic(logic), }, ]; @@ -377,7 +379,7 @@ onMounted(async () => { const genres = (urlGenres as string).split(",").filter((g) => g.trim()); galleryFilterStore.setSelectedFilterGenres(genres); if (urlGenresLogic !== undefined) { - galleryFilterStore.setGenresLogic(urlGenresLogic as "any" | "all"); + galleryFilterStore.setGenresLogic(urlGenresLogic as FilterLogicOperator); } } @@ -388,7 +390,7 @@ onMounted(async () => { galleryFilterStore.setSelectedFilterFranchises(franchises); if (urlFranchisesLogic !== undefined) { galleryFilterStore.setFranchisesLogic( - urlFranchisesLogic as "any" | "all", + urlFranchisesLogic as FilterLogicOperator, ); } } @@ -400,7 +402,7 @@ onMounted(async () => { galleryFilterStore.setSelectedFilterCollections(collections); if (urlCollectionsLogic !== undefined) { galleryFilterStore.setCollectionsLogic( - urlCollectionsLogic as "any" | "all", + urlCollectionsLogic as FilterLogicOperator, ); } } @@ -411,7 +413,9 @@ onMounted(async () => { .filter((c) => c.trim()); galleryFilterStore.setSelectedFilterCompanies(companies); if (urlCompaniesLogic !== undefined) { - galleryFilterStore.setCompaniesLogic(urlCompaniesLogic as "any" | "all"); + galleryFilterStore.setCompaniesLogic( + urlCompaniesLogic as FilterLogicOperator, + ); } } @@ -422,7 +426,7 @@ onMounted(async () => { galleryFilterStore.setSelectedFilterAgeRatings(ageRatings); if (urlAgeRatingsLogic !== undefined) { galleryFilterStore.setAgeRatingsLogic( - urlAgeRatingsLogic as "any" | "all", + urlAgeRatingsLogic as FilterLogicOperator, ); } } @@ -431,7 +435,9 @@ onMounted(async () => { const regions = (urlRegions as string).split(",").filter((r) => r.trim()); galleryFilterStore.setSelectedFilterRegions(regions); if (urlRegionsLogic !== undefined) { - galleryFilterStore.setRegionsLogic(urlRegionsLogic as "any" | "all"); + galleryFilterStore.setRegionsLogic( + urlRegionsLogic as FilterLogicOperator, + ); } } @@ -441,7 +447,9 @@ onMounted(async () => { .filter((l) => l.trim()); galleryFilterStore.setSelectedFilterLanguages(languages); if (urlLanguagesLogic !== undefined) { - galleryFilterStore.setLanguagesLogic(urlLanguagesLogic as "any" | "all"); + galleryFilterStore.setLanguagesLogic( + urlLanguagesLogic as FilterLogicOperator, + ); } } @@ -449,7 +457,9 @@ onMounted(async () => { const statuses = (urlStatuses as string).split(",").filter((s) => s.trim()); galleryFilterStore.setSelectedFilterStatuses(statuses); if (urlStatusesLogic !== undefined) { - galleryFilterStore.setStatusesLogic(urlStatusesLogic as "any" | "all"); + galleryFilterStore.setStatusesLogic( + urlStatusesLogic as FilterLogicOperator, + ); } } @@ -460,7 +470,7 @@ onMounted(async () => { galleryFilterStore.setSelectedFilterPlayerCounts(playerCounts); if (urlPlayerCountsLogic !== undefined) { galleryFilterStore.setPlayerCountsLogic( - urlPlayerCountsLogic as "any" | "all", + urlPlayerCountsLogic as FilterLogicOperator, ); } } @@ -568,7 +578,7 @@ onMounted(async () => { > @@ -579,7 +589,18 @@ onMounted(async () => { > + + + diff --git a/frontend/src/components/common/Collection/Dialog/CreateSmartCollection.vue b/frontend/src/components/common/Collection/Dialog/CreateSmartCollection.vue index d27663c8e..1ce4880d5 100644 --- a/frontend/src/components/common/Collection/Dialog/CreateSmartCollection.vue +++ b/frontend/src/components/common/Collection/Dialog/CreateSmartCollection.vue @@ -11,12 +11,14 @@ import { ROUTES } from "@/plugins/router"; import collectionApi from "@/services/api/collection"; import storeCollections from "@/stores/collections"; import storeGalleryFilter from "@/stores/galleryFilter"; +import storeRoms from "@/stores/roms"; import type { Events } from "@/types/emitter"; import { getStatusKeyForText } from "@/utils"; const { t } = useI18n(); const galleryFilterStore = storeGalleryFilter(); const collectionsStore = storeCollections(); +const romsStore = storeRoms(); const { mdAndUp } = useDisplay(); const router = useRouter(); const show = ref(false); @@ -24,6 +26,7 @@ const name = ref(""); const description = ref(""); const isPublic = ref(false); +const { currentPlatform } = storeToRefs(romsStore); const { searchTerm, filterMatched, @@ -70,10 +73,13 @@ const filterSummary = computed(() => { const filters = []; if (searchTerm.value) filters.push(`Search: "${searchTerm.value}"`); - if (selectedPlatforms.value && selectedPlatforms.value.length > 0) + if (selectedPlatforms.value && selectedPlatforms.value.length > 0) { filters.push( - `Platforms: ${selectedPlatforms.value.map((p) => p.name).join(", ")}`, + `Platforms: ${selectedPlatforms.value.map((p) => p.display_name).join(", ")}`, ); + } else if (currentPlatform.value) { + filters.push(`Platform: ${currentPlatform.value.display_name}`); + } if (filterMatched.value) filters.push("Matched only"); if (filterFavorites.value) filters.push("Favorites"); if (filterDuplicates.value) filters.push("Duplicates"); @@ -125,14 +131,14 @@ async function createSmartCollection() { emitter?.emit("showLoadingDialog", { loading: true, scrim: true }); try { - const filterCriteria: Record< - string, - number | boolean | string | string[] | number[] | (string | null)[] | null - > = {}; + const filterCriteria: Record = {}; if (searchTerm.value) filterCriteria.search_term = searchTerm.value; - if (selectedPlatforms.value && selectedPlatforms.value.length > 0) + if (selectedPlatforms.value && selectedPlatforms.value.length > 0) { filterCriteria.platform_ids = selectedPlatforms.value.map((p) => p.id); + } else if (currentPlatform.value) { + filterCriteria.platform_ids = [currentPlatform.value.id]; + } if (filterMatched.value) filterCriteria.matched = true; if (filterFavorites.value) filterCriteria.favorite = true; if (filterDuplicates.value) filterCriteria.duplicate = true; @@ -142,27 +148,27 @@ async function createSmartCollection() { if (filterVerified.value) filterCriteria.verified = true; if (selectedGenres.value && selectedGenres.value.length > 0) { filterCriteria.genres = selectedGenres.value; - if (selectedGenres.value.length > 1) + if (selectedGenres.value.length > 0) filterCriteria.genres_logic = genresLogic.value; } if (selectedFranchises.value && selectedFranchises.value.length > 0) { filterCriteria.franchises = selectedFranchises.value; - if (selectedFranchises.value.length > 1) + if (selectedFranchises.value.length > 0) filterCriteria.franchises_logic = franchisesLogic.value; } if (selectedCollections.value && selectedCollections.value.length > 0) { filterCriteria.collections = selectedCollections.value; - if (selectedCollections.value.length > 1) + if (selectedCollections.value.length > 0) filterCriteria.collections_logic = collectionsLogic.value; } if (selectedCompanies.value && selectedCompanies.value.length > 0) { filterCriteria.companies = selectedCompanies.value; - if (selectedCompanies.value.length > 1) + if (selectedCompanies.value.length > 0) filterCriteria.companies_logic = companiesLogic.value; } if (selectedAgeRatings.value && selectedAgeRatings.value.length > 0) { filterCriteria.age_ratings = selectedAgeRatings.value; - if (selectedAgeRatings.value.length > 1) + if (selectedAgeRatings.value.length > 0) filterCriteria.age_ratings_logic = ageRatingsLogic.value; } if (selectedStatuses.value && selectedStatuses.value.length > 0) { @@ -176,12 +182,12 @@ async function createSmartCollection() { } if (selectedRegions.value && selectedRegions.value.length > 0) { filterCriteria.regions = selectedRegions.value; - if (selectedRegions.value.length > 1) + if (selectedRegions.value.length > 0) filterCriteria.regions_logic = regionsLogic.value; } if (selectedLanguages.value && selectedLanguages.value.length > 0) { filterCriteria.languages = selectedLanguages.value; - if (selectedLanguages.value.length > 1) + if (selectedLanguages.value.length > 0) filterCriteria.languages_logic = languagesLogic.value; } diff --git a/frontend/src/services/api/rom.ts b/frontend/src/services/api/rom.ts index 7fcde968e..d7ea00584 100644 --- a/frontend/src/services/api/rom.ts +++ b/frontend/src/services/api/rom.ts @@ -188,39 +188,39 @@ async function getRoms({ : undefined, // Logic operators genres_logic: - selectedGenres && selectedGenres.length > 1 + selectedGenres && selectedGenres.length > 0 ? genresLogic || "any" : undefined, franchises_logic: - selectedFranchises && selectedFranchises.length > 1 + selectedFranchises && selectedFranchises.length > 0 ? franchisesLogic || "any" : undefined, collections_logic: - selectedCollections && selectedCollections.length > 1 + selectedCollections && selectedCollections.length > 0 ? collectionsLogic || "any" : undefined, companies_logic: - selectedCompanies && selectedCompanies.length > 1 + selectedCompanies && selectedCompanies.length > 0 ? companiesLogic || "any" : undefined, age_ratings_logic: - selectedAgeRatings && selectedAgeRatings.length > 1 + selectedAgeRatings && selectedAgeRatings.length > 0 ? ageRatingsLogic || "any" : undefined, regions_logic: - selectedRegions && selectedRegions.length > 1 + selectedRegions && selectedRegions.length > 0 ? regionsLogic || "any" : undefined, languages_logic: - selectedLanguages && selectedLanguages.length > 1 + selectedLanguages && selectedLanguages.length > 0 ? languagesLogic || "any" : undefined, statuses_logic: - selectedStatuses && selectedStatuses.length > 1 + selectedStatuses && selectedStatuses.length > 0 ? statusesLogic || "any" : undefined, player_counts_logic: - selectedPlayerCounts && selectedPlayerCounts.length > 1 + selectedPlayerCounts && selectedPlayerCounts.length > 0 ? playerCountsLogic || "any" : undefined, ...(filterMatched !== null ? { matched: filterMatched } : {}), diff --git a/frontend/src/services/cache/api.ts b/frontend/src/services/cache/api.ts index f302bcffb..a1721bbdb 100644 --- a/frontend/src/services/cache/api.ts +++ b/frontend/src/services/cache/api.ts @@ -80,39 +80,39 @@ class CachedApiService { : undefined, // Logic operators genres_logic: - params.selectedGenres && params.selectedGenres.length > 1 + params.selectedGenres && params.selectedGenres.length > 0 ? params.genresLogic || "any" : undefined, franchises_logic: - params.selectedFranchises && params.selectedFranchises.length > 1 + params.selectedFranchises && params.selectedFranchises.length > 0 ? params.franchisesLogic || "any" : undefined, collections_logic: - params.selectedCollections && params.selectedCollections.length > 1 + params.selectedCollections && params.selectedCollections.length > 0 ? params.collectionsLogic || "any" : undefined, companies_logic: - params.selectedCompanies && params.selectedCompanies.length > 1 + params.selectedCompanies && params.selectedCompanies.length > 0 ? params.companiesLogic || "any" : undefined, age_ratings_logic: - params.selectedAgeRatings && params.selectedAgeRatings.length > 1 + params.selectedAgeRatings && params.selectedAgeRatings.length > 0 ? params.ageRatingsLogic || "any" : undefined, regions_logic: - params.selectedRegions && params.selectedRegions.length > 1 + params.selectedRegions && params.selectedRegions.length > 0 ? params.regionsLogic || "any" : undefined, languages_logic: - params.selectedLanguages && params.selectedLanguages.length > 1 + params.selectedLanguages && params.selectedLanguages.length > 0 ? params.languagesLogic || "any" : undefined, statuses_logic: - params.selectedStatuses && params.selectedStatuses.length > 1 + params.selectedStatuses && params.selectedStatuses.length > 0 ? params.statusesLogic || "any" : undefined, player_counts_logic: - params.selectedPlayerCounts && params.selectedPlayerCounts.length > 1 + params.selectedPlayerCounts && params.selectedPlayerCounts.length > 0 ? params.playerCountsLogic || "any" : undefined, ...(params.filterMatched !== null diff --git a/frontend/src/stores/galleryFilter.ts b/frontend/src/stores/galleryFilter.ts index f00c02f77..b7c2e26cd 100644 --- a/frontend/src/stores/galleryFilter.ts +++ b/frontend/src/stores/galleryFilter.ts @@ -15,6 +15,8 @@ export type FilterType = | "language" | "playerCount"; +export type FilterLogicOperator = "any" | "all" | "none"; + const defaultFilterState = { activeFilterDrawer: false, searchTerm: null as string | null, @@ -47,15 +49,15 @@ const defaultFilterState = { selectedPlayerCounts: [] 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", - playerCountsLogic: "any" as "any" | "all", + genresLogic: "any" as FilterLogicOperator, + franchisesLogic: "any" as FilterLogicOperator, + collectionsLogic: "any" as FilterLogicOperator, + companiesLogic: "any" as FilterLogicOperator, + ageRatingsLogic: "any" as FilterLogicOperator, + regionsLogic: "any" as FilterLogicOperator, + languagesLogic: "any" as FilterLogicOperator, + statusesLogic: "any" as FilterLogicOperator, + playerCountsLogic: "any" as FilterLogicOperator, }; export default defineStore("galleryFilter", { @@ -105,55 +107,55 @@ export default defineStore("galleryFilter", { setSelectedFilterGenres(genres: string[]) { this.selectedGenres = genres; }, - setGenresLogic(logic: "any" | "all") { + setGenresLogic(logic: FilterLogicOperator) { this.genresLogic = logic; }, setSelectedFilterFranchises(franchises: string[]) { this.selectedFranchises = franchises; }, - setFranchisesLogic(logic: "any" | "all") { + setFranchisesLogic(logic: FilterLogicOperator) { this.franchisesLogic = logic; }, setSelectedFilterCollections(collections: string[]) { this.selectedCollections = collections; }, - setCollectionsLogic(logic: "any" | "all") { + setCollectionsLogic(logic: FilterLogicOperator) { this.collectionsLogic = logic; }, setSelectedFilterCompanies(companies: string[]) { this.selectedCompanies = companies; }, - setCompaniesLogic(logic: "any" | "all") { + setCompaniesLogic(logic: FilterLogicOperator) { this.companiesLogic = logic; }, setSelectedFilterAgeRatings(ageRatings: string[]) { this.selectedAgeRatings = ageRatings; }, - setAgeRatingsLogic(logic: "any" | "all") { + setAgeRatingsLogic(logic: FilterLogicOperator) { this.ageRatingsLogic = logic; }, setSelectedFilterRegions(regions: string[]) { this.selectedRegions = regions; }, - setRegionsLogic(logic: "any" | "all") { + setRegionsLogic(logic: FilterLogicOperator) { this.regionsLogic = logic; }, setSelectedFilterLanguages(languages: string[]) { this.selectedLanguages = languages; }, - setLanguagesLogic(logic: "any" | "all") { + setLanguagesLogic(logic: FilterLogicOperator) { this.languagesLogic = logic; }, setSelectedFilterPlayerCounts(playerCounts: string[]) { this.selectedPlayerCounts = playerCounts; }, - setPlayerCountsLogic(logic: "any" | "all") { + setPlayerCountsLogic(logic: FilterLogicOperator) { this.playerCountsLogic = logic; }, setSelectedFilterStatuses(statuses: string[]) { this.selectedStatuses = statuses; }, - setStatusesLogic(logic: "any" | "all") { + setStatusesLogic(logic: FilterLogicOperator) { this.statusesLogic = logic; }, setFilterMatched(value: boolean | null) { From b49522e3e878a94b44a015ba53ede7daaea3a843 Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Thu, 22 Jan 2026 15:27:18 -0500 Subject: [PATCH 2/2] changes from bot review --- backend/handler/database/roms_handler.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/handler/database/roms_handler.py b/backend/handler/database/roms_handler.py index 01b31befe..205130f4c 100644 --- a/backend/handler/database/roms_handler.py +++ b/backend/handler/database/roms_handler.py @@ -374,7 +374,6 @@ class DBRomsHandler(DBBaseHandler): match_all: bool = False, match_none: bool = False, ) -> Query: - print("GENRES", match_all, match_none) op = json_array_contains_all if match_all else json_array_contains_any condition = op(RomMetadata.genres, values, session=session) return query.filter(~condition) if match_none else query.filter(condition) @@ -488,7 +487,10 @@ class DBRomsHandler(DBBaseHandler): match_all: bool = False, match_none: bool = False, ) -> Query: - return query.filter(RomMetadata.player_count.in_(values)) + condition = RomMetadata.player_count.in_(values) + if match_none: + return query.filter(not_(condition)) + return query.filter(condition) @begin_session def filter_roms(