diff --git a/next-ui/src/components.d.ts b/next-ui/src/components.d.ts
index 39ff74c2..b3eb1788 100644
--- a/next-ui/src/components.d.ts
+++ b/next-ui/src/components.d.ts
@@ -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']
diff --git a/next-ui/src/components/filter/IncludeExclude.vue b/next-ui/src/components/filter/IncludeExclude.vue
new file mode 100644
index 00000000..50f77500
--- /dev/null
+++ b/next-ui/src/components/filter/IncludeExclude.vue
@@ -0,0 +1,47 @@
+
+ handleChange(newValue)"
+ />
+
+
+
+
+
+
+
diff --git a/next-ui/src/components/filter/by/Complete.vue b/next-ui/src/components/filter/by/Complete.vue
new file mode 100644
index 00000000..4769ae02
--- /dev/null
+++ b/next-ui/src/components/filter/by/Complete.vue
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
diff --git a/next-ui/src/components/filter/by/OneShot.vue b/next-ui/src/components/filter/by/OneShot.vue
new file mode 100644
index 00000000..8fcc50dc
--- /dev/null
+++ b/next-ui/src/components/filter/by/OneShot.vue
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
diff --git a/next-ui/src/components/filter/by/Unavailable.vue b/next-ui/src/components/filter/by/Unavailable.vue
new file mode 100644
index 00000000..0bbda471
--- /dev/null
+++ b/next-ui/src/components/filter/by/Unavailable.vue
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
diff --git a/next-ui/src/functions/filter.ts b/next-ui/src/functions/filter.ts
index 69a37a60..1ceaa142 100644
--- a/next-ui/src/functions/filter.ts
+++ b/next-ui/src/functions/filter.ts
@@ -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,
key: string,
diff --git a/next-ui/src/pages/libraries/[id]/series.vue b/next-ui/src/pages/libraries/[id]/series.vue
index 2111948e..7e28918b 100644
--- a/next-ui/src/pages/libraries/[id]/series.vue
+++ b/next-ui/src/pages/libraries/[id]/series.vue
@@ -59,6 +59,17 @@
+
+
+
+
+
+
+
+
-
-
-
@@ -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),
diff --git a/next-ui/src/types/filter.ts b/next-ui/src/types/filter.ts
index b93d946f..ac1cdbc8 100644
--- a/next-ui/src/types/filter.ts
+++ b/next-ui/src/types/filter.ts
@@ -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
+export type FilterIncludeExclude = v.InferOutput
+
export type FilterType = FilterTypeAnyAll | FilterTypeSimpleList