From e397be4b2e56a9b1b4f3d5287029ee42f95af51a Mon Sep 17 00:00:00 2001 From: herrrta <73949927+herrrta@users.noreply.github.com> Date: Wed, 5 Mar 2025 00:32:30 -0500 Subject: [PATCH] feat: Better Jellyseerr search results #586 - fetch 4 pages at once to maximize search results - add local sorting options --- .../jellyseerr/company/[companyId].tsx | 6 +- app/(auth)/(tabs)/(search)/index.tsx | 129 +++++++++++------- components/filters/FilterButton.tsx | 2 +- components/filters/FilterSheet.tsx | 4 +- components/jellyseerr/JellyseerrIndexPage.tsx | 113 ++++++++++----- components/search/SearchItemWrapper.tsx | 29 ++-- hooks/useJellyseerr.ts | 9 +- translations/de.json | 9 +- translations/en.json | 9 +- translations/es.json | 9 +- translations/fr.json | 9 +- translations/it.json | 9 +- translations/ja.json | 9 +- translations/nl.json | 9 +- translations/tr.json | 9 +- translations/zh-CN.json | 9 +- translations/zh-TW.json | 9 +- 17 files changed, 264 insertions(+), 118 deletions(-) diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/company/[companyId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/company/[companyId].tsx index c5eda557..cf8111bb 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/company/[companyId].tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/company/[companyId].tsx @@ -1,12 +1,8 @@ -import {router, useLocalSearchParams, useSegments,} from "expo-router"; +import {useLocalSearchParams} from "expo-router"; import React, {useMemo,} from "react"; -import {TouchableOpacity} from "react-native"; import {useInfiniteQuery} from "@tanstack/react-query"; import {Endpoints, useJellyseerr} from "@/hooks/useJellyseerr"; -import {Text} from "@/components/common/Text"; import {Image} from "expo-image"; -import Poster from "@/components/posters/Poster"; -import JellyseerrMediaIcon from "@/components/jellyseerr/JellyseerrMediaIcon"; import {DiscoverSliderType} from "@/utils/jellyseerr/server/constants/discover"; import ParallaxSlideShow from "@/components/jellyseerr/ParallaxSlideShow"; import {MovieResult, Results, TvResult} from "@/utils/jellyseerr/server/models/Search"; diff --git a/app/(auth)/(tabs)/(search)/index.tsx b/app/(auth)/(tabs)/(search)/index.tsx index 6d1ac344..c7e29512 100644 --- a/app/(auth)/(tabs)/(search)/index.tsx +++ b/app/(auth)/(tabs)/(search)/index.tsx @@ -1,39 +1,30 @@ -import { Input } from "@/components/common/Input"; -import { Text } from "@/components/common/Text"; -import { TouchableItemRouter } from "@/components/common/TouchableItemRouter"; +import {Text} from "@/components/common/Text"; +import {TouchableItemRouter} from "@/components/common/TouchableItemRouter"; import ContinueWatchingPoster from "@/components/ContinueWatchingPoster"; -import { Tag } from "@/components/GenreTags"; -import { ItemCardText } from "@/components/ItemCardText"; -import { JellyserrIndexPage } from "@/components/jellyseerr/JellyseerrIndexPage"; +import {Tag} from "@/components/GenreTags"; +import {ItemCardText} from "@/components/ItemCardText"; +import {JellyseerrSearchSort, JellyserrIndexPage} from "@/components/jellyseerr/JellyseerrIndexPage"; import MoviePoster from "@/components/posters/MoviePoster"; import SeriesPoster from "@/components/posters/SeriesPoster"; -import { LoadingSkeleton } from "@/components/search/LoadingSkeleton"; -import { SearchItemWrapper } from "@/components/search/SearchItemWrapper"; -import { useJellyseerr } from "@/hooks/useJellyseerr"; -import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; -import { useSettings } from "@/utils/atoms/settings"; -import { - BaseItemDto, - BaseItemKind, -} from "@jellyfin/sdk/lib/generated-client/models"; -import { getItemsApi, getSearchApi } from "@jellyfin/sdk/lib/utils/api"; -import { useQuery } from "@tanstack/react-query"; +import {LoadingSkeleton} from "@/components/search/LoadingSkeleton"; +import {SearchItemWrapper} from "@/components/search/SearchItemWrapper"; +import {useJellyseerr} from "@/hooks/useJellyseerr"; +import {apiAtom, userAtom} from "@/providers/JellyfinProvider"; +import {useSettings} from "@/utils/atoms/settings"; +import {BaseItemDto, BaseItemKind,} from "@jellyfin/sdk/lib/generated-client/models"; +import {getItemsApi, getSearchApi} from "@jellyfin/sdk/lib/utils/api"; +import {useQuery} from "@tanstack/react-query"; import axios from "axios"; -import { Href, router, useLocalSearchParams, useNavigation } from "expo-router"; -import { useAtom } from "jotai"; -import React, { - useCallback, - useEffect, - useLayoutEffect, - useMemo, - useRef, - useState, -} from "react"; -import { Platform, ScrollView, TouchableOpacity, View } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { useDebounce } from "use-debounce"; -import { useTranslation } from "react-i18next"; -import { eventBus } from "@/utils/eventBus"; +import {router, useLocalSearchParams, useNavigation} from "expo-router"; +import {useAtom} from "jotai"; +import React, {useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState,} from "react"; +import {Platform, ScrollView, TouchableOpacity, View} from "react-native"; +import {useSafeAreaInsets} from "react-native-safe-area-context"; +import {useDebounce} from "use-debounce"; +import {useTranslation} from "react-i18next"; +import {eventBus} from "@/utils/eventBus"; +import {sortOrderOptions} from "@/utils/atoms/filters"; +import {FilterButton} from "@/components/filters/FilterButton"; type SearchType = "Library" | "Discover"; @@ -64,6 +55,8 @@ export default function search() { const [settings] = useSettings(); const { jellyseerrApi } = useJellyseerr(); + const [jellyseerrOrderBy, setJellyseerrOrderBy] = useState(JellyseerrSearchSort.DEFAULT) + const [jellyseerrSortOrder, setJellyseerrSortOrder] = useState<"asc" | "desc">("desc") const searchEngine = useMemo(() => { return settings?.searchEngine || "Jellyfin"; @@ -241,26 +234,52 @@ export default function search() { }} > {jellyseerrApi && ( - - setSearchType("Library")}> - - - setSearchType("Discover")}> - - - + <> + + setSearchType("Library")}> + + + setSearchType("Discover")}> + + + {!loading && noResults && debouncedSearch.length > 0 && ( + + Object.keys(JellyseerrSearchSort).filter(v => isNaN(Number(v)))} + set={value => setJellyseerrOrderBy(value[0])} + values={[jellyseerrOrderBy]} + title={t("library.filters.sort_by")} + renderItemLabel={(item) => t(`home.settings.plugins.jellyseerr.order_by.${item}`)} + showSearch={false} + /> + ["asc", "desc"]} + set={value => setJellyseerrSortOrder(value[0])} + values={[jellyseerrSortOrder]} + title={t("library.filters.sort_order")} + renderItemLabel={(item) => t(`library.filters.${item}`)} + showSearch={false} + /> + + )} + + )} @@ -353,7 +372,11 @@ export default function search() { /> ) : ( - + )} {searchType === "Library" && ( diff --git a/components/filters/FilterButton.tsx b/components/filters/FilterButton.tsx index de8caa2e..a96e7348 100644 --- a/components/filters/FilterButton.tsx +++ b/components/filters/FilterButton.tsx @@ -13,7 +13,7 @@ interface FilterButtonProps extends ViewProps { title: string; set: (value: T[]) => void; queryFn: (params: any) => Promise; - searchFilter: (item: T, query: string) => boolean; + searchFilter?: (item: T, query: string) => boolean; renderItemLabel: (item: T) => React.ReactNode; icon?: "filter" | "sort"; } diff --git a/components/filters/FilterSheet.tsx b/components/filters/FilterSheet.tsx index cc5d4300..7e8c3cd2 100644 --- a/components/filters/FilterSheet.tsx +++ b/components/filters/FilterSheet.tsx @@ -28,7 +28,7 @@ interface Props extends ViewProps { values: T[]; set: (value: T[]) => void; title: string; - searchFilter: (item: T, query: string) => boolean; + searchFilter?: (item: T, query: string) => boolean; renderItemLabel: (item: T) => React.ReactNode; showSearch?: boolean; } @@ -88,7 +88,7 @@ export const FilterSheet = ({ if (!search) return _data; const results = []; for (let i = 0; i < (_data?.length || 0); i++) { - if (_data && searchFilter(_data[i], search)) { + if (_data && searchFilter?.(_data[i], search)) { results.push(_data[i]); } } diff --git a/components/jellyseerr/JellyseerrIndexPage.tsx b/components/jellyseerr/JellyseerrIndexPage.tsx index 55b45b80..0363ae1e 100644 --- a/components/jellyseerr/JellyseerrIndexPage.tsx +++ b/components/jellyseerr/JellyseerrIndexPage.tsx @@ -7,7 +7,7 @@ import { TvResult, } from "@/utils/jellyseerr/server/models/Search"; import { useReactNavigationQuery } from "@/utils/useReactNavigationQuery"; -import React, { useMemo } from "react"; +import React, {useMemo, useState} from "react"; import { View, ViewProps } from "react-native"; import { useAnimatedReaction, @@ -21,17 +21,32 @@ import { LoadingSkeleton } from "../search/LoadingSkeleton"; import { SearchItemWrapper } from "../search/SearchItemWrapper"; import PersonPoster from "./PersonPoster"; import { useTranslation } from "react-i18next"; -import {uniqBy} from "lodash"; +import {orderBy, uniqBy} from "lodash"; +import {useInfiniteQuery} from "@tanstack/react-query"; interface Props extends ViewProps { searchQuery: string; + sortType?: JellyseerrSearchSort; + order?: "asc" | "desc"; } -export const JellyserrIndexPage: React.FC = ({ searchQuery }) => { +export enum JellyseerrSearchSort { + DEFAULT, + VOTE_COUNT_AND_AVERAGE, + POPULARITY +} + +export const JellyserrIndexPage: React.FC = ({ + searchQuery, + sortType, + order +}) => { const { jellyseerrApi } = useJellyseerr(); const opacity = useSharedValue(1); const { t } = useTranslation(); + const [loadInitialPages, setLoadInitialPages] = useState(false) + const { data: jellyseerrDiscoverSettings, isFetching: f1, @@ -43,30 +58,33 @@ export const JellyserrIndexPage: React.FC = ({ searchQuery }) => { }); const { - data: jellyseerrResults, + data: jellyseerrResultPages, isFetching: f2, isLoading: l2, - } = useReactNavigationQuery({ + isFetchingNextPage: n2, + hasNextPage, + fetchNextPage + } = useInfiniteQuery({ queryKey: ["search", "jellyseerr", "results", searchQuery], - queryFn: async () => { - const response = await jellyseerrApi?.search({ - query: new URLSearchParams(searchQuery).toString(), - page: 1, - language: "en", - }); - return response?.results; - }, + queryFn: async ({pageParam}) => + jellyseerrApi?.search({ + query: new URLSearchParams(searchQuery || "").toString(), + page: Number(pageParam), + }), enabled: !!jellyseerrApi && searchQuery.length > 0, - }); + staleTime: 0, + initialPageParam: 1, + getNextPageParam: (lastPage, pages) => { + const firstPage = pages?.[0] + const mostRecentPage = lastPage || pages?.[pages?.length - 1] + const currentPage = mostRecentPage?.page || 1 - const animatedStyle = useAnimatedStyle(() => { - return { - opacity: opacity.value, - }; + return Math.min(currentPage + 1, firstPage?.totalPages || 1) + }, }); useAnimatedReaction( - () => f1 || f2 || l1 || l2, + () => f1 || f2 || l1 || l2 || n2, (isLoading) => { if (isLoading) { opacity.value = withTiming(1, { duration: 200 }); @@ -76,31 +94,63 @@ export const JellyserrIndexPage: React.FC = ({ searchQuery }) => { } ); + const sortingType = useMemo( + () => { + if (!sortType) return; + switch (Number(JellyseerrSearchSort[sortType])) { + case JellyseerrSearchSort.VOTE_COUNT_AND_AVERAGE: + return ["voteCount", "voteAverage"]; + case JellyseerrSearchSort.POPULARITY: + return ["voteCount", "popularity"] + default: + return undefined + } + }, + [sortType, order] + ) + + const jellyseerrResults = useMemo( + () => { + const lastPage = jellyseerrResultPages?.pages?.[jellyseerrResultPages?.pages?.length - 1] + + if ((lastPage?.page || 0) % 5 !== 0 && hasNextPage && !loadInitialPages) { + fetchNextPage() + setLoadInitialPages(lastPage?.page === 4 || (lastPage !== undefined && lastPage.totalPages == lastPage.page)) + } + + return uniqBy(jellyseerrResultPages?.pages?.flatMap?.(page => page?.results || []), "id") + }, + [jellyseerrResultPages, fetchNextPage, hasNextPage] + ); + const jellyseerrMovieResults = useMemo( () => - uniqBy( + orderBy( jellyseerrResults?.filter((r) => r.mediaType === MediaType.MOVIE) as MovieResult[], - "id" + sortingType || [m => m.title.toLowerCase() == searchQuery.toLowerCase()], + order || "desc" ), - [jellyseerrResults] + [jellyseerrResults, sortingType, order] ); const jellyseerrTvResults = useMemo( () => - uniqBy( + orderBy( jellyseerrResults?.filter((r) => r.mediaType === MediaType.TV) as TvResult[], - "id" + sortingType || [t => t.originalName.toLowerCase() == searchQuery.toLowerCase()], + order || "desc" ), - [jellyseerrResults] + [jellyseerrResults, sortingType, order] ); const jellyseerrPersonResults = useMemo( () => - uniqBy( + orderBy( jellyseerrResults?.filter((r) => r.mediaType === "person") as PersonResult[], - "id" + sortingType || [p => p.name.toLowerCase() == searchQuery.toLowerCase()], + order || "desc" ), - [jellyseerrResults] + [jellyseerrResults, sortingType, order] ); if (!searchQuery.length) @@ -112,7 +162,7 @@ export const JellyserrIndexPage: React.FC = ({ searchQuery }) => { return ( - + {!jellyseerrMovieResults?.length && !jellyseerrTvResults?.length && @@ -120,7 +170,8 @@ export const JellyserrIndexPage: React.FC = ({ searchQuery }) => { !f1 && !f2 && !l1 && - !l2 && ( + !l2 && + !loadInitialPages && ( {t("search.no_results_found_for")} @@ -131,7 +182,7 @@ export const JellyserrIndexPage: React.FC = ({ searchQuery }) => { )} - + = { ids?: string[] | null; items?: T[]; renderItem: (item: any) => React.ReactNode; header?: string; + onEndReached?: (() => void) | null | undefined; }; export const SearchItemWrapper = ({ @@ -19,6 +20,7 @@ export const SearchItemWrapper = ({ items, renderItem, header, + onEndReached }: PropsWithChildren>) => { const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); @@ -54,17 +56,22 @@ export const SearchItemWrapper = ({ return ( <> {header} - - {data && data?.length > 0 - ? data.map((item) => renderItem(item)) - : items && items?.length > 0 - ? items.map((i) => renderItem(i)) - : undefined} - + keyExtractor={(_, index) => index.toString()} + estimatedItemSize={250} + /*@ts-ignore */ + data={data || items} + onEndReachedThreshold={1} + onEndReached={onEndReached} + //@ts-ignore + renderItem={({item, index}) => item ? renderItem(item) : <>} + /> ); }; diff --git a/hooks/useJellyseerr.ts b/hooks/useJellyseerr.ts index 32e36513..7c860180 100644 --- a/hooks/useJellyseerr.ts +++ b/hooks/useJellyseerr.ts @@ -44,7 +44,7 @@ import { interface SearchParams { query: string; page: number; - language: string; + // language: string; } interface SearchResults { @@ -214,11 +214,10 @@ export class JellyseerrApi { } async search(params: SearchParams): Promise { - const response = await this.axios?.get( + return this.axios?.get( Endpoints.API_V1 + Endpoints.SEARCH, { params } - ); - return response?.data; + ).then(({ data }) => data) } async request(request: MediaRequestBody): Promise { @@ -467,7 +466,7 @@ export const useJellyseerr = () => { const getTitle = (item: TvResult | TvDetails | MovieResult | MovieDetails) => { return isJellyseerrResult(item) - ? (item.mediaType == MediaType.MOVIE ? item?.originalTitle : item?.name) + ? (item.mediaType == MediaType.MOVIE ? item?.title : item?.name) : (item.mediaInfo.mediaType == MediaType.MOVIE ? (item as MovieDetails)?.title : (item as TvDetails)?.name) }; diff --git a/translations/de.json b/translations/de.json index cca7b183..19258147 100644 --- a/translations/de.json +++ b/translations/de.json @@ -174,7 +174,12 @@ "tv_quota_days": "TV-Anfragetage", "reset_jellyseerr_config_button": "Setze Jellyseerr-Konfiguration zurück", "unlimited": "Unlimitiert", - "plus_n_more": "+{{n}} more" + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } }, "marlin_search": { "enable_marlin_search": "Aktiviere Marlin Search", @@ -329,6 +334,8 @@ "years": "Jahre", "sort_by": "Sortieren nach", "sort_order": "Sortierreihenfolge", + "asc": "Ascending", + "desc": "Descending", "tags": "Tags" } }, diff --git a/translations/en.json b/translations/en.json index 40594300..e804e5cb 100644 --- a/translations/en.json +++ b/translations/en.json @@ -174,7 +174,12 @@ "tv_quota_days": "TV quota days", "reset_jellyseerr_config_button": "Reset Jellyseerr config", "unlimited": "Unlimited", - "plus_n_more": "+{{n}} more" + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } }, "marlin_search": { "enable_marlin_search": "Enable Marlin Search ", @@ -333,6 +338,8 @@ "years": "Years", "sort_by": "Sort By", "sort_order": "Sort Order", + "asc": "Ascending", + "desc": "Descending", "tags": "Tags" } }, diff --git a/translations/es.json b/translations/es.json index 9a2962a3..463d86f2 100644 --- a/translations/es.json +++ b/translations/es.json @@ -174,7 +174,12 @@ "tv_quota_days": "Días de cuota de series", "reset_jellyseerr_config_button": "Restablecer configuración de Jellyseerr", "unlimited": "Ilimitado", - "plus_n_more": "+{{n}} more" + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } }, "marlin_search": { "enable_marlin_search": "Habilitar búsqueda de Marlin", @@ -329,6 +334,8 @@ "years": "Años", "sort_by": "Ordenar por", "sort_order": "Ordenar", + "asc": "Ascending", + "desc": "Descending", "tags": "Etiquetas" } }, diff --git a/translations/fr.json b/translations/fr.json index 719b5fae..6df653ff 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -175,7 +175,12 @@ "tv_quota_days": "Jours de quota TV", "reset_jellyseerr_config_button": "Réinitialiser la configuration Jellyseerr", "unlimited": "Illimité", - "plus_n_more": "+{{n}} more" + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } }, "marlin_search": { "enable_marlin_search": "Activer Marlin Search ", @@ -330,6 +335,8 @@ "years": "Années", "sort_by": "Trier par", "sort_order": "Ordre de tri", + "asc": "Ascending", + "desc": "Descending", "tags": "Tags" } }, diff --git a/translations/it.json b/translations/it.json index fc713f8e..87882704 100644 --- a/translations/it.json +++ b/translations/it.json @@ -174,7 +174,12 @@ "tv_quota_days": "Giorni di quota per le serie TV", "reset_jellyseerr_config_button": "Ripristina la configurazione di Jellyseerr", "unlimited": "Illimitato", - "plus_n_more": "+{{n}} more" + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } }, "marlin_search": { "enable_marlin_search": "Abilita la ricerca Marlin ", @@ -329,6 +334,8 @@ "years": "Anni", "sort_by": "Ordina per", "sort_order": "Criterio di ordinamento", + "asc": "Ascending", + "desc": "Descending", "tags": "Tag" } }, diff --git a/translations/ja.json b/translations/ja.json index 085b6c3d..dfdbc59d 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -173,7 +173,12 @@ "tv_quota_days": "テレビのクオータ日数", "reset_jellyseerr_config_button": "Jellyseerrの設定をリセット", "unlimited": "無制限", - "plus_n_more": "+{{n}} more" + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } }, "marlin_search": { "enable_marlin_search": "マーリン検索を有効にする ", @@ -328,6 +333,8 @@ "years": "年", "sort_by": "ソート", "sort_order": "ソート順", + "asc": "Ascending", + "desc": "Descending", "tags": "タグ" } }, diff --git a/translations/nl.json b/translations/nl.json index 0e44a305..1f87dfc8 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -174,7 +174,12 @@ "tv_quota_days": "Serie Quota dagen", "reset_jellyseerr_config_button": "Jellyseerr opnieuw instellen", "unlimited": "Ongelimiteerd", - "plus_n_more": "+{{n}} more" + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } }, "marlin_search": { "enable_marlin_search": "Marlin Search inschakelen ", @@ -329,6 +334,8 @@ "years": "Jaren", "sort_by": "Sorteren op", "sort_order": "Sorteer volgorde", + "asc": "Ascending", + "desc": "Descending", "tags": "Labels" } }, diff --git a/translations/tr.json b/translations/tr.json index a9c65b02..47f6fc02 100644 --- a/translations/tr.json +++ b/translations/tr.json @@ -173,7 +173,12 @@ "tv_quota_days": "TV kota günleri", "reset_jellyseerr_config_button": "Jellyseerr yapılandırmasını sıfırla", "unlimited": "Sınırsız", - "plus_n_more": "+{{n}} more" + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } }, "marlin_search": { "enable_marlin_search": "Marlin Aramasını Etkinleştir ", @@ -328,6 +333,8 @@ "years": "Yıllar", "sort_by": "Sırala", "sort_order": "Sıralama düzeni", + "asc": "Ascending", + "desc": "Descending", "tags": "Etiketler" } }, diff --git a/translations/zh-CN.json b/translations/zh-CN.json index 2fb3abf9..ad2d0468 100644 --- a/translations/zh-CN.json +++ b/translations/zh-CN.json @@ -173,7 +173,12 @@ "tv_quota_days": "剧集配额天数", "reset_jellyseerr_config_button": "重置 Jellyseerr 设置", "unlimited": "无限制", - "plus_n_more": "+{{n}} more" + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } }, "marlin_search": { "enable_marlin_search": "启用 Marlin 搜索", @@ -328,6 +333,8 @@ "years": "年份", "sort_by": "排序依据", "sort_order": "排序顺序", + "asc": "Ascending", + "desc": "Descending", "tags": "标签" } }, diff --git a/translations/zh-TW.json b/translations/zh-TW.json index 3f127a6b..d4f42fe3 100644 --- a/translations/zh-TW.json +++ b/translations/zh-TW.json @@ -173,7 +173,12 @@ "tv_quota_days": "電視配額天數", "reset_jellyseerr_config_button": "重置 Jellyseerr 配置", "unlimited": "無限制", - "plus_n_more": "+{{n}} more" + "plus_n_more": "+{{n}} more", + "order_by": { + "DEFAULT": "Default", + "VOTE_COUNT_AND_AVERAGE": "Vote count and average", + "POPULARITY": "Popularity" + } }, "marlin_search": { "enable_marlin_search": "啟用 Marlin 搜索", @@ -328,6 +333,8 @@ "years": "年份", "sort_by": "排序依據", "sort_order": "排序順序", + "asc": "Ascending", + "desc": "Descending", "tags": "標籤" } },