diff --git a/front/src/components/items/context-menus.tsx b/front/src/components/items/context-menus.tsx index 955da0e8..69ac3da6 100644 --- a/front/src/components/items/context-menus.tsx +++ b/front/src/components/items/context-menus.tsx @@ -15,14 +15,14 @@ import { watchListIcon } from "./watchlist-info"; // import { useDownloader } from "../../packages/ui/src/downloadses/ui/src/downloads"; export const EpisodesContext = ({ - type = "episode", + kind = "episode", slug, showSlug, status, force, ...props }: { - type?: "serie" | "movie" | "episode"; + kind?: "serie" | "movie" | "episode"; showSlug?: string | null; slug: string; status: WatchStatusV | null; @@ -34,17 +34,17 @@ export const EpisodesContext = ({ const { t } = useTranslation(); const mutation = useMutation({ - path: [type, slug, "watchStatus"], + path: [kind, slug, "watchStatus"], compute: (newStatus: WatchStatusV | null) => ({ method: newStatus ? "POST" : "DELETE", params: newStatus ? { status: newStatus } : undefined, }), - invalidate: [type, slug], + invalidate: [kind, slug], }); const metadataRefreshMutation = useMutation({ method: "POST", - path: [type, slug, "refresh"], + path: [kind, slug, "refresh"], invalidate: null, }); @@ -54,7 +54,10 @@ export const EpisodesContext = ({ Trigger={IconButton} icon={MoreVert} {...tooltip(t("misc.more"))} - {...(css([Platform.OS !== "web" && !force && { display: "none" }], props) as any)} + {...(css( + [Platform.OS !== "web" && !force && { display: "none" }], + props, + ) as any)} > {showSlug && ( ( }`)} + label={t( + `show.watchlistMark.${x.toLowerCase() as Lowercase}`, + )} onSelect={() => mutation.mutate(x)} selected={x === status} /> @@ -83,7 +88,7 @@ export const EpisodesContext = ({ /> )} - {type !== "serie" && ( + {kind !== "serie" && ( <> {/* )} @@ -113,20 +118,20 @@ export const EpisodesContext = ({ }; export const ItemContext = ({ - type, + kind, slug, status, force, ...props }: { - type: "movie" | "serie"; + kind: "movie" | "serie"; slug: string; status: WatchStatusV | null; force?: boolean; } & Partial>>) => { return ( { const { css } = useYoshiki("episodebox"); @@ -48,7 +54,7 @@ export const ItemGrid = ({ href, slug, name, - type, + kind, subtitle, poster, watchStatus, @@ -63,7 +69,7 @@ export const ItemGrid = ({ poster: KImage | null; watchStatus: WatchStatusV | null; watchPercent: number | null; - type: "movie" | "serie" | "collection"; + kind: "movie" | "serie" | "collection"; unseenEpisodesCount: number | null; } & Stylable<"text">) => { const [moreOpened, setMoreOpened] = useState(false); @@ -111,11 +117,16 @@ export const ItemGrid = ({ layout={{ width: percent(100) }} {...(css("poster") as { style: ImageStyle })} > - - {type === "movie" && watchPercent && } - {type !== "collection" && ( + + {kind === "movie" && watchPercent && ( + + )} + {kind !== "collection" && ( theme.dark.background, }, "more", - Platform.OS === "web" && moreOpened && { display: important("flex") }, + Platform.OS === "web" && + moreOpened && { display: important("flex") }, ])} /> )} -

+

{name}

{subtitle && ( diff --git a/front/src/components/items/item-list.tsx b/front/src/components/items/item-list.tsx index e8012011..7794fb81 100644 --- a/front/src/components/items/item-list.tsx +++ b/front/src/components/items/item-list.tsx @@ -1,26 +1,26 @@ import { useState } from "react"; import { Platform, View } from "react-native"; import { percent, px, rem, useYoshiki } from "yoshiki/native"; -import type { KyooImage, WatchStatusV } from "~/models"; +import type { KImage, WatchStatusV } from "~/models"; import { GradientImageBackground, Heading, + important, Link, P, Poster, PosterBackground, Skeleton, - imageBorderRadius, - important, ts, } from "~/primitives"; import type { Layout } from "~/query"; +import { ItemContext } from "./context-menus"; import { ItemWatchStatus } from "./item-helpers"; export const ItemList = ({ href, slug, - type, + kind, name, subtitle, thumbnail, @@ -31,11 +31,11 @@ export const ItemList = ({ }: { href: string; slug: string; - type: "movie" | "show" | "collection"; + kind: "movie" | "serie" | "collection"; name: string; subtitle: string | null; - poster: KyooImage | null; - thumbnail: KyooImage | null; + poster: KImage | null; + thumbnail: KImage | null; watchStatus: WatchStatusV | null; unseenEpisodesCount: number | null; }) => { @@ -43,98 +43,110 @@ export const ItemList = ({ const [moreOpened, setMoreOpened] = useState(false); return ( - setMoreOpened(true)} - {...css( - { - alignItems: "center", - justifyContent: "space-evenly", - flexDirection: "row", - height: ItemList.layout.size, - borderRadius: px(imageBorderRadius), - overflow: "hidden", - marginX: ItemList.layout.gap, - child: { - more: { - opacity: 0, - }, - }, - fover: { - title: { - textDecorationLine: "underline", - }, - more: { - opacity: 100, - }, + {...css({ + child: { + more: { + opacity: 0, }, }, - props, - )} + fover: { + title: { + textDecorationLine: "underline", + }, + more: { + opacity: 100, + }, + }, + })} > - - - {name} - - {type !== "collection" && ( - setMoreOpened(v)} - {...css([ - { - // I dont know why marginLeft gets overwritten by the margin: px(2) so we important - marginLeft: important(ts(2)), - bg: (theme) => theme.darkOverlay, - }, - "more", - Platform.OS === "web" && moreOpened && { opacity: important(100) }, - ])} - /> - )} - - {subtitle && ( -

- {subtitle} -

- )} -
- - - -
+ + {name} + + {kind !== "collection" && ( + setMoreOpened(v)} + {...css([ + { + // I dont know why marginLeft gets overwritten by the margin: px(2) so we important + marginLeft: important(ts(2)), + bg: (theme) => theme.darkOverlay, + }, + "more", + Platform.OS === "web" && + moreOpened && { opacity: important(100) }, + ])} + /> + )} + + {subtitle && ( +

+ {subtitle} +

+ )} + + + + + + ); }; @@ -149,7 +161,7 @@ ItemList.Loader = (props: object) => { justifyContent: "space-evenly", flexDirection: "row", height: ItemList.layout.size, - borderRadius: px(imageBorderRadius), + borderRadius: px(10), overflow: "hidden", bg: (theme) => theme.dark.background, marginX: ItemList.layout.gap, diff --git a/front/src/primitives/image-background.tsx b/front/src/primitives/image-background.tsx index 727d790e..9fda0352 100644 --- a/front/src/primitives/image-background.tsx +++ b/front/src/primitives/image-background.tsx @@ -2,6 +2,7 @@ import { ImageBackground as EImageBackground } from "expo-image"; import { LinearGradient, type LinearGradientProps } from "expo-linear-gradient"; import type { ComponentProps, ReactNode } from "react"; import type { ImageStyle } from "react-native"; +import { Platform } from "react-native"; import { useYoshiki } from "yoshiki/native"; import type { KImage } from "~/models"; import { useToken } from "~/providers/account-context"; @@ -31,11 +32,13 @@ export const ImageBackground = ({ ); }; diff --git a/front/src/primitives/image.tsx b/front/src/primitives/image.tsx index 7663dc91..6650f9dc 100644 --- a/front/src/primitives/image.tsx +++ b/front/src/primitives/image.tsx @@ -1,6 +1,6 @@ import { Image as EImage } from "expo-image"; import type { ComponentProps } from "react"; -import type { ImageStyle, ViewStyle } from "react-native"; +import { type ImageStyle, Platform, type ViewStyle } from "react-native"; import { useYoshiki } from "yoshiki/native"; import type { YoshikiStyle } from "yoshiki/src/type"; import type { KImage } from "~/models"; @@ -41,11 +41,13 @@ export const Image = ({ }> = Partial & U[keyof U]; export type Breakpoint = T | AtLeastOne>; @@ -7,9 +7,9 @@ export type Breakpoint = T | AtLeastOne>; // copied from yoshiki. const useBreakpoint = () => { const { width } = useWindowDimensions(); - const idx = Object.values(breakpoints).findIndex((x) => width <= x); + const idx = Object.values(breakpoints).findLastIndex((x) => x <= width); if (idx === -1) return 0; - return idx - 1; + return idx; }; const getBreakpointValue = (value: Breakpoint, breakpoint: number): T => { diff --git a/front/src/primitives/utils/spacing.tsx b/front/src/primitives/utils/spacing.tsx index 2cb39cdc..e30d1356 100644 --- a/front/src/primitives/utils/spacing.tsx +++ b/front/src/primitives/utils/spacing.tsx @@ -1,12 +1,11 @@ import { Platform } from "react-native"; -import { px } from "yoshiki/native"; export const important = (value: T): T => { return `${value} !important` as T; }; export const ts = (spacing: number) => { - return px(spacing * 8); + return spacing * 8; }; export const focusReset: object = diff --git a/front/src/query/fetch-infinite.tsx b/front/src/query/fetch-infinite.tsx index 0b521997..48fb804b 100644 --- a/front/src/query/fetch-infinite.tsx +++ b/front/src/query/fetch-infinite.tsx @@ -80,12 +80,10 @@ export const InfiniteFetch = ({ onEndReachedThreshold={0.5} onRefresh={layout.layout !== "horizontal" ? refetch : undefined} refreshing={isRefetching} - ListHeaderComponent={Header} ItemSeparatorComponent={divider === true ? HR : (divider as any) || undefined} ListEmptyComponent={Empty} - - contentContainerStyle={{ gap, margin: gap }} + contentContainerStyle={{ gap, marginHorizontal: gap }} {...props} /> ); diff --git a/front/src/ui/browse/header.tsx b/front/src/ui/browse/header.tsx index 2fd93f82..e2ce0bae 100644 --- a/front/src/ui/browse/header.tsx +++ b/front/src/ui/browse/header.tsx @@ -11,10 +11,22 @@ import ViewList from "@material-symbols/svg-400/rounded/view_list.svg"; import { useTranslation } from "react-i18next"; import { type PressableProps, View } from "react-native"; import { useYoshiki } from "yoshiki/native"; -import { HR, Icon, IconButton, Menu, P, PressableFeedback, tooltip, ts } from "~/primitives"; -import { type SortBy, type SortOrd, availableSorts } from "./types"; +import { + HR, + Icon, + IconButton, + Menu, + P, + PressableFeedback, + tooltip, + ts, +} from "~/primitives"; +import { availableSorts, type SortBy, type SortOrd } from "./types"; -const SortTrigger = ({ sortBy, ...props }: { sortBy: SortBy } & PressableProps) => { +const SortTrigger = ({ + sortBy, + ...props +}: { sortBy: SortBy } & PressableProps) => { const { css } = useYoshiki(); const { t } = useTranslation(); @@ -48,8 +60,17 @@ const MediaTypeTrigger = ({ {...css({ flexDirection: "row", alignItems: "center" }, props as any)} {...tooltip(t("browse.mediatype-tt"))} > - -

{t(mediaType !== "all" ? `browse.mediatypekey.${mediaType}` : "browse.mediatypelabel")}

+ +

+ {t( + mediaType !== "all" + ? `browse.mediatypekey.${mediaType}` + : "browse.mediatypelabel", + )} +

); }; @@ -75,8 +96,9 @@ export const BrowseSettings = ({ 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}` : ""); + const mediaType = /kind eq (\w+)/.exec(filter)?.[1] ?? "all"; + const setMediaType = (kind: string) => + setFilter(kind !== "all" ? `kind eq ${kind}` : ""); return ( setSort(x, sortBy === x && sortOrd === "asc" ? "desc" : "asc")} + onSelect={() => + setSort(x, sortBy === x && sortOrd === "asc" ? "desc" : "asc") + } /> ))} @@ -115,8 +139,13 @@ export const BrowseSettings = ({ {...css({ padding: ts(0.5), marginY: "auto" })} /> - - + + {Object.keys(MediaTypeIcons).map((x) => ( { const [filter, setFilter] = useQueryState("filter", ""); - const [sort, setSort] = useQueryState("sortBy", ""); - const sortBy = (sort?.split(":")[0] as SortBy) || "name"; - const sortOrd = (sort?.split(":")[1] as SortOrd) || "asc"; + const [sort, setSort] = useQueryState("sort", "name"); + const sortOrd = sort.startsWith("-") ? "desc" : "asc"; + const sortBy = (sort.startsWith("-") ? sort.substring(1) : sort) as SortBy; - const [layout, setLayout] = useState<"grid" | "list">("grid"); + const [layout, setLayout] = useQueryState<"grid" | "list">("layout", "grid"); const LayoutComponent = layout === "grid" ? ItemGrid : ItemList; return ( @@ -25,7 +24,7 @@ export const BrowsePage = () => { sortBy={sortBy} sortOrd={sortOrd} setSort={(key, ord) => { - setSort(`${key}:${ord}`); + setSort(ord === "desc" ? `-${key}` : key); }} filter={filter} setFilter={setFilter} @@ -41,7 +40,7 @@ export const BrowsePage = () => { BrowsePage.query = ( filter?: string, - sortKey?: SortBy, + sortBy?: SortBy, sortOrd?: SortOrd, ): QueryIdentifier => { return { @@ -49,7 +48,7 @@ BrowsePage.query = ( path: ["shows"], infinite: true, params: { - sort: sortKey ? `${sortKey}:${sortOrd ?? "asc"}` : "name:asc", + sort: sortBy ? `${sortOrd === "desc" ? "-" : ""}${sortBy}` : "name", filter, }, }; diff --git a/front/src/utils.ts b/front/src/utils.ts index 086a4d23..7d65e942 100644 --- a/front/src/utils.ts +++ b/front/src/utils.ts @@ -1,5 +1,6 @@ import { NavigationContext, useRoute } from "@react-navigation/native"; import { useContext } from "react"; +import type { Movie, Show } from "~/models"; export function setServerData(key: string, val: any) {} export function getServerData(key: string) { @@ -35,4 +36,3 @@ export const getDisplayDate = (data: Show | Movie) => { } return null; }; -