mirror of
https://github.com/gotson/komga.git
synced 2026-05-08 04:22:28 +02:00
language filter
This commit is contained in:
parent
1ca37aa589
commit
f7a975f79f
8 changed files with 212 additions and 11 deletions
|
|
@ -184,3 +184,36 @@ export const sharingLabelsQuery = defineQueryOptions(
|
|||
}
|
||||
},
|
||||
)
|
||||
|
||||
export const languagesQuery = defineQueryOptions(
|
||||
({
|
||||
search,
|
||||
library_id,
|
||||
collection_id,
|
||||
pageRequest,
|
||||
}: {
|
||||
search?: string
|
||||
library_id?: string[]
|
||||
collection_id?: string[]
|
||||
pageRequest?: PageRequest
|
||||
}) => {
|
||||
const queryParams = {
|
||||
search: search,
|
||||
library_id: library_id,
|
||||
collection_id: collection_id,
|
||||
...pageRequest,
|
||||
}
|
||||
return {
|
||||
key: ['languages', queryParams],
|
||||
query: () =>
|
||||
komgaClient
|
||||
.GET('/api/v2/languages', {
|
||||
params: {
|
||||
query: queryParams,
|
||||
},
|
||||
})
|
||||
// unwrap the openapi-fetch structure on success
|
||||
.then((res) => res.data),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
|||
1
next-ui/src/components.d.ts
vendored
1
next-ui/src/components.d.ts
vendored
|
|
@ -37,6 +37,7 @@ declare module 'vue' {
|
|||
FilterAnyAll: typeof import('./components/filter/AnyAll.vue')['default']
|
||||
FilterByAuthor: typeof import('./components/filter/by/Author.vue')['default']
|
||||
FilterByGenre: typeof import('./components/filter/by/Genre.vue')['default']
|
||||
FilterByLanguage: typeof import('./components/filter/by/Language.vue')['default']
|
||||
FilterByPublisher: typeof import('./components/filter/by/Publisher.vue')['default']
|
||||
FilterBySeriesStatus: typeof import('./components/filter/by/SeriesStatus.vue')['default']
|
||||
FilterBySharingLabel: typeof import('./components/filter/by/SharingLabel.vue')['default']
|
||||
|
|
|
|||
59
next-ui/src/components/filter/by/Language.stories.ts
Normal file
59
next-ui/src/components/filter/by/Language.stories.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import Language from './Language.vue'
|
||||
import { fn } from 'storybook/test'
|
||||
import { httpTyped } from '@/mocks/api/httpTyped'
|
||||
import { mockPage } from '@/mocks/api/pageable'
|
||||
import { PageRequest } from '@/types/PageRequest'
|
||||
|
||||
const meta = {
|
||||
component: Language,
|
||||
render: (args: object) => ({
|
||||
components: { Language },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<Language v-model="args.modelValue"/>',
|
||||
}),
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Language filter.',
|
||||
},
|
||||
},
|
||||
},
|
||||
args: {
|
||||
'onUpdate:modelValue': fn(),
|
||||
modelValue: [],
|
||||
},
|
||||
} satisfies Meta<typeof Language>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
}
|
||||
|
||||
export const NoData: Story = {
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: [
|
||||
httpTyped.get('/api/v2/languages', ({ response }) =>
|
||||
response(200).json(mockPage([], new PageRequest())),
|
||||
),
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const InitialValue: Story = {
|
||||
args: {
|
||||
modelValue: [
|
||||
{ i: 'e', v: 'en' },
|
||||
{ i: 'i', v: 'fr' },
|
||||
{ i: 'i', v: 'it' },
|
||||
],
|
||||
},
|
||||
}
|
||||
73
next-ui/src/components/filter/by/Language.vue
Normal file
73
next-ui/src/components/filter/by/Language.vue
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
<template>
|
||||
<FilterSearchList
|
||||
v-model="model"
|
||||
v-model:mode="modelMode"
|
||||
v-model:search="search"
|
||||
:items="allItems"
|
||||
:search-items="searchResults"
|
||||
show-mode-selector
|
||||
>
|
||||
</FilterSearchList>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useQuery } from '@pinia/colada'
|
||||
import { languagesQuery } from '@/colada/referential'
|
||||
import { PageRequest } from '@/types/PageRequest'
|
||||
import * as v from 'valibot'
|
||||
import { type AnyAll, filterKeys, filterMessages, SchemaString } from '@/types/filter'
|
||||
import type { ItemType } from '@/components/filter/List.vue'
|
||||
import { useIntl } from 'vue-intl'
|
||||
import { languageDisplayNames } from '@/utils/i18n/locale-helper'
|
||||
|
||||
type SchString = v.InferOutput<typeof SchemaString>
|
||||
|
||||
const intl = useIntl()
|
||||
|
||||
const model = defineModel<SchString[]>({ default: [] })
|
||||
const modelMode = defineModel<AnyAll>('mode', { default: 'anyOf' })
|
||||
|
||||
const search = ref()
|
||||
|
||||
const filterContext = inject(filterKeys.context, {})
|
||||
|
||||
const apiQuery = {
|
||||
...filterContext,
|
||||
}
|
||||
|
||||
const { data: items } = useQuery(() => ({
|
||||
...languagesQuery({
|
||||
pageRequest: PageRequest.Unpaged(),
|
||||
...apiQuery,
|
||||
}),
|
||||
}))
|
||||
const searchResults = computed(() =>
|
||||
items.value?.content
|
||||
?.map((it) => toItemType(it))
|
||||
.filter((it) => it.title.toLocaleLowerCase().includes(search.value?.toLocaleLowerCase() || '')),
|
||||
)
|
||||
|
||||
const allItems = computed(() => {
|
||||
const itemTypes = (items.value?.content ?? []).map((it) => toItemType(it))
|
||||
return [
|
||||
{
|
||||
title: intl.formatMessage(filterMessages.any!),
|
||||
value: { a: 'any' },
|
||||
valueExclude: { a: 'none' },
|
||||
},
|
||||
...itemTypes,
|
||||
]
|
||||
})
|
||||
|
||||
function toItemType(value: string): ItemType<SchString> {
|
||||
return {
|
||||
title: `${languageDisplayNames.of(value)} (${value})`,
|
||||
value: { i: 'i', v: value },
|
||||
valueExclude: { i: 'e', v: value },
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts"></script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
@ -59,17 +59,26 @@ export function schemaFilterAuthorsToConditions(
|
|||
}
|
||||
}
|
||||
|
||||
export function schemaFilterNullableStringToConditions(
|
||||
export function schemaFilterStringToConditions(
|
||||
filter: InferOutput<typeof SchemaFilterStrings>,
|
||||
key: string,
|
||||
nullable: boolean,
|
||||
) {
|
||||
const list = filter.v.map((it) => {
|
||||
if (v.is(SchemaAnyNone, it)) {
|
||||
return {
|
||||
[key]: {
|
||||
operator: it.a === 'any' ? 'isNotNull' : 'isNull',
|
||||
},
|
||||
}
|
||||
if (nullable)
|
||||
return {
|
||||
[key]: {
|
||||
operator: it.a === 'any' ? 'isNotNull' : 'isNull',
|
||||
},
|
||||
}
|
||||
else
|
||||
return {
|
||||
[key]: {
|
||||
operator: it.a === 'any' ? 'isNot' : 'is',
|
||||
value: '',
|
||||
},
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
[key]: {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ const mockGenres = doMockStrings(10000, 'Genre')
|
|||
const mockTags = doMockStrings(10000, 'Tag')
|
||||
const mockPublishers = doMockStrings(10000, 'Publisher')
|
||||
const mockSharingLabels = doMockStrings(150, 'SharingLabel')
|
||||
const mockLanguages = ['de', 'en', 'en-US', 'es', 'fr', 'fr-CA', 'ja', 'it']
|
||||
|
||||
function filterAndPage(
|
||||
search: string | null,
|
||||
|
|
@ -98,6 +99,17 @@ export const referentialHandlers = [
|
|||
),
|
||||
),
|
||||
),
|
||||
httpTyped.get('/api/v2/languages', ({ query, response }) =>
|
||||
response(200).json(
|
||||
filterAndPage(
|
||||
query.get('search'),
|
||||
mockLanguages,
|
||||
query.get('page'),
|
||||
query.get('size'),
|
||||
query.get('unpaged'),
|
||||
),
|
||||
),
|
||||
),
|
||||
httpTyped.get('/api/v2/authors', ({ query, response }) => {
|
||||
const search = query.get('search')
|
||||
const role = query.get('role')
|
||||
|
|
|
|||
|
|
@ -103,6 +103,17 @@
|
|||
v-model:mode="filterSharingLabel.m"
|
||||
/>
|
||||
</FilterExpansionPanel>
|
||||
|
||||
<FilterExpansionPanel
|
||||
title="Language"
|
||||
:count="filterLanguage.v.length"
|
||||
@clear="clearFilter(filterLanguage)"
|
||||
>
|
||||
<FilterByLanguage
|
||||
v-model="filterLanguage.v"
|
||||
v-model:mode="filterLanguage.m"
|
||||
/>
|
||||
</FilterExpansionPanel>
|
||||
</v-expansion-panels>
|
||||
|
||||
<v-divider />
|
||||
|
|
@ -213,7 +224,7 @@ import { useSelectionStore } from '@/stores/selection'
|
|||
import { useDisplay } from 'vuetify'
|
||||
import {
|
||||
schemaFilterAuthorsToConditions,
|
||||
schemaFilterNullableStringToConditions,
|
||||
schemaFilterStringToConditions,
|
||||
schemaFilterSeriesStatusToConditions,
|
||||
} from '@/functions/filter'
|
||||
import * as v from 'valibot'
|
||||
|
|
@ -280,15 +291,17 @@ const { data: filterGenre } = useRouteQuerySchema('genre', SchemaFilterStrings)
|
|||
const { data: filterTag } = useRouteQuerySchema('tag', SchemaFilterStrings)
|
||||
const { data: filterPublisher } = useRouteQuerySchema('publisher', SchemaFilterStrings)
|
||||
const { data: filterSharingLabel } = useRouteQuerySchema('sharingLabel', SchemaFilterStrings)
|
||||
const { data: filterLanguage } = useRouteQuerySchema('language', SchemaFilterStrings)
|
||||
|
||||
const conds = computed(() => ({
|
||||
allOf: [
|
||||
librariesCondition.value as components['schemas']['AnyOfSeries'],
|
||||
schemaFilterSeriesStatusToConditions(filterSeriesStatus.value),
|
||||
schemaFilterNullableStringToConditions(filterGenre.value, 'genre'),
|
||||
schemaFilterNullableStringToConditions(filterTag.value, 'tag'),
|
||||
schemaFilterNullableStringToConditions(filterPublisher.value, 'publisher'),
|
||||
schemaFilterNullableStringToConditions(filterSharingLabel.value, 'sharingLabel'),
|
||||
schemaFilterStringToConditions(filterGenre.value, 'genre', true),
|
||||
schemaFilterStringToConditions(filterTag.value, 'tag', true),
|
||||
schemaFilterStringToConditions(filterPublisher.value, 'publisher', false),
|
||||
schemaFilterStringToConditions(filterSharingLabel.value, 'sharingLabel', true),
|
||||
schemaFilterStringToConditions(filterLanguage.value, 'language', false),
|
||||
...Object.entries(filterAuthors).map(([, filter]) =>
|
||||
schemaFilterAuthorsToConditions(toValue(filter.filter), toValue(filter.role)),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ export function getLocale(): string {
|
|||
}
|
||||
|
||||
export const currentLocale = getLocale()
|
||||
export const languageDisplayNames = new Intl.DisplayNames(currentLocale, { type: 'language' })
|
||||
|
||||
/**
|
||||
* Save the locale to localStorage and reloads the window if it has changed.
|
||||
|
|
|
|||
Loading…
Reference in a new issue