mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-09 03:04:20 -04:00
Rework browse settings
This commit is contained in:
parent
9498dea3fd
commit
69c4e4e6d8
9
front/i18n-d.d.ts
vendored
9
front/i18n-d.d.ts
vendored
@ -1,9 +0,0 @@
|
||||
import "i18next";
|
||||
import type en from "../public/translations/en.json";
|
||||
|
||||
declare module "i18next" {
|
||||
interface CustomTypeOptions {
|
||||
returnNull: false;
|
||||
resources: { translation: typeof en };
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
export * from "./item-grid";
|
||||
export * from "./item-list";
|
@ -1,23 +1,3 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Refresh from "@material-symbols/svg-400/rounded/autorenew.svg";
|
||||
// import Download from "@material-symbols/svg-400/rounded/download.svg";
|
||||
import Info from "@material-symbols/svg-400/rounded/info.svg";
|
||||
@ -27,12 +7,15 @@ import type { ComponentProps } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Platform } from "react-native";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import { WatchStatusV } from "~/models";
|
||||
import type { Serie } from "~/models";
|
||||
import { HR, IconButton, Menu, tooltip } from "~/primitives";
|
||||
import { useAccount } from "~/providers/account-context";
|
||||
import { useMutation } from "~/query";
|
||||
import { watchListIcon } from "./watchlist-info";
|
||||
// import { useDownloader } from "../../packages/ui/src/downloadses/ui/src/downloads";
|
||||
|
||||
type WatchStatusV = NonNullable<Serie["watchStatus"]>["status"];
|
||||
|
||||
export const EpisodesContext = ({
|
||||
type = "episode",
|
||||
slug,
|
23
front/src/components/items/index.ts
Normal file
23
front/src/components/items/index.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import type { ComponentProps } from "react";
|
||||
import type { Show } from "~/models";
|
||||
import { getDisplayDate } from "~/utils";
|
||||
import { ItemGrid } from "./item-grid";
|
||||
import { ItemList } from "./item-list";
|
||||
|
||||
export const itemMap = (
|
||||
item: Show,
|
||||
): ComponentProps<typeof ItemGrid> & ComponentProps<typeof ItemList> => ({
|
||||
kind: item.kind,
|
||||
slug: item.slug,
|
||||
name: item.name,
|
||||
subtitle: item.kind !== "collection" ? getDisplayDate(item) : null,
|
||||
href: item.href,
|
||||
poster: item.poster,
|
||||
thumbnail: item.thumbnail,
|
||||
watchStatus: item.kind !== "collection" ? (item.watchStatus?.status ?? null) : null,
|
||||
watchPercent: item.kind === "movie" ? (item.watchStatus?.percent ?? null) : null,
|
||||
// unseenEpisodesCount:
|
||||
// item.kind === "serie" ? (item.watchStatus?.unseenEpisodesCount ?? item.episodesCount!) : null,
|
||||
});
|
||||
|
||||
export { ItemGrid, ItemList };
|
@ -4,18 +4,21 @@ import BookmarkAdded from "@material-symbols/svg-400/rounded/bookmark_added-fill
|
||||
import BookmarkRemove from "@material-symbols/svg-400/rounded/bookmark_remove.svg";
|
||||
import type { ComponentProps } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { WatchStatusV } from "~/models";
|
||||
import type { Serie } from "~/models";
|
||||
import { IconButton, Menu, tooltip } from "~/primitives";
|
||||
import { useAccount } from "~/providers/account-context";
|
||||
import { useMutation } from "~/query";
|
||||
|
||||
export const watchListIcon = (status: WatchStatusV | null) => {
|
||||
type WatchStatus = NonNullable<Serie["watchStatus"]>["status"];
|
||||
const WatchStatus = ["completed", "watching", "rewatching", "dropped", "planned"] as const;
|
||||
|
||||
export const watchListIcon = (status: WatchStatus | null) => {
|
||||
switch (status) {
|
||||
case null:
|
||||
return BookmarkAdd;
|
||||
case WatchStatusV.Completed:
|
||||
case "completed":
|
||||
return BookmarkAdded;
|
||||
case WatchStatusV.Droped:
|
||||
case "dropped":
|
||||
return BookmarkRemove;
|
||||
default:
|
||||
return Bookmark;
|
||||
@ -30,7 +33,7 @@ export const WatchListInfo = ({
|
||||
}: {
|
||||
type: "movie" | "show" | "episode";
|
||||
slug: string;
|
||||
status: WatchStatusV | null;
|
||||
status: WatchStatus | null;
|
||||
color: ComponentProps<typeof IconButton>["color"];
|
||||
}) => {
|
||||
const account = useAccount();
|
||||
@ -38,7 +41,7 @@ export const WatchListInfo = ({
|
||||
|
||||
const mutation = useMutation({
|
||||
path: [type, slug, "watchStatus"],
|
||||
compute: (newStatus: WatchStatusV | null) => ({
|
||||
compute: (newStatus: WatchStatus | null) => ({
|
||||
method: newStatus ? "POST" : "DELETE",
|
||||
params: newStatus ? { status: newStatus } : undefined,
|
||||
}),
|
||||
@ -57,12 +60,12 @@ export const WatchListInfo = ({
|
||||
return (
|
||||
<IconButton
|
||||
icon={BookmarkAdd}
|
||||
onPress={() => mutation.mutate(WatchStatusV.Planned)}
|
||||
onPress={() => mutation.mutate("planned")}
|
||||
{...tooltip(t("show.watchlistAdd"))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
case WatchStatusV.Completed:
|
||||
case "completed":
|
||||
return (
|
||||
<IconButton
|
||||
icon={BookmarkAdded}
|
||||
@ -71,9 +74,10 @@ export const WatchListInfo = ({
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
case WatchStatusV.Planned:
|
||||
case WatchStatusV.Watching:
|
||||
case WatchStatusV.Droped:
|
||||
case "planned":
|
||||
case "watching":
|
||||
case "rewatching":
|
||||
case "dropped":
|
||||
return (
|
||||
<Menu
|
||||
Trigger={IconButton}
|
||||
@ -81,10 +85,10 @@ export const WatchListInfo = ({
|
||||
{...tooltip(t("show.watchlistEdit"))}
|
||||
{...props}
|
||||
>
|
||||
{Object.values(WatchStatusV).map((x) => (
|
||||
{Object.values(WatchStatus).map((x) => (
|
||||
<Menu.Item
|
||||
key={x}
|
||||
label={t(`show.watchlistMark.${x.toLowerCase() as Lowercase<WatchStatusV>}`)}
|
||||
label={t(`show.watchlistMark.${x}`)}
|
||||
onSelect={() => mutation.mutate(x)}
|
||||
selected={x === status}
|
||||
/>
|
@ -8,3 +8,4 @@ export const Show = z.union([
|
||||
Movie.and(z.object({ kind: z.literal("movie") })),
|
||||
Collection.and(z.object({ kind: z.literal("collection") })),
|
||||
]);
|
||||
export type Show = z.infer<typeof Show>;
|
||||
|
@ -1,79 +1,83 @@
|
||||
import { HR, Icon, IconButton, Menu, P, PressableFeedback, tooltip, ts } from "@kyoo/primitives";
|
||||
import ArrowDownward from "@material-symbols/svg-400/rounded/arrow_downward.svg";
|
||||
import ArrowUpward from "@material-symbols/svg-400/rounded/arrow_upward.svg";
|
||||
import Collection from "@material-symbols/svg-400/rounded/collections_bookmark.svg";
|
||||
import FilterList from "@material-symbols/svg-400/rounded/filter_list.svg";
|
||||
import GridView from "@material-symbols/svg-400/rounded/grid_view.svg";
|
||||
import Movie from "@material-symbols/svg-400/rounded/movie.svg";
|
||||
import Sort from "@material-symbols/svg-400/rounded/sort.svg";
|
||||
import TV from "@material-symbols/svg-400/rounded/tv.svg";
|
||||
import All from "@material-symbols/svg-400/rounded/view_headline.svg";
|
||||
import ViewList from "@material-symbols/svg-400/rounded/view_list.svg";
|
||||
import { forwardRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { type PressableProps, View } from "react-native";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import { Layout, type MediaType, MediaTypeAll, SearchSort, SortOrd } from "./types";
|
||||
import { HR, Icon, IconButton, Menu, P, PressableFeedback, tooltip, ts } from "~/primitives";
|
||||
import { type SortBy, type SortOrd, availableSorts } from "./types";
|
||||
|
||||
const SortTrigger = forwardRef<View, PressableProps & { sortKey: string }>(function SortTrigger(
|
||||
{ sortKey, ...props },
|
||||
ref,
|
||||
) {
|
||||
const SortTrigger = ({ sortBy, ...props }: { sortBy: SortBy } & PressableProps) => {
|
||||
const { css } = useYoshiki();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<PressableFeedback
|
||||
ref={ref}
|
||||
{...css({ flexDirection: "row", alignItems: "center" }, props as any)}
|
||||
{...tooltip(t("browse.sortby-tt"))}
|
||||
>
|
||||
<Icon icon={Sort} {...css({ paddingX: ts(0.5) })} />
|
||||
<P>{t(`browse.sortkey.${sortKey}` as any)}</P>
|
||||
<P>{t(`browse.sortkey.${sortBy}`)}</P>
|
||||
</PressableFeedback>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const MediaTypeTrigger = forwardRef<View, PressableProps & { mediaType: MediaType }>(
|
||||
function MediaTypeTrigger({ mediaType, ...props }, ref) {
|
||||
const MediaTypeIcons = {
|
||||
all: All,
|
||||
movie: Movie,
|
||||
serie: TV,
|
||||
collection: Collection,
|
||||
};
|
||||
|
||||
const MediaTypeTrigger = ({
|
||||
mediaType,
|
||||
...props
|
||||
}: PressableProps & { mediaType: keyof typeof MediaTypeIcons }) => {
|
||||
const { css } = useYoshiki();
|
||||
const { t } = useTranslation();
|
||||
const labelKey =
|
||||
mediaType !== MediaTypeAll ? `browse.mediatypekey.${mediaType.key}` : "browse.mediatypelabel";
|
||||
const icon = mediaType !== MediaTypeAll ? (mediaType?.icon ?? FilterList) : FilterList;
|
||||
|
||||
return (
|
||||
<PressableFeedback
|
||||
ref={ref}
|
||||
{...css({ flexDirection: "row", alignItems: "center" }, props as any)}
|
||||
{...tooltip(t("browse.mediatype-tt"))}
|
||||
>
|
||||
<Icon icon={icon} {...css({ paddingX: ts(0.5) })} />
|
||||
<P>{t(labelKey as any)}</P>
|
||||
<Icon icon={MediaTypeIcons[mediaType] ?? FilterList} {...css({ paddingX: ts(0.5) })} />
|
||||
<P>{t(mediaType !== "all" ? `browse.mediatypekey.${mediaType}` : "browse.mediatypelabel")}</P>
|
||||
</PressableFeedback>
|
||||
);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const BrowseSettings = ({
|
||||
availableSorts,
|
||||
sortKey,
|
||||
sortBy,
|
||||
sortOrd,
|
||||
setSort,
|
||||
availableMediaTypes,
|
||||
mediaType,
|
||||
setMediaType,
|
||||
filter,
|
||||
setFilter,
|
||||
layout,
|
||||
setLayout,
|
||||
}: {
|
||||
availableSorts: string[];
|
||||
sortKey: string;
|
||||
sortBy: SortBy;
|
||||
sortOrd: SortOrd;
|
||||
setSort: (sort: string, ord: SortOrd) => void;
|
||||
availableMediaTypes: MediaType[];
|
||||
mediaType: MediaType;
|
||||
setMediaType: (mediaType: MediaType) => void;
|
||||
layout: Layout;
|
||||
setLayout: (layout: Layout) => void;
|
||||
setSort: (sort: SortBy, ord: SortOrd) => void;
|
||||
filter: string;
|
||||
setFilter: (filter: string) => void;
|
||||
layout: "grid" | "list";
|
||||
setLayout: (layout: "grid" | "list") => void;
|
||||
}) => {
|
||||
const { css, theme } = useYoshiki();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// TODO: have a proper filter frontend
|
||||
const mediaType = /kind eq (\w+)/.exec(filter)?.groups?.[0] ?? "all";
|
||||
const setMediaType = (kind: string) => setFilter(kind !== "all " ? `kind eq ${kind}` : "");
|
||||
|
||||
return (
|
||||
<View
|
||||
{...css({
|
||||
@ -84,49 +88,41 @@ export const BrowseSettings = ({
|
||||
})}
|
||||
>
|
||||
<View {...css({ flexDirection: "row" })}>
|
||||
<Menu Trigger={SortTrigger} sortKey={sortKey}>
|
||||
<Menu Trigger={SortTrigger} sortBy={sortBy}>
|
||||
{availableSorts.map((x) => (
|
||||
<Menu.Item
|
||||
key={x}
|
||||
label={t(`browse.sortkey.${x}` as any)}
|
||||
selected={sortKey === x}
|
||||
icon={
|
||||
x !== SearchSort.Relevance
|
||||
? sortOrd === SortOrd.Asc
|
||||
? ArrowUpward
|
||||
: ArrowDownward
|
||||
: undefined
|
||||
}
|
||||
onSelect={() =>
|
||||
setSort(x, sortKey === x && sortOrd === SortOrd.Asc ? SortOrd.Desc : SortOrd.Asc)
|
||||
}
|
||||
label={t(`browse.sortkey.${x}`)}
|
||||
selected={sortBy === x}
|
||||
icon={sortOrd === "asc" ? ArrowUpward : ArrowDownward}
|
||||
onSelect={() => setSort(x, sortBy === x && sortOrd === "asc" ? "desc" : "asc")}
|
||||
/>
|
||||
))}
|
||||
</Menu>
|
||||
<HR orientation="vertical" />
|
||||
<IconButton
|
||||
icon={GridView}
|
||||
onPress={() => setLayout(Layout.Grid)}
|
||||
color={layout === Layout.Grid ? theme.accent : undefined}
|
||||
onPress={() => setLayout("grid")}
|
||||
color={layout === "grid" ? theme.accent : undefined}
|
||||
{...tooltip(t("browse.switchToGrid"))}
|
||||
{...css({ padding: ts(0.5), marginY: "auto" })}
|
||||
/>
|
||||
<IconButton
|
||||
icon={ViewList}
|
||||
onPress={() => setLayout(Layout.List)}
|
||||
color={layout === Layout.List ? theme.accent : undefined}
|
||||
onPress={() => setLayout("list")}
|
||||
color={layout === "list" ? theme.accent : undefined}
|
||||
{...tooltip(t("browse.switchToList"))}
|
||||
{...css({ padding: ts(0.5), marginY: "auto" })}
|
||||
/>
|
||||
</View>
|
||||
<View {...css({ flexGrow: 1, flexDirection: "row", alignItems: "center" })}>
|
||||
<Menu Trigger={MediaTypeTrigger} mediaType={mediaType}>
|
||||
{availableMediaTypes.map((x) => (
|
||||
<Menu Trigger={MediaTypeTrigger} mediaType={mediaType as keyof typeof MediaTypeIcons}>
|
||||
{Object.keys(MediaTypeIcons).map((x) => (
|
||||
<Menu.Item
|
||||
key={x.key}
|
||||
label={t(`browse.mediatypekey.${x.key}` as any)}
|
||||
key={x}
|
||||
label={t(`browse.mediatypekey.${x}`)}
|
||||
selected={mediaType === x}
|
||||
icon={x.icon}
|
||||
icon={MediaTypeIcons[x as keyof typeof MediaTypeIcons]}
|
||||
onSelect={() => setMediaType(x)}
|
||||
/>
|
||||
))}
|
||||
|
@ -1,90 +1,34 @@
|
||||
import { type ComponentProps, useState } from "react";
|
||||
import { ItemGrid } from "~/components";
|
||||
import { type LibraryItem, LibraryItemP, getDisplayDate } from "~/models";
|
||||
import { InfiniteFetch, type QueryIdentifier, type QueryPage } from "~/query";
|
||||
import { useState } from "react";
|
||||
import { ItemGrid, itemMap } from "~/components/items";
|
||||
import { ItemList } from "~/components/items";
|
||||
import { Show } from "~/models";
|
||||
import { InfiniteFetch, type QueryIdentifier } from "~/query";
|
||||
import { useQueryState } from "~/utils";
|
||||
import { DefaultLayout } from "../../../packages/ui/src/layout";
|
||||
import { ItemList } from "../../components/item-list";
|
||||
import { BrowseSettings } from "./header";
|
||||
import {
|
||||
Layout,
|
||||
type MediaType,
|
||||
MediaTypeAll,
|
||||
MediaTypeKey,
|
||||
MediaTypes,
|
||||
SortBy,
|
||||
SortOrd,
|
||||
} from "./types";
|
||||
import type { SortBy, SortOrd } from "./types";
|
||||
|
||||
export const itemMap = (
|
||||
item: LibraryItem,
|
||||
): ComponentProps<typeof ItemGrid> & ComponentProps<typeof ItemList> => ({
|
||||
slug: item.slug,
|
||||
name: item.name,
|
||||
subtitle: item.kind !== "collection" ? getDisplayDate(item) : null,
|
||||
href: item.href,
|
||||
poster: item.poster,
|
||||
thumbnail: item.thumbnail,
|
||||
watchStatus: item.kind !== "collection" ? (item.watchStatus?.status ?? null) : null,
|
||||
type: item.kind,
|
||||
watchPercent: item.kind !== "collection" ? (item.watchStatus?.watchedPercent ?? null) : null,
|
||||
unseenEpisodesCount:
|
||||
item.kind === "show" ? (item.watchStatus?.unseenEpisodesCount ?? item.episodesCount!) : null,
|
||||
});
|
||||
|
||||
export const createFilterString = (mediaType: MediaType): string | undefined => {
|
||||
return mediaType !== MediaTypeAll ? `kind eq ${mediaType.key}` : undefined;
|
||||
};
|
||||
|
||||
const query = (
|
||||
mediaType: MediaType,
|
||||
sortKey?: SortBy,
|
||||
sortOrd?: SortOrd,
|
||||
): QueryIdentifier<LibraryItem> => {
|
||||
return {
|
||||
parser: LibraryItemP,
|
||||
path: ["items"],
|
||||
infinite: true,
|
||||
params: {
|
||||
sortBy: sortKey ? `${sortKey}:${sortOrd ?? "asc"}` : "name:asc",
|
||||
filter: createFilterString(mediaType),
|
||||
fields: ["watchStatus", "episodesCount"],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getMediaTypeFromParam = (mediaTypeParam?: string): MediaType => {
|
||||
const mediaTypeKey = (mediaTypeParam as MediaTypeKey) || MediaTypeKey.All;
|
||||
return MediaTypes.find((t) => t.key === mediaTypeKey) ?? MediaTypeAll;
|
||||
};
|
||||
|
||||
export const BrowsePage: QueryPage = () => {
|
||||
export const BrowsePage = () => {
|
||||
const [filter, setFilter] = useQueryState("filter", "");
|
||||
const [sort, setSort] = useQueryState("sortBy", "");
|
||||
const [mediaTypeParam, setMediaTypeParam] = useQueryState("mediaType", "");
|
||||
const sortKey = (sort?.split(":")[0] as SortBy) || SortBy.Name;
|
||||
const sortOrd = (sort?.split(":")[1] as SortOrd) || SortOrd.Asc;
|
||||
const [layout, setLayout] = useState(Layout.Grid);
|
||||
const sortBy = (sort?.split(":")[0] as SortBy) || "name";
|
||||
const sortOrd = (sort?.split(":")[1] as SortOrd) || "asc";
|
||||
|
||||
const mediaType = getMediaTypeFromParam(mediaTypeParam);
|
||||
const LayoutComponent = layout === Layout.Grid ? ItemGrid : ItemList;
|
||||
const [layout, setLayout] = useState<"grid" | "list">("grid");
|
||||
const LayoutComponent = layout === "grid" ? ItemGrid : ItemList;
|
||||
|
||||
return (
|
||||
<InfiniteFetch
|
||||
query={query(mediaType, sortKey, sortOrd)}
|
||||
query={BrowsePage.query(filter, sortBy, sortOrd)}
|
||||
layout={LayoutComponent.layout}
|
||||
Header={
|
||||
<BrowseSettings
|
||||
availableSorts={Object.values(SortBy)}
|
||||
sortKey={sortKey}
|
||||
sortBy={sortBy}
|
||||
sortOrd={sortOrd}
|
||||
setSort={(key, ord) => {
|
||||
setSort(`${key}:${ord}`);
|
||||
}}
|
||||
mediaType={mediaType}
|
||||
availableMediaTypes={MediaTypes}
|
||||
setMediaType={(mediaType) => {
|
||||
setMediaTypeParam(mediaType.key);
|
||||
}}
|
||||
filter={filter}
|
||||
setFilter={setFilter}
|
||||
layout={layout}
|
||||
setLayout={setLayout}
|
||||
/>
|
||||
@ -95,9 +39,18 @@ export const BrowsePage: QueryPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
BrowsePage.getLayout = DefaultLayout;
|
||||
|
||||
BrowsePage.getFetchUrls = ({ mediaType, sortBy }) => {
|
||||
const mediaTypeObj = getMediaTypeFromParam(mediaType);
|
||||
return [query(mediaTypeObj, sortBy?.split("-")[0] as SortBy, sortBy?.split("-")[1] as SortOrd)];
|
||||
BrowsePage.query = (
|
||||
filter?: string,
|
||||
sortKey?: SortBy,
|
||||
sortOrd?: SortOrd,
|
||||
): QueryIdentifier<Show> => {
|
||||
return {
|
||||
parser: Show,
|
||||
path: ["shows"],
|
||||
infinite: true,
|
||||
params: {
|
||||
sort: sortKey ? `${sortKey}:${sortOrd ?? "asc"}` : "name:asc",
|
||||
filter,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -1,64 +1,3 @@
|
||||
import Collection from "@material-symbols/svg-400/rounded/collections_bookmark.svg";
|
||||
import Movie from "@material-symbols/svg-400/rounded/movie.svg";
|
||||
import TV from "@material-symbols/svg-400/rounded/tv.svg";
|
||||
import All from "@material-symbols/svg-400/rounded/view_headline.svg";
|
||||
import type { ComponentType } from "react";
|
||||
import type { SvgProps } from "react-native-svg";
|
||||
|
||||
export enum SortBy {
|
||||
Name = "name",
|
||||
StartAir = "startAir",
|
||||
EndAir = "endAir",
|
||||
AddedDate = "addedDate",
|
||||
Ratings = "rating",
|
||||
}
|
||||
|
||||
export enum SearchSort {
|
||||
Relevance = "relevance",
|
||||
AirDate = "airDate",
|
||||
AddedDate = "addedDate",
|
||||
Ratings = "rating",
|
||||
}
|
||||
|
||||
export enum SortOrd {
|
||||
Asc = "asc",
|
||||
Desc = "desc",
|
||||
}
|
||||
|
||||
export enum Layout {
|
||||
Grid,
|
||||
List,
|
||||
}
|
||||
|
||||
export enum MediaTypeKey {
|
||||
All = "all",
|
||||
Movie = "movie",
|
||||
Show = "show",
|
||||
Collection = "collection",
|
||||
}
|
||||
|
||||
export interface MediaType {
|
||||
key: MediaTypeKey;
|
||||
icon: ComponentType<SvgProps>;
|
||||
}
|
||||
|
||||
export const MediaTypeAll: MediaType = {
|
||||
key: MediaTypeKey.All,
|
||||
icon: All,
|
||||
};
|
||||
|
||||
export const MediaTypes: MediaType[] = [
|
||||
MediaTypeAll,
|
||||
{
|
||||
key: MediaTypeKey.Movie,
|
||||
icon: Movie,
|
||||
},
|
||||
{
|
||||
key: MediaTypeKey.Show,
|
||||
icon: TV,
|
||||
},
|
||||
{
|
||||
key: MediaTypeKey.Collection,
|
||||
icon: Collection,
|
||||
},
|
||||
];
|
||||
export const availableSorts = ["name", "startAir", "endAir", "createdAt", "rating"] as const;
|
||||
export type SortBy = (typeof availableSorts)[number];
|
||||
export type SortOrd = "asc" | "desc";
|
||||
|
Loading…
x
Reference in New Issue
Block a user