mirror of
https://github.com/rommapp/romm.git
synced 2026-02-18 00:27:41 +01:00
Refactor gallery filter components to use toggle buttons for filter states, allowing null values for all filters. Update filter logic in the store and API services to accommodate new states. Enhance UI for better visibility and interaction with filter options in the gallery app.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import binascii
|
||||
import json
|
||||
import logging
|
||||
from base64 import b64encode
|
||||
from datetime import datetime, timezone
|
||||
from io import BytesIO
|
||||
@@ -258,9 +259,10 @@ def get_roms(
|
||||
description=(
|
||||
"Associated genre. Multiple values are allowed by repeating the"
|
||||
" parameter, and results that match any of the values will be"
|
||||
" returned."
|
||||
" returned. Maximum 50 values allowed."
|
||||
),
|
||||
alias="genre",
|
||||
max_length=50,
|
||||
),
|
||||
] = None,
|
||||
franchises: Annotated[
|
||||
@@ -269,9 +271,10 @@ def get_roms(
|
||||
description=(
|
||||
"Associated franchise. Multiple values are allowed by repeating"
|
||||
" the parameter, and results that match any of the values will"
|
||||
" be returned."
|
||||
" be returned. Maximum 50 values allowed."
|
||||
),
|
||||
alias="franchise",
|
||||
max_length=50,
|
||||
),
|
||||
] = None,
|
||||
collections: Annotated[
|
||||
@@ -280,9 +283,10 @@ def get_roms(
|
||||
description=(
|
||||
"Associated collection. Multiple values are allowed by"
|
||||
" repeating the parameter, and results that match any of the"
|
||||
" values will be returned."
|
||||
" values will be returned. Maximum 50 values allowed."
|
||||
),
|
||||
alias="collection",
|
||||
max_length=50,
|
||||
),
|
||||
] = None,
|
||||
companies: Annotated[
|
||||
@@ -291,9 +295,10 @@ def get_roms(
|
||||
description=(
|
||||
"Associated company. Multiple values are allowed by repeating"
|
||||
" the parameter, and results that match any of the values will"
|
||||
" be returned."
|
||||
" be returned. Maximum 50 values allowed."
|
||||
),
|
||||
alias="company",
|
||||
max_length=50,
|
||||
),
|
||||
] = None,
|
||||
age_ratings: Annotated[
|
||||
@@ -302,9 +307,10 @@ def get_roms(
|
||||
description=(
|
||||
"Associated age rating. Multiple values are allowed by"
|
||||
" repeating the parameter, and results that match any of the"
|
||||
" values will be returned."
|
||||
" values will be returned. Maximum 20 values allowed."
|
||||
),
|
||||
alias="age_rating",
|
||||
max_length=20,
|
||||
),
|
||||
] = None,
|
||||
selected_status: Annotated[
|
||||
@@ -317,9 +323,10 @@ def get_roms(
|
||||
description=(
|
||||
"Associated region tag. Multiple values are allowed by"
|
||||
" repeating the parameter, and results that match any of the"
|
||||
" values will be returned."
|
||||
" values will be returned. Maximum 30 values allowed."
|
||||
),
|
||||
alias="region",
|
||||
max_length=30,
|
||||
),
|
||||
] = None,
|
||||
languages: Annotated[
|
||||
@@ -328,9 +335,10 @@ def get_roms(
|
||||
description=(
|
||||
"Associated language tag. Multiple values are allowed by"
|
||||
" repeating the parameter, and results that match any of the"
|
||||
" values will be returned."
|
||||
" values will be returned. Maximum 30 values allowed."
|
||||
),
|
||||
alias="language",
|
||||
max_length=30,
|
||||
),
|
||||
] = None,
|
||||
order_by: Annotated[
|
||||
@@ -344,6 +352,43 @@ def get_roms(
|
||||
) -> CustomLimitOffsetPage[SimpleRomSchema]:
|
||||
"""Retrieve roms."""
|
||||
|
||||
# Sanitize and validate filter arrays
|
||||
def sanitize_filter_array(
|
||||
values: list[str] | None, max_length: int = 50, filter_name: str = ""
|
||||
) -> list[str] | None:
|
||||
"""Sanitize filter array by removing empty strings and limiting size."""
|
||||
if not values:
|
||||
return None
|
||||
|
||||
try:
|
||||
# Remove empty/whitespace-only values and strip whitespace
|
||||
sanitized = [
|
||||
v.strip() for v in values if v and isinstance(v, str) and v.strip()
|
||||
]
|
||||
if not sanitized:
|
||||
return None
|
||||
|
||||
# Limit array size to prevent abuse
|
||||
if len(sanitized) > max_length:
|
||||
logging.warning(
|
||||
f"Filter array '{filter_name}' truncated from {len(sanitized)} to {max_length} items"
|
||||
)
|
||||
sanitized = sanitized[:max_length]
|
||||
|
||||
return sanitized
|
||||
except Exception as e:
|
||||
logging.error(f"Error sanitizing filter array '{filter_name}': {e}")
|
||||
return None
|
||||
|
||||
# Apply sanitization with specific limits and names
|
||||
genres = sanitize_filter_array(genres, 50, "genres")
|
||||
franchises = sanitize_filter_array(franchises, 50, "franchises")
|
||||
collections = sanitize_filter_array(collections, 50, "collections")
|
||||
companies = sanitize_filter_array(companies, 50, "companies")
|
||||
age_ratings = sanitize_filter_array(age_ratings, 20, "age_ratings")
|
||||
regions = sanitize_filter_array(regions, 30, "regions")
|
||||
languages = sanitize_filter_array(languages, 30, "languages")
|
||||
|
||||
# Get the base roms query
|
||||
query, order_by_attr = db_rom_handler.get_roms_query(
|
||||
user_id=request.user.id,
|
||||
|
||||
@@ -213,36 +213,23 @@ class DBCollectionsHandler(DBBaseHandler):
|
||||
# Extract filter criteria
|
||||
criteria = smart_collection.filter_criteria
|
||||
|
||||
# Backwards compatibility with old criteria names
|
||||
genres = criteria.get("genres") or (
|
||||
[criteria["selected_genre"]] if criteria.get("selected_genre") else None
|
||||
)
|
||||
franchises = criteria.get("franchises") or (
|
||||
[criteria["selected_franchise"]]
|
||||
if criteria.get("selected_franchise")
|
||||
else None
|
||||
)
|
||||
collections = criteria.get("collections") or (
|
||||
[criteria["selected_collection"]]
|
||||
if criteria.get("selected_collection")
|
||||
else None
|
||||
)
|
||||
companies = criteria.get("companies") or (
|
||||
[criteria["selected_company"]] if criteria.get("selected_company") else None
|
||||
)
|
||||
age_ratings = criteria.get("age_ratings") or (
|
||||
[criteria["selected_age_rating"]]
|
||||
if criteria.get("selected_age_rating")
|
||||
else None
|
||||
)
|
||||
regions = criteria.get("regions") or (
|
||||
[criteria["selected_region"]] if criteria.get("selected_region") else None
|
||||
)
|
||||
languages = criteria.get("languages") or (
|
||||
[criteria["selected_language"]]
|
||||
if criteria.get("selected_language")
|
||||
else None
|
||||
)
|
||||
# Convert legacy single-value criteria to arrays for backward compatibility
|
||||
def convert_legacy_filter(new_key: str, old_key: str) -> list[str] | None:
|
||||
"""Convert legacy single-value filter to array format."""
|
||||
if new_value := criteria.get(new_key):
|
||||
return new_value if isinstance(new_value, list) else [new_value]
|
||||
if old_value := criteria.get(old_key):
|
||||
return [old_value]
|
||||
return None
|
||||
|
||||
# Apply conversions
|
||||
genres = convert_legacy_filter("genres", "selected_genre")
|
||||
franchises = convert_legacy_filter("franchises", "selected_franchise")
|
||||
collections = convert_legacy_filter("collections", "selected_collection")
|
||||
companies = convert_legacy_filter("companies", "selected_company")
|
||||
age_ratings = convert_legacy_filter("age_ratings", "selected_age_rating")
|
||||
regions = convert_legacy_filter("regions", "selected_region")
|
||||
languages = convert_legacy_filter("languages", "selected_language")
|
||||
|
||||
# Use the existing filter_roms method with the stored criteria
|
||||
return db_rom_handler.get_roms_scalar(
|
||||
|
||||
@@ -587,9 +587,15 @@ class DBRomsHandler(DBBaseHandler):
|
||||
)
|
||||
)
|
||||
|
||||
if genres or franchises or collections or companies or age_ratings:
|
||||
# Optimize JOINs - only join tables when needed
|
||||
needs_metadata_join = any(
|
||||
[genres, franchises, collections, companies, age_ratings]
|
||||
)
|
||||
|
||||
if needs_metadata_join:
|
||||
query = query.outerjoin(RomMetadata)
|
||||
|
||||
# Apply metadata filters efficiently
|
||||
if genres:
|
||||
query = self.filter_by_genres(query, session=session, values=genres)
|
||||
if franchises:
|
||||
@@ -604,6 +610,8 @@ class DBRomsHandler(DBBaseHandler):
|
||||
query = self.filter_by_age_ratings(
|
||||
query, session=session, values=age_ratings
|
||||
)
|
||||
|
||||
# Apply rom-level filters
|
||||
if regions:
|
||||
query = self.filter_by_regions(query, session=session, values=regions)
|
||||
if languages:
|
||||
|
||||
@@ -44,7 +44,7 @@ def is_mariadb(conn: sa.Connection, min_version: tuple[int, ...] | None = None)
|
||||
|
||||
|
||||
def json_array_contains_value(
|
||||
column: sa.Column, value: str | int, *, session: Session
|
||||
column: sa.Column | Any, value: str | int, *, session: Session
|
||||
) -> ColumnElement:
|
||||
"""Check if a JSON array column contains the given value."""
|
||||
conn = session.get_bind()
|
||||
@@ -66,12 +66,16 @@ def json_array_contains_value(
|
||||
|
||||
|
||||
def json_array_contains_any(
|
||||
column: sa.Column, values: Sequence[str] | Sequence[int], *, session: Session
|
||||
column: sa.Column | Any, values: Sequence[str] | Sequence[int], *, session: Session
|
||||
) -> ColumnElement:
|
||||
"""Check if a JSON array column contains any of the given values."""
|
||||
if not values:
|
||||
return sa.false()
|
||||
|
||||
# Optimize for single value case
|
||||
if len(values) == 1:
|
||||
return json_array_contains_value(column, values[0], session=session)
|
||||
|
||||
conn = session.get_bind()
|
||||
if is_postgresql(conn):
|
||||
# In PostgreSQL, string arrays can be checked for overlap using the `?|` operator.
|
||||
@@ -98,7 +102,7 @@ def json_array_contains_any(
|
||||
|
||||
|
||||
def json_array_contains_all(
|
||||
column: sa.Column, values: Sequence[Any], *, session: Session
|
||||
column: sa.Column | Any, values: Sequence[Any], *, session: Session
|
||||
) -> ColumnElement:
|
||||
"""Check if a JSON array column contains all of the given values."""
|
||||
if not values:
|
||||
|
||||
@@ -499,15 +499,25 @@ watch(
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<v-spacer />
|
||||
<v-btn @click="closeAddNote">{{ t("common.cancel") }}</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
:disabled="!newNoteTitle.trim() || newNoteTitleErrors.length > 0"
|
||||
@click="addNewNote"
|
||||
>
|
||||
{{ t("common.add") }}
|
||||
</v-btn>
|
||||
<v-row class="justify-center pa-2" no-gutters>
|
||||
<v-btn-group divided density="compact">
|
||||
<v-btn class="bg-toplayer" @click="closeAddNote">{{
|
||||
t("common.cancel")
|
||||
}}</v-btn>
|
||||
<v-btn
|
||||
class="bg-toplayer text-romm-green"
|
||||
:disabled="!newNoteTitle.trim() || newNoteTitleErrors.length > 0"
|
||||
:variant="
|
||||
!newNoteTitle.trim() || newNoteTitleErrors.length > 0
|
||||
? 'plain'
|
||||
: 'flat'
|
||||
"
|
||||
@click="addNewNote"
|
||||
>
|
||||
{{ t("common.add") }}
|
||||
</v-btn>
|
||||
</v-btn-group>
|
||||
</v-row>
|
||||
</template>
|
||||
</RDialog>
|
||||
|
||||
|
||||
@@ -9,11 +9,10 @@ import { useDisplay } from "vuetify";
|
||||
import SearchTextField from "@/components/Gallery/AppBar/Search/SearchTextField.vue";
|
||||
import FilterDuplicatesBtn from "@/components/Gallery/AppBar/common/FilterDrawer/FilterDuplicatesBtn.vue";
|
||||
import FilterFavoritesBtn from "@/components/Gallery/AppBar/common/FilterDrawer/FilterFavoritesBtn.vue";
|
||||
import FilterMatchedBtn from "@/components/Gallery/AppBar/common/FilterDrawer/FilterMatchedBtn.vue";
|
||||
import FilterMatchStateBtn from "@/components/Gallery/AppBar/common/FilterDrawer/FilterMatchStateBtn.vue";
|
||||
import FilterMissingBtn from "@/components/Gallery/AppBar/common/FilterDrawer/FilterMissingBtn.vue";
|
||||
import FilterPlayablesBtn from "@/components/Gallery/AppBar/common/FilterDrawer/FilterPlayablesBtn.vue";
|
||||
import FilterRaBtn from "@/components/Gallery/AppBar/common/FilterDrawer/FilterRaBtn.vue";
|
||||
import FilterUnmatchedBtn from "@/components/Gallery/AppBar/common/FilterDrawer/FilterUnmatchedBtn.vue";
|
||||
import FilterVerifiedBtn from "@/components/Gallery/AppBar/common/FilterDrawer/FilterVerifiedBtn.vue";
|
||||
import MissingFromFSIcon from "@/components/common/MissingFromFSIcon.vue";
|
||||
import PlatformIcon from "@/components/common/Platform/PlatformIcon.vue";
|
||||
@@ -317,7 +316,7 @@ onMounted(async () => {
|
||||
v-model="activeFilterDrawer"
|
||||
mobile
|
||||
floating
|
||||
width="400"
|
||||
width="500"
|
||||
:class="{
|
||||
'ml-2': activeFilterDrawer,
|
||||
'drawer-mobile': smAndDown && activeFilterDrawer,
|
||||
@@ -331,11 +330,7 @@ onMounted(async () => {
|
||||
</v-list-item>
|
||||
</template>
|
||||
<v-list-item>
|
||||
<FilterUnmatchedBtn :tabindex="activeFilterDrawer ? 0 : -1" />
|
||||
<FilterMatchedBtn
|
||||
class="mt-2"
|
||||
:tabindex="activeFilterDrawer ? 0 : -1"
|
||||
/>
|
||||
<FilterMatchStateBtn :tabindex="activeFilterDrawer ? 0 : -1" />
|
||||
<FilterFavoritesBtn
|
||||
class="mt-2"
|
||||
:tabindex="activeFilterDrawer ? 0 : -1"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { Emitter } from "mitt";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { computed } from "vue";
|
||||
import { inject } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import storeGalleryFilter from "@/stores/galleryFilter";
|
||||
@@ -13,29 +14,81 @@ const galleryFilterStore = storeGalleryFilter();
|
||||
const { fetchTotalRoms } = storeToRefs(romsStore);
|
||||
const { filterDuplicates } = storeToRefs(galleryFilterStore);
|
||||
const emitter = inject<Emitter<Events>>("emitter");
|
||||
function setDuplicates() {
|
||||
galleryFilterStore.switchFilterDuplicates();
|
||||
|
||||
// Computed property to determine current state
|
||||
const currentState = computed(() => {
|
||||
if (filterDuplicates.value === true) return "duplicates";
|
||||
if (filterDuplicates.value === false) return "not-duplicates";
|
||||
return "all"; // null
|
||||
});
|
||||
|
||||
// Handler for state changes
|
||||
function setState(state: string | null) {
|
||||
if (!state) return;
|
||||
|
||||
galleryFilterStore.setFilterDuplicatesState(
|
||||
state as "all" | "duplicates" | "not-duplicates",
|
||||
);
|
||||
emitter?.emit("filterRoms", null);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-btn
|
||||
block
|
||||
variant="tonal"
|
||||
:color="filterDuplicates ? 'primary' : ''"
|
||||
:disabled="fetchTotalRoms > 10000"
|
||||
@click="setDuplicates"
|
||||
<div
|
||||
class="d-flex align-center justify-space-between py-2 px-4 rounded-lg border"
|
||||
:class="{ 'opacity-50': fetchTotalRoms > 10000 }"
|
||||
>
|
||||
<v-icon :color="filterDuplicates ? 'primary' : ''">
|
||||
mdi-card-multiple
|
||||
</v-icon>
|
||||
<span
|
||||
class="ml-2"
|
||||
:class="{
|
||||
'text-primary': filterDuplicates,
|
||||
}"
|
||||
>{{ t("platform.show-duplicates") }}
|
||||
</span>
|
||||
</v-btn>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon
|
||||
:color="currentState !== 'all' ? 'primary' : 'grey-lighten-1'"
|
||||
class="mr-3"
|
||||
>
|
||||
mdi-card-multiple
|
||||
</v-icon>
|
||||
<span
|
||||
:class="
|
||||
currentState !== 'all'
|
||||
? 'text-primary font-weight-medium'
|
||||
: 'text-medium-emphasis'
|
||||
"
|
||||
class="text-body-1"
|
||||
>
|
||||
{{ t("platform.show-duplicates") }}
|
||||
</span>
|
||||
</div>
|
||||
<v-btn-toggle
|
||||
:model-value="currentState"
|
||||
color="primary"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
:disabled="fetchTotalRoms > 10000"
|
||||
@update:model-value="setState"
|
||||
>
|
||||
<v-btn value="all" size="small">All</v-btn>
|
||||
<v-tooltip text="Show duplicate ROMs only" location="bottom">
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
value="duplicates"
|
||||
size="small"
|
||||
v-bind="props"
|
||||
:disabled="fetchTotalRoms > 10000"
|
||||
>
|
||||
<v-icon size="x-large">mdi-card-multiple</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<v-tooltip text="Show non-duplicate ROMs only" location="bottom">
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
value="not-duplicates"
|
||||
size="small"
|
||||
v-bind="props"
|
||||
:disabled="fetchTotalRoms > 10000"
|
||||
>
|
||||
<v-icon size="x-large">mdi-card-outline</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</v-btn-toggle>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { Emitter } from "mitt";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { computed } from "vue";
|
||||
import { inject } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import storeGalleryFilter from "@/stores/galleryFilter";
|
||||
@@ -10,26 +11,69 @@ const { t } = useI18n();
|
||||
const galleryFilterStore = storeGalleryFilter();
|
||||
const { filterFavorites } = storeToRefs(galleryFilterStore);
|
||||
const emitter = inject<Emitter<Events>>("emitter");
|
||||
function setFavorites() {
|
||||
galleryFilterStore.switchFilterFavorites();
|
||||
|
||||
// Computed property to determine current state
|
||||
const currentState = computed(() => {
|
||||
if (filterFavorites.value === true) return "favorites";
|
||||
if (filterFavorites.value === false) return "not-favorites";
|
||||
return "all"; // null
|
||||
});
|
||||
|
||||
// Handler for state changes
|
||||
function setState(state: string | null) {
|
||||
if (!state) return;
|
||||
|
||||
galleryFilterStore.setFilterFavoritesState(
|
||||
state as "all" | "favorites" | "not-favorites",
|
||||
);
|
||||
emitter?.emit("filterRoms", null);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-btn
|
||||
block
|
||||
variant="tonal"
|
||||
:color="filterFavorites ? 'primary' : ''"
|
||||
@click="setFavorites"
|
||||
<div
|
||||
class="d-flex align-center justify-space-between py-2 px-4 rounded-lg border"
|
||||
>
|
||||
<v-icon :color="filterFavorites ? 'primary' : ''"> mdi-star </v-icon
|
||||
><span
|
||||
class="ml-2"
|
||||
:class="{
|
||||
'text-primary': filterFavorites,
|
||||
}"
|
||||
>{{ t("platform.show-favorites") }}</span
|
||||
<div class="d-flex align-center">
|
||||
<v-icon
|
||||
:color="currentState !== 'all' ? 'primary' : 'grey-lighten-1'"
|
||||
class="mr-3"
|
||||
>
|
||||
mdi-star
|
||||
</v-icon>
|
||||
<span
|
||||
:class="
|
||||
currentState !== 'all'
|
||||
? 'text-primary font-weight-medium'
|
||||
: 'text-medium-emphasis'
|
||||
"
|
||||
class="text-body-1"
|
||||
>
|
||||
{{ t("platform.show-favorites") }}
|
||||
</span>
|
||||
</div>
|
||||
<v-btn-toggle
|
||||
:model-value="currentState"
|
||||
color="primary"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
@update:model-value="setState"
|
||||
>
|
||||
</v-btn>
|
||||
<v-btn value="all" size="small">All</v-btn>
|
||||
<v-tooltip text="Show favorite ROMs only" location="bottom">
|
||||
<template #activator="{ props }">
|
||||
<v-btn value="favorites" size="small" v-bind="props">
|
||||
<v-icon size="x-large">mdi-star</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<v-tooltip text="Show non-favorite ROMs only" location="bottom">
|
||||
<template #activator="{ props }">
|
||||
<v-btn value="not-favorites" size="small" v-bind="props">
|
||||
<v-icon size="x-large">mdi-star-outline</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</v-btn-toggle>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
<script setup lang="ts">
|
||||
import type { Emitter } from "mitt";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { computed } from "vue";
|
||||
import { inject } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import storeGalleryFilter from "@/stores/galleryFilter";
|
||||
import type { Events } from "@/types/emitter";
|
||||
|
||||
const { t } = useI18n();
|
||||
const galleryFilterStore = storeGalleryFilter();
|
||||
const { filterMatched, filterUnmatched } = storeToRefs(galleryFilterStore);
|
||||
const emitter = inject<Emitter<Events>>("emitter");
|
||||
|
||||
// Computed property to determine current state
|
||||
const matchState = computed(() => {
|
||||
if (filterMatched.value) return "matched";
|
||||
if (filterUnmatched.value) return "unmatched";
|
||||
return "all";
|
||||
});
|
||||
|
||||
// Handler for state changes
|
||||
function setMatchState(state: string) {
|
||||
switch (state) {
|
||||
case "matched":
|
||||
galleryFilterStore.setFilterMatched(true);
|
||||
break;
|
||||
case "unmatched":
|
||||
galleryFilterStore.setFilterUnmatched(true);
|
||||
break;
|
||||
default: // "all"
|
||||
galleryFilterStore.setFilterMatched(false);
|
||||
galleryFilterStore.setFilterUnmatched(false);
|
||||
break;
|
||||
}
|
||||
emitter?.emit("filterRoms", null);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="d-flex align-center justify-space-between py-2 px-4 rounded-lg border"
|
||||
>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon
|
||||
:color="matchState !== 'all' ? 'primary' : 'grey-lighten-1'"
|
||||
class="mr-3"
|
||||
>
|
||||
mdi-file-search-outline
|
||||
</v-icon>
|
||||
<span
|
||||
:class="
|
||||
matchState !== 'all'
|
||||
? 'text-primary font-weight-medium'
|
||||
: 'text-medium-emphasis'
|
||||
"
|
||||
class="text-body-1"
|
||||
>
|
||||
Match Status
|
||||
</span>
|
||||
</div>
|
||||
<v-btn-toggle
|
||||
:model-value="matchState"
|
||||
color="primary"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
@update:model-value="setMatchState"
|
||||
>
|
||||
<v-btn value="all" size="small"> All </v-btn>
|
||||
<v-tooltip :text="t('platform.show-matched')" location="bottom">
|
||||
<template #activator="{ props }">
|
||||
<v-btn value="matched" size="small" v-bind="props">
|
||||
<v-icon size="x-large">mdi-file-find</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<v-tooltip :text="t('platform.show-unmatched')" location="bottom">
|
||||
<template #activator="{ props }">
|
||||
<v-btn value="unmatched" size="small" v-bind="props">
|
||||
<v-icon size="x-large">mdi-file-find-outline</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</v-btn-toggle>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,35 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { Emitter } from "mitt";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { inject } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import storeGalleryFilter from "@/stores/galleryFilter";
|
||||
import type { Events } from "@/types/emitter";
|
||||
|
||||
const { t } = useI18n();
|
||||
const galleryFilterStore = storeGalleryFilter();
|
||||
const { filterMatched } = storeToRefs(galleryFilterStore);
|
||||
const emitter = inject<Emitter<Events>>("emitter");
|
||||
function setUnmatched() {
|
||||
galleryFilterStore.switchFilterMatched();
|
||||
emitter?.emit("filterRoms", null);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-btn
|
||||
block
|
||||
variant="tonal"
|
||||
:color="filterMatched ? 'primary' : ''"
|
||||
@click="setUnmatched"
|
||||
>
|
||||
<v-icon :color="filterMatched ? 'primary' : ''"> mdi-file-find </v-icon
|
||||
><span
|
||||
class="ml-2"
|
||||
:class="{
|
||||
'text-primary': filterMatched,
|
||||
}"
|
||||
>{{ t("platform.show-matched") }}</span
|
||||
>
|
||||
</v-btn>
|
||||
</template>
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { Emitter } from "mitt";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { computed } from "vue";
|
||||
import { inject } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import storeGalleryFilter from "@/stores/galleryFilter";
|
||||
@@ -10,27 +11,69 @@ const { t } = useI18n();
|
||||
const galleryFilterStore = storeGalleryFilter();
|
||||
const { filterMissing } = storeToRefs(galleryFilterStore);
|
||||
const emitter = inject<Emitter<Events>>("emitter");
|
||||
function setMissing() {
|
||||
galleryFilterStore.switchFilterMissing();
|
||||
|
||||
// Computed property to determine current state
|
||||
const currentState = computed(() => {
|
||||
if (filterMissing.value === true) return "missing";
|
||||
if (filterMissing.value === false) return "not-missing";
|
||||
return "all"; // null
|
||||
});
|
||||
|
||||
// Handler for state changes
|
||||
function setState(state: string | null) {
|
||||
if (!state) return;
|
||||
|
||||
galleryFilterStore.setFilterMissingState(
|
||||
state as "all" | "missing" | "not-missing",
|
||||
);
|
||||
emitter?.emit("filterRoms", null);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-btn
|
||||
block
|
||||
variant="tonal"
|
||||
:color="filterMissing ? 'primary' : ''"
|
||||
@click="setMissing"
|
||||
<div
|
||||
class="d-flex align-center justify-space-between py-2 px-4 rounded-lg border"
|
||||
>
|
||||
<v-icon :color="filterMissing ? 'primary' : ''">
|
||||
mdi-folder-question </v-icon
|
||||
><span
|
||||
class="ml-2"
|
||||
:class="{
|
||||
'text-primary': filterMissing,
|
||||
}"
|
||||
>{{ t("platform.show-missing") }}</span
|
||||
<div class="d-flex align-center">
|
||||
<v-icon
|
||||
:color="currentState !== 'all' ? 'primary' : 'grey-lighten-1'"
|
||||
class="mr-3"
|
||||
>
|
||||
mdi-folder-question
|
||||
</v-icon>
|
||||
<span
|
||||
:class="
|
||||
currentState !== 'all'
|
||||
? 'text-primary font-weight-medium'
|
||||
: 'text-medium-emphasis'
|
||||
"
|
||||
class="text-body-1"
|
||||
>
|
||||
{{ t("platform.show-missing") }}
|
||||
</span>
|
||||
</div>
|
||||
<v-btn-toggle
|
||||
:model-value="currentState"
|
||||
color="primary"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
@update:model-value="setState"
|
||||
>
|
||||
</v-btn>
|
||||
<v-btn value="all" size="small">All</v-btn>
|
||||
<v-tooltip text="Show missing ROMs only" location="bottom">
|
||||
<template #activator="{ props }">
|
||||
<v-btn value="missing" size="small" v-bind="props">
|
||||
<v-icon size="x-large">mdi-folder-question</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<v-tooltip text="Show non-missing ROMs only" location="bottom">
|
||||
<template #activator="{ props }">
|
||||
<v-btn value="not-missing" size="small" v-bind="props">
|
||||
<v-icon size="x-large">mdi-folder-check</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</v-btn-toggle>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { Emitter } from "mitt";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { computed } from "vue";
|
||||
import { inject } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import storeGalleryFilter from "@/stores/galleryFilter";
|
||||
@@ -14,27 +15,80 @@ const { fetchTotalRoms } = storeToRefs(romsStore);
|
||||
const { filterPlayables } = storeToRefs(galleryFilterStore);
|
||||
const emitter = inject<Emitter<Events>>("emitter");
|
||||
|
||||
function setPlayables() {
|
||||
galleryFilterStore.switchFilterPlayables();
|
||||
// Computed property to determine current state
|
||||
const currentState = computed(() => {
|
||||
if (filterPlayables.value === true) return "playables";
|
||||
if (filterPlayables.value === false) return "not-playables";
|
||||
return "all"; // null
|
||||
});
|
||||
|
||||
// Handler for state changes
|
||||
function setState(state: string | null) {
|
||||
if (!state) return;
|
||||
|
||||
galleryFilterStore.setFilterPlayablesState(
|
||||
state as "all" | "playables" | "not-playables",
|
||||
);
|
||||
emitter?.emit("filterRoms", null);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-btn
|
||||
block
|
||||
variant="tonal"
|
||||
:color="filterPlayables ? 'primary' : ''"
|
||||
:disabled="fetchTotalRoms > 10000"
|
||||
@click="setPlayables"
|
||||
<div
|
||||
class="d-flex align-center justify-space-between py-2 px-4 rounded-lg border"
|
||||
:class="{ 'opacity-50': fetchTotalRoms > 10000 }"
|
||||
>
|
||||
<v-icon :color="filterPlayables ? 'primary' : ''"> mdi-play </v-icon>
|
||||
<span
|
||||
class="ml-2"
|
||||
:class="{
|
||||
'text-primary': filterPlayables,
|
||||
}"
|
||||
>{{ t("platform.show-playables") }}
|
||||
</span>
|
||||
</v-btn>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon
|
||||
:color="currentState !== 'all' ? 'primary' : 'grey-lighten-1'"
|
||||
class="mr-3"
|
||||
>
|
||||
mdi-play
|
||||
</v-icon>
|
||||
<span
|
||||
:class="
|
||||
currentState !== 'all'
|
||||
? 'text-primary font-weight-medium'
|
||||
: 'text-medium-emphasis'
|
||||
"
|
||||
class="text-body-1"
|
||||
>
|
||||
{{ t("platform.show-playables") }}
|
||||
</span>
|
||||
</div>
|
||||
<v-btn-toggle
|
||||
:model-value="currentState"
|
||||
color="primary"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
:disabled="fetchTotalRoms > 10000"
|
||||
@update:model-value="setState"
|
||||
>
|
||||
<v-btn value="all" size="small">All</v-btn>
|
||||
<v-tooltip text="Show playable ROMs only" location="bottom">
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
value="playables"
|
||||
size="small"
|
||||
v-bind="props"
|
||||
:disabled="fetchTotalRoms > 10000"
|
||||
>
|
||||
<v-icon size="x-large">mdi-play</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<v-tooltip text="Show non-playable ROMs only" location="bottom">
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
value="not-playables"
|
||||
size="small"
|
||||
v-bind="props"
|
||||
:disabled="fetchTotalRoms > 10000"
|
||||
>
|
||||
<v-icon size="x-large">mdi-play-outline</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</v-btn-toggle>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { Emitter } from "mitt";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { computed } from "vue";
|
||||
import { inject } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import storeGalleryFilter from "@/stores/galleryFilter";
|
||||
@@ -14,27 +15,81 @@ const { fetchTotalRoms } = storeToRefs(romsStore);
|
||||
const { filterRA } = storeToRefs(galleryFilterStore);
|
||||
const emitter = inject<Emitter<Events>>("emitter");
|
||||
|
||||
function setRA() {
|
||||
galleryFilterStore.switchFilterRA();
|
||||
// Computed property to determine current state
|
||||
const currentState = computed(() => {
|
||||
if (filterRA.value === true) return "has-ra";
|
||||
if (filterRA.value === false) return "no-ra";
|
||||
return "all"; // null
|
||||
});
|
||||
|
||||
// Handler for state changes
|
||||
function setState(state: string | null) {
|
||||
if (!state) return;
|
||||
|
||||
galleryFilterStore.setFilterRAState(state as "all" | "has-ra" | "no-ra");
|
||||
emitter?.emit("filterRoms", null);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-btn
|
||||
block
|
||||
variant="tonal"
|
||||
:color="filterRA ? 'primary' : ''"
|
||||
:disabled="fetchTotalRoms > 10000"
|
||||
@click="setRA"
|
||||
<div
|
||||
class="d-flex align-center justify-space-between py-2 px-4 rounded-lg border"
|
||||
:class="{ 'opacity-50': fetchTotalRoms > 10000 }"
|
||||
>
|
||||
<v-icon :color="filterRA ? 'primary' : ''"> mdi-trophy </v-icon>
|
||||
<span
|
||||
class="ml-2"
|
||||
:class="{
|
||||
'text-primary': filterRA,
|
||||
}"
|
||||
>{{ t("platform.show-ra") }}
|
||||
</span>
|
||||
</v-btn>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon
|
||||
:color="currentState !== 'all' ? 'primary' : 'grey-lighten-1'"
|
||||
class="mr-3"
|
||||
>
|
||||
mdi-trophy
|
||||
</v-icon>
|
||||
<span
|
||||
:class="
|
||||
currentState !== 'all'
|
||||
? 'text-primary font-weight-medium'
|
||||
: 'text-medium-emphasis'
|
||||
"
|
||||
class="text-body-1"
|
||||
>
|
||||
{{ t("platform.show-ra") }}
|
||||
</span>
|
||||
</div>
|
||||
<v-btn-toggle
|
||||
:model-value="currentState"
|
||||
color="primary"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
:disabled="fetchTotalRoms > 10000"
|
||||
@update:model-value="setState"
|
||||
>
|
||||
<v-btn value="all" size="small">All</v-btn>
|
||||
<v-tooltip text="Show ROMs with RetroAchievements only" location="bottom">
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
value="has-ra"
|
||||
size="small"
|
||||
v-bind="props"
|
||||
:disabled="fetchTotalRoms > 10000"
|
||||
>
|
||||
<v-icon size="x-large">mdi-trophy</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<v-tooltip
|
||||
text="Show ROMs without RetroAchievements only"
|
||||
location="bottom"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
value="no-ra"
|
||||
size="small"
|
||||
v-bind="props"
|
||||
:disabled="fetchTotalRoms > 10000"
|
||||
>
|
||||
<v-icon size="x-large">mdi-trophy-outline</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</v-btn-toggle>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { Emitter } from "mitt";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { inject } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import storeGalleryFilter from "@/stores/galleryFilter";
|
||||
import type { Events } from "@/types/emitter";
|
||||
|
||||
const { t } = useI18n();
|
||||
const galleryFilterStore = storeGalleryFilter();
|
||||
const { filterUnmatched } = storeToRefs(galleryFilterStore);
|
||||
const emitter = inject<Emitter<Events>>("emitter");
|
||||
function setUnmatched() {
|
||||
galleryFilterStore.switchFilterUnmatched();
|
||||
emitter?.emit("filterRoms", null);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-btn
|
||||
block
|
||||
variant="tonal"
|
||||
:color="filterUnmatched ? 'primary' : ''"
|
||||
@click="setUnmatched"
|
||||
>
|
||||
<v-icon :color="filterUnmatched ? 'primary' : ''">
|
||||
mdi-file-find-outline </v-icon
|
||||
><span
|
||||
class="ml-2"
|
||||
:class="{
|
||||
'text-primary': filterUnmatched,
|
||||
}"
|
||||
>{{ t("platform.show-unmatched") }}</span
|
||||
>
|
||||
</v-btn>
|
||||
</template>
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { Emitter } from "mitt";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { computed } from "vue";
|
||||
import { inject } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import storeGalleryFilter from "@/stores/galleryFilter";
|
||||
@@ -10,27 +11,69 @@ const { t } = useI18n();
|
||||
const galleryFilterStore = storeGalleryFilter();
|
||||
const { filterVerified } = storeToRefs(galleryFilterStore);
|
||||
const emitter = inject<Emitter<Events>>("emitter");
|
||||
function setVerified() {
|
||||
galleryFilterStore.switchFilterVerified();
|
||||
|
||||
// Computed property to determine current state
|
||||
const currentState = computed(() => {
|
||||
if (filterVerified.value === true) return "verified";
|
||||
if (filterVerified.value === false) return "not-verified";
|
||||
return "all"; // null
|
||||
});
|
||||
|
||||
// Handler for state changes
|
||||
function setState(state: string | null) {
|
||||
if (!state) return;
|
||||
|
||||
galleryFilterStore.setFilterVerifiedState(
|
||||
state as "all" | "verified" | "not-verified",
|
||||
);
|
||||
emitter?.emit("filterRoms", null);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-btn
|
||||
block
|
||||
variant="tonal"
|
||||
:color="filterVerified ? 'primary' : ''"
|
||||
@click="setVerified"
|
||||
<div
|
||||
class="d-flex align-center justify-space-between py-2 px-4 rounded-lg border"
|
||||
>
|
||||
<v-icon :color="filterVerified ? 'primary' : ''">
|
||||
mdi-check-decagram </v-icon
|
||||
><span
|
||||
class="ml-2"
|
||||
:class="{
|
||||
'text-primary': filterVerified,
|
||||
}"
|
||||
>{{ t("platform.show-verified") }}</span
|
||||
<div class="d-flex align-center">
|
||||
<v-icon
|
||||
:color="currentState !== 'all' ? 'primary' : 'grey-lighten-1'"
|
||||
class="mr-3"
|
||||
>
|
||||
mdi-check-decagram
|
||||
</v-icon>
|
||||
<span
|
||||
:class="
|
||||
currentState !== 'all'
|
||||
? 'text-primary font-weight-medium'
|
||||
: 'text-medium-emphasis'
|
||||
"
|
||||
class="text-body-1"
|
||||
>
|
||||
{{ t("platform.show-verified") }}
|
||||
</span>
|
||||
</div>
|
||||
<v-btn-toggle
|
||||
:model-value="currentState"
|
||||
color="primary"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
@update:model-value="setState"
|
||||
>
|
||||
</v-btn>
|
||||
<v-btn value="all" size="small">All</v-btn>
|
||||
<v-tooltip text="Show verified ROMs only" location="bottom">
|
||||
<template #activator="{ props }">
|
||||
<v-btn value="verified" size="small" v-bind="props">
|
||||
<v-icon size="x-large">mdi-check-decagram</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<v-tooltip text="Show non-verified ROMs only" location="bottom">
|
||||
<template #activator="{ props }">
|
||||
<v-btn value="not-verified" size="small" v-bind="props">
|
||||
<v-icon size="x-large">mdi-check-decagram-outline</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</v-btn-toggle>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -90,6 +90,10 @@ const filterSummary = computed(() => {
|
||||
return filters || ["No filters applied"];
|
||||
});
|
||||
|
||||
function toggleCollectionVisibility() {
|
||||
isPublic.value = !isPublic.value;
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
show.value = false;
|
||||
name.value = "";
|
||||
@@ -212,12 +216,16 @@ async function createSmartCollection() {
|
||||
</v-row>
|
||||
<v-row class="pa-2" no-gutters>
|
||||
<v-col>
|
||||
<v-switch
|
||||
v-model="isPublic"
|
||||
:label="t('collection.public-desc')"
|
||||
color="primary"
|
||||
hide-details
|
||||
/>
|
||||
<v-btn
|
||||
:color="isPublic ? 'romm-green' : 'accent'"
|
||||
variant="outlined"
|
||||
@click="toggleCollectionVisibility"
|
||||
>
|
||||
<v-icon class="mr-2">
|
||||
{{ isPublic ? "mdi-lock-open-variant" : "mdi-lock" }}
|
||||
</v-icon>
|
||||
{{ isPublic ? t("rom.public") : t("rom.private") }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
@@ -242,8 +250,7 @@ async function createSmartCollection() {
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<template #append>
|
||||
<v-divider />
|
||||
<template #footer>
|
||||
<v-row class="justify-center pa-2" no-gutters>
|
||||
<v-btn-group divided density="compact">
|
||||
<v-btn class="bg-toplayer" @click="closeDialog">
|
||||
|
||||
@@ -72,12 +72,12 @@ export interface GetRomsParams {
|
||||
orderDir?: string | null;
|
||||
filterUnmatched?: boolean;
|
||||
filterMatched?: boolean;
|
||||
filterFavorites?: boolean;
|
||||
filterDuplicates?: boolean;
|
||||
filterPlayables?: boolean;
|
||||
filterRA?: boolean;
|
||||
filterMissing?: boolean;
|
||||
filterVerified?: boolean;
|
||||
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;
|
||||
@@ -139,12 +139,12 @@ async function getRoms({
|
||||
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 } : {}),
|
||||
...(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 } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
22
frontend/src/services/cache/api.ts
vendored
22
frontend/src/services/cache/api.ts
vendored
@@ -50,12 +50,22 @@ class CachedApiService {
|
||||
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 } : {}),
|
||||
...(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<GetRomsResponse>(config, onBackgroundUpdate);
|
||||
|
||||
@@ -28,12 +28,12 @@ const defaultFilterState = {
|
||||
filterStatuses: Object.values(romStatusMap).map((status) => status.text),
|
||||
filterUnmatched: false,
|
||||
filterMatched: false,
|
||||
filterFavorites: false,
|
||||
filterDuplicates: false,
|
||||
filterPlayables: false,
|
||||
filterRA: false,
|
||||
filterMissing: false,
|
||||
filterVerified: false,
|
||||
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,
|
||||
@@ -121,52 +121,166 @@ export default defineStore("galleryFilter", {
|
||||
this.filterMatched = !this.filterMatched;
|
||||
this.filterUnmatched = false;
|
||||
},
|
||||
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.filterFavorites !== null ||
|
||||
this.filterDuplicates !== null ||
|
||||
this.filterPlayables !== null ||
|
||||
this.filterRA !== null ||
|
||||
this.filterMissing !== null ||
|
||||
this.filterVerified !== null ||
|
||||
this.selectedPlatform ||
|
||||
this.selectedGenre ||
|
||||
this.selectedFranchise ||
|
||||
@@ -193,12 +307,12 @@ export default defineStore("galleryFilter", {
|
||||
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.filterFavorites = null;
|
||||
this.filterDuplicates = null;
|
||||
this.filterPlayables = null;
|
||||
this.filterRA = null;
|
||||
this.filterMissing = null;
|
||||
this.filterVerified = null;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user