add missing series filter

This commit is contained in:
Gauthier Roebroeck 2026-03-25 14:39:10 +08:00
parent d7b93d51f6
commit 380beec6a4
8 changed files with 162 additions and 10 deletions

View file

@ -37,15 +37,20 @@ declare module 'vue' {
FilterAnyAll: typeof import('./components/filter/AnyAll.vue')['default']
FilterByAgeRating: typeof import('./components/filter/by/AgeRating.vue')['default']
FilterByAuthor: typeof import('./components/filter/by/Author.vue')['default']
FilterByComplete: typeof import('./components/filter/by/Complete.vue')['default']
FilterByGenre: typeof import('./components/filter/by/Genre.vue')['default']
FilterByIncludeExclude: typeof import('./components/filter/by/IncludeExclude.vue')['default']
FilterByLanguage: typeof import('./components/filter/by/Language.vue')['default']
FilterByOneShot: typeof import('./components/filter/by/OneShot.vue')['default']
FilterByPublisher: typeof import('./components/filter/by/Publisher.vue')['default']
FilterByReadStatus: typeof import('./components/filter/by/ReadStatus.vue')['default']
FilterByReleaseYear: typeof import('./components/filter/by/ReleaseYear.vue')['default']
FilterBySeriesStatus: typeof import('./components/filter/by/SeriesStatus.vue')['default']
FilterBySharingLabel: typeof import('./components/filter/by/SharingLabel.vue')['default']
FilterByTag: typeof import('./components/filter/by/Tag.vue')['default']
FilterByUnavailable: typeof import('./components/filter/by/Unavailable.vue')['default']
FilterExpansionPanel: typeof import('./components/filter/ExpansionPanel.vue')['default']
FilterIncludeExclude: typeof import('./components/filter/IncludeExclude.vue')['default']
FilterList: typeof import('./components/filter/List.vue')['default']
FilterSearchList: typeof import('./components/filter/SearchList.vue')['default']
FilterSelectRange: typeof import('./components/filter/SelectRange.vue')['default']

View file

@ -0,0 +1,47 @@
<template>
<FilterTriState
:model-value="effectiveModel"
:label="label"
@change="(newValue) => handleChange(newValue)"
/>
</template>
<script setup lang="ts">
import { type FilterIncludeExclude } from '@/types/filter'
import type { IncludeExclude } from '@/components/filter/TriState.vue'
const model = defineModel<FilterIncludeExclude>({ required: true })
const { label } = defineProps<{
label: string
}>()
const effectiveModel = computed<IncludeExclude>(() => {
switch (model.value.i) {
case 'i':
return 'include'
case 'e':
return 'exclude'
default:
return undefined
}
})
function handleChange(newVal: IncludeExclude) {
switch (newVal) {
case 'include':
model.value = { i: 'i' }
break
case 'exclude':
model.value = { i: 'e' }
break
default:
model.value = { i: undefined }
break
}
}
</script>
<script lang="ts"></script>
<style scoped></style>

View file

@ -0,0 +1,22 @@
<template>
<FilterIncludeExclude
v-model="model"
:label="
$formatMessage({
description: 'Filter: complete',
defaultMessage: 'Complete',
id: 'ZEbDUo',
})
"
/>
</template>
<script setup lang="ts">
import { type FilterIncludeExclude } from '@/types/filter'
const model = defineModel<FilterIncludeExclude>({ required: true })
</script>
<script lang="ts"></script>
<style scoped></style>

View file

@ -0,0 +1,22 @@
<template>
<FilterIncludeExclude
v-model="model"
:label="
$formatMessage({
description: 'Filter: one-shot',
defaultMessage: 'One-shot',
id: '4uqafX',
})
"
/>
</template>
<script setup lang="ts">
import { type FilterIncludeExclude } from '@/types/filter'
const model = defineModel<FilterIncludeExclude>({ required: true })
</script>
<script lang="ts"></script>
<style scoped></style>

View file

@ -0,0 +1,22 @@
<template>
<FilterIncludeExclude
v-model="model"
:label="
$formatMessage({
description: 'Filter: unavailable',
defaultMessage: 'Unavailable',
id: '+pyj1u',
})
"
/>
</template>
<script setup lang="ts">
import { type FilterIncludeExclude } from '@/types/filter'
const model = defineModel<FilterIncludeExclude>({ required: true })
</script>
<script lang="ts"></script>
<style scoped></style>

View file

@ -1,4 +1,5 @@
import {
type FilterIncludeExclude,
SchemaAnyNone,
SchemaFilterAuthors,
SchemaFilterReadStatus,
@ -81,6 +82,15 @@ export function schemaFilterReadStatusToConditions(
}
}
export function schemaFilterIncludeExcludeToConditions(filter: FilterIncludeExclude, key: string) {
if (!filter.i) return null
return {
[key]: {
operator: filter.i === 'i' ? 'isTrue' : 'isFalse',
},
}
}
export function schemaFilterStringToConditions(
filter: InferOutput<typeof SchemaFilterStrings>,
key: string,

View file

@ -59,6 +59,17 @@
</div>
</v-list-subheader>
<FilterByReadStatus
v-model="filterReadStatus.v"
class="py-0"
/>
<v-list class="py-0">
<FilterByOneShot v-model="filterOneShot" />
<FilterByComplete v-model="filterComplete" />
<FilterByUnavailable v-model="filterUnavailable" />
</v-list>
<v-expansion-panels
v-model="filterExpansionPanels"
variant="accordion"
@ -66,13 +77,6 @@
flat
tile
>
<FilterExpansionPanel
:title="$formatMessage(commonMessages.filterPanelReadStatus)"
:count="filterReadStatus.v.length"
@clear="clearFilter(filterReadStatus)"
>
<FilterByReadStatus v-model="filterReadStatus.v" />
</FilterExpansionPanel>
<FilterExpansionPanel
:title="$formatMessage(commonMessages.filterPanelSeriesStatus)"
:count="filterSeriesStatus.v.length"
@ -188,7 +192,6 @@
<SortList
v-model="sortActive"
:items="sortOptions"
color="primary"
mandatory
/>
</v-list>
@ -265,7 +268,7 @@
import { useInfiniteQuery, useQuery } from '@pinia/colada'
import { seriesListQuery } from '@/colada/series'
import type { components } from '@/generated/openapi/komga'
import { PageRequest } from '@/types/PageRequest'
import { PageRequest, sortToString } from '@/types/PageRequest'
import { useGetLibrariesById } from '@/composables/libraries'
import { useAppStore } from '@/stores/app'
import { useItemsPerPage, usePagination } from '@/composables/pagination'
@ -280,15 +283,18 @@ import {
schemaFilterReleaseYearToConditions,
schemaFilterAgeRatingToConditions,
schemaFilterReadStatusToConditions,
schemaFilterIncludeExcludeToConditions,
} from '@/functions/filter'
import * as v from 'valibot'
import {
type FilterIncludeExclude,
type FilterType,
type FilterTypeSelectRange,
SchemaFilterAuthors,
SchemaFilterReadStatus,
SchemaFilterSeriesStatus,
SchemaFilterStrings,
SchemaIncludeExclude,
SchemaSeriesAgeRatings,
SchemaSeriesReleaseYears,
} from '@/types/filter'
@ -356,6 +362,10 @@ function clearFilterSelectRange(filter: FilterTypeSelectRange) {
filter.max = undefined
}
function clearFilterSolo(filter: FilterIncludeExclude) {
filter.i = undefined
}
function clearFilters() {
clearFilter(filterSeriesStatus.value)
clearFilter(filterReadStatus.value)
@ -364,6 +374,9 @@ function clearFilters() {
clearFilter(filterPublisher.value)
clearFilter(filterSharingLabel.value)
clearFilter(filterLanguage.value)
clearFilterSolo(filterComplete.value)
clearFilterSolo(filterUnavailable.value)
clearFilterSolo(filterOneShot.value)
clearFilterSelectRange(filterReleaseYear.value)
clearFilterSelectRange(filterAgeRating.value)
Object.entries(filterAuthors).map(([, filter]) => clearFilter(filter.filter))
@ -371,6 +384,9 @@ function clearFilters() {
const filterCount = computed(
() =>
(!!filterComplete.value.i ? 1 : 0) +
(!!filterUnavailable.value.i ? 1 : 0) +
(!!filterOneShot.value.i ? 1 : 0) +
filterReadStatus.value.v.length +
filterSeriesStatus.value.v.length +
filterGenre.value.v.length +
@ -394,6 +410,9 @@ const { data: filterSharingLabel } = useRouteQuerySchema('sharingLabel', SchemaF
const { data: filterLanguage } = useRouteQuerySchema('language', SchemaFilterStrings)
const { data: filterReleaseYear } = useRouteQuerySchema('year', SchemaSeriesReleaseYears)
const { data: filterAgeRating } = useRouteQuerySchema('age', SchemaSeriesAgeRatings)
const { data: filterComplete } = useRouteQuerySchema('complete', SchemaIncludeExclude)
const { data: filterUnavailable } = useRouteQuerySchema('unavailable', SchemaIncludeExclude)
const { data: filterOneShot } = useRouteQuerySchema('oneshot', SchemaIncludeExclude)
const { convertSortOptionDescriptor } = useIntlFormatter()
const sortActive = appStore.getSortActive(viewName.value, [
@ -404,6 +423,9 @@ const sortOptions = sortSeries.map((it) => convertSortOptionDescriptor(it))
const conds = computed(() => ({
allOf: [
librariesCondition.value as components['schemas']['AnyOfSeries'],
schemaFilterIncludeExcludeToConditions(filterComplete.value, 'complete'),
schemaFilterIncludeExcludeToConditions(filterUnavailable.value, 'deleted'),
schemaFilterIncludeExcludeToConditions(filterOneShot.value, 'oneShot'),
schemaFilterSeriesStatusToConditions(filterSeriesStatus.value),
schemaFilterReadStatusToConditions(filterReadStatus.value),
schemaFilterStringToConditions(filterGenre.value, 'genre', true),

View file

@ -41,7 +41,7 @@ export const SchemaAnyAll = v.strictObject({
m: v.optional(v.picklist(['anyOf', 'allOf']), 'anyOf'),
})
const SchemaIncludeExclude = v.strictObject({
export const SchemaIncludeExclude = v.strictObject({
/**
* Shorthand for `include`.
*
@ -153,4 +153,6 @@ export const SchemaFilterSelectRange = createSchemaFilterSelectRange(
)
export type FilterTypeSelectRange = v.InferOutput<typeof SchemaFilterSelectRange>
export type FilterIncludeExclude = v.InferOutput<typeof SchemaIncludeExclude>
export type FilterType = FilterTypeAnyAll | FilterTypeSimpleList