mirror of
https://github.com/zoriya/Kyoo.git
synced 2026-03-02 23:20:00 -05:00
Rework genres, news & recommended of home page
This commit is contained in:
parent
41e00532d8
commit
8f71099e7e
@ -1,5 +1,7 @@
|
||||
import { isLetter } from "char-info";
|
||||
import {
|
||||
anyStringOf,
|
||||
charWhere,
|
||||
digit,
|
||||
float,
|
||||
int,
|
||||
@ -43,7 +45,17 @@ function t<T>(parser: Parjser<T>): Parjser<T> {
|
||||
return parser.pipe(thenq(string(" ").pipe(many())));
|
||||
}
|
||||
|
||||
const enumP = t(letter().pipe(many1(), stringify()).expects("an enum value"));
|
||||
const enumP = t(
|
||||
charWhere(
|
||||
(x) =>
|
||||
isLetter(x) ||
|
||||
x === "-" || {
|
||||
reason: "Expected a letter or a `-`",
|
||||
},
|
||||
)
|
||||
.pipe(many1(), stringify())
|
||||
.expects("an enum value"),
|
||||
);
|
||||
|
||||
const property = t(letter().pipe(many1(), stringify())).expects("a property");
|
||||
|
||||
|
||||
@ -1,31 +1,12 @@
|
||||
/*
|
||||
* 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 { useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { View } from "react-native";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import { ItemGrid, itemMap } from "~/components/items";
|
||||
import type { Genre, Show } from "~/models";
|
||||
import { type Genre, Show } from "~/models";
|
||||
import { H3, ts } from "~/primitives";
|
||||
import { InfiniteFetch, type QueryIdentifier } from "~/query";
|
||||
import { EmptyView } from "../errors";
|
||||
|
||||
export const Header = ({ title }: { title: string }) => {
|
||||
const { css } = useYoshiki();
|
||||
@ -56,7 +37,11 @@ export const GenreGrid = ({ genre }: { genre: Genre }) => {
|
||||
query={GenreGrid.query(genre)}
|
||||
layout={{ ...ItemGrid.layout, layout: "horizontal" }}
|
||||
placeholderCount={2}
|
||||
empty={displayEmpty.current ? t("home.none") : undefined}
|
||||
Empty={
|
||||
displayEmpty.current ? (
|
||||
<EmptyView message={t("home.none")} />
|
||||
) : undefined
|
||||
}
|
||||
Render={({ item }) => <ItemGrid {...itemMap(item)} />}
|
||||
Loader={ItemGrid.Loader}
|
||||
/>
|
||||
@ -69,7 +54,6 @@ GenreGrid.query = (genre: Genre): QueryIdentifier<Show> => ({
|
||||
infinite: true,
|
||||
path: ["api", "shows"],
|
||||
params: {
|
||||
fields: ["watchStatus"],
|
||||
filter: `genres has ${genre}`,
|
||||
sort: "random",
|
||||
// Limit the initial numbers of items
|
||||
|
||||
@ -1,34 +1,13 @@
|
||||
/*
|
||||
* 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 { useTranslation } from "react-i18next";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import { EntryBox, entryDisplayNumber } from "~/components/entries";
|
||||
import { ItemGrid } from "~/components/items";
|
||||
import type { Entry } from "~/models";
|
||||
import { Entry } from "~/models";
|
||||
import { InfiniteFetch, type QueryIdentifier } from "~/query";
|
||||
import { EmptyView } from "../errors";
|
||||
import { Header } from "./genre";
|
||||
|
||||
export const NewsList = () => {
|
||||
const { t } = useTranslation();
|
||||
const { css } = useYoshiki();
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -36,41 +15,46 @@ export const NewsList = () => {
|
||||
<InfiniteFetch
|
||||
query={NewsList.query()}
|
||||
layout={{ ...ItemGrid.layout, layout: "horizontal" }}
|
||||
getItemType={(x, i) => (x?.kind === "movie" || (!x && i % 2) ? "movie" : "episode")}
|
||||
getItemSize={(kind) => (kind === "episode" ? 2 : 1)}
|
||||
empty={t("home.none")}
|
||||
// getItemType={(x, i) =>
|
||||
// x?.kind === "movie" || (!x && i % 2) ? "movie" : "episode"
|
||||
// }
|
||||
// getItemSizeMult={(_, __, kind) => (kind === "episode" ? 2 : 1)}
|
||||
Empty={<EmptyView message={t("home.none")} />}
|
||||
Render={({ item }) => {
|
||||
if (item.kind === "episode" || item.kind === "special") {
|
||||
return (
|
||||
<EntryBox
|
||||
slug={item.slug}
|
||||
serieSlug={item.serie!.slug}
|
||||
name={`${item.serie!.name} ${entryDisplayNumber(item)}`}
|
||||
description={item.name}
|
||||
thumbnail={item.thumbnail}
|
||||
href={item.href ?? "#"}
|
||||
watchedPercent={item.watchStatus?.percent || null}
|
||||
// TODO: Move this into the ItemList (using getItemSize)
|
||||
// @ts-expect-error This is a web only property
|
||||
{...css({ gridColumnEnd: "span 2" })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
// if (item.kind === "episode" || item.kind === "special") {
|
||||
return (
|
||||
<ItemGrid
|
||||
href={item.href ?? "#"}
|
||||
<EntryBox
|
||||
slug={item.slug}
|
||||
kind={"movie"}
|
||||
name={item.name!}
|
||||
subtitle={item.airDate ? new Date(item.airDate).getFullYear().toString() : null}
|
||||
poster={item.kind === "movie" ? item.poster : null}
|
||||
watchStatus={item.watchStatus?.status || null}
|
||||
watchPercent={item.watchStatus?.percent || null}
|
||||
unseenEpisodesCount={null}
|
||||
serieSlug={item.slug}
|
||||
name={`${item.name} ${entryDisplayNumber(item)}`}
|
||||
description={item.name}
|
||||
thumbnail={item.thumbnail}
|
||||
href={item.href ?? "#"}
|
||||
watchedPercent={item.watchStatus?.percent || null}
|
||||
/>
|
||||
);
|
||||
// }
|
||||
// return (
|
||||
// <ItemGrid
|
||||
// href={item.href ?? "#"}
|
||||
// slug={item.slug}
|
||||
// kind={"movie"}
|
||||
// name={item.name!}
|
||||
// subtitle={
|
||||
// item.airDate
|
||||
// ? new Date(item.airDate).getFullYear().toString()
|
||||
// : null
|
||||
// }
|
||||
// poster={item.kind === "movie" ? item.poster : null}
|
||||
// watchStatus={item.watchStatus?.status || null}
|
||||
// watchPercent={item.watchStatus?.percent || null}
|
||||
// unseenEpisodesCount={null}
|
||||
// />
|
||||
// );
|
||||
}}
|
||||
Loader={({ index }) => (index % 2 ? <EntryBox.Loader /> : <ItemGrid.Loader />)}
|
||||
Loader={({ index }) =>
|
||||
index % 2 ? <EntryBox.Loader /> : <ItemGrid.Loader />
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@ -81,8 +65,6 @@ NewsList.query = (): QueryIdentifier<Entry> => ({
|
||||
infinite: true,
|
||||
path: ["api", "news"],
|
||||
params: {
|
||||
// Limit the initial numbers of items
|
||||
limit: 10,
|
||||
fields: ["serie", "watchStatus"],
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,34 +1,15 @@
|
||||
/*
|
||||
* 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 PlayArrow from "@material-symbols/svg-400/rounded/play_arrow-fill.svg";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ScrollView, View } from "react-native";
|
||||
import { type Theme, calc, percent, px, rem, useYoshiki } from "yoshiki/native";
|
||||
import { ItemGrid, ItemWatchStatus } from "~/components/items";
|
||||
import { ItemGrid } from "~/components/items";
|
||||
import { ItemContext } from "~/components/items/context-menus";
|
||||
import type { Genre, KImage, Show, WatchStatusV } from "~/models";
|
||||
import { type Genre, type KImage, Show, type WatchStatusV } from "~/models";
|
||||
import { getDisplayDate } from "~/utils";
|
||||
import {
|
||||
Chip,
|
||||
focusReset,
|
||||
H3,
|
||||
IconFab,
|
||||
Link,
|
||||
@ -40,12 +21,7 @@ import {
|
||||
ts,
|
||||
} from "~/primitives";
|
||||
import { InfiniteFetch, type Layout, type QueryIdentifier } from "~/query";
|
||||
|
||||
const imageBorderRadius = 6;
|
||||
const focusReset = {
|
||||
boxShadow: "unset",
|
||||
outline: "none",
|
||||
};
|
||||
import { ItemWatchStatus } from "~/components/items/item-helpers";
|
||||
|
||||
export const ItemDetails = ({
|
||||
slug,
|
||||
@ -99,7 +75,7 @@ export const ItemDetails = ({
|
||||
bottom: 0,
|
||||
flexDirection: "row",
|
||||
bg: (theme) => theme.variant.background,
|
||||
borderRadius: calc(px(imageBorderRadius), "+", ts(0.25)),
|
||||
borderRadius: px(12),
|
||||
overflow: "hidden",
|
||||
borderColor: (theme) => theme.background,
|
||||
borderWidth: ts(0.25),
|
||||
@ -132,13 +108,28 @@ export const ItemDetails = ({
|
||||
p: ts(1),
|
||||
})}
|
||||
>
|
||||
<P {...css([{ m: 0, color: (theme: Theme) => theme.colors.white }, "title"])}>{name}</P>
|
||||
{subtitle && <SubP {...css({ m: 0 })}>{subtitle}</SubP>}
|
||||
<P
|
||||
{...css([
|
||||
{ m: 0, color: (theme: Theme) => theme.colors.white },
|
||||
"title",
|
||||
])}
|
||||
>
|
||||
{name}
|
||||
</P>
|
||||
{subtitle && <SubP {...(css({ m: 0 }) as any)}>{subtitle}</SubP>}
|
||||
</View>
|
||||
<ItemWatchStatus watchStatus={watchStatus} unseenEpisodesCount={unseenEpisodesCount} />
|
||||
<ItemWatchStatus
|
||||
watchStatus={watchStatus}
|
||||
unseenEpisodesCount={unseenEpisodesCount}
|
||||
/>
|
||||
</PosterBackground>
|
||||
<View
|
||||
{...css({ flexShrink: 1, flexGrow: 1, justifyContent: "flex-end", marginBottom: px(50) })}
|
||||
{...css({
|
||||
flexShrink: 1,
|
||||
flexGrow: 1,
|
||||
justifyContent: "flex-end",
|
||||
marginBottom: px(50),
|
||||
})}
|
||||
>
|
||||
<View
|
||||
{...css({
|
||||
@ -154,13 +145,14 @@ export const ItemDetails = ({
|
||||
status={watchStatus}
|
||||
isOpen={moreOpened}
|
||||
setOpen={(v) => setMoreOpened(v)}
|
||||
force
|
||||
/>
|
||||
)}
|
||||
{tagline && <P {...css({ p: ts(1) })}>{tagline}</P>}
|
||||
</View>
|
||||
<ScrollView {...css({ pX: ts(1) })}>
|
||||
<SubP {...css({ textAlign: "justify" })}>{description ?? t("show.noOverview")}</SubP>
|
||||
<SubP {...css({ textAlign: "justify" })}>
|
||||
{description ?? t("show.noOverview")}
|
||||
</SubP>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</Link>
|
||||
@ -174,10 +166,10 @@ export const ItemDetails = ({
|
||||
right: ts(0.25),
|
||||
borderWidth: ts(0.25),
|
||||
borderColor: "transparent",
|
||||
borderBottomEndRadius: px(imageBorderRadius),
|
||||
borderBottomEndRadius: px(6),
|
||||
overflow: "hidden",
|
||||
// Calculate the size of the poster
|
||||
left: calc(ItemDetails.layout.size, "*", 2 / 3),
|
||||
left: calc(px(ItemDetails.layout.size), "*", 2 / 3),
|
||||
bg: (theme) => theme.themeOverlay,
|
||||
flexDirection: "row",
|
||||
pX: 4,
|
||||
@ -186,9 +178,18 @@ export const ItemDetails = ({
|
||||
})}
|
||||
>
|
||||
{genres && (
|
||||
<ScrollView horizontal contentContainerStyle={{ alignItems: "center" }}>
|
||||
<ScrollView
|
||||
horizontal
|
||||
contentContainerStyle={{ alignItems: "center" }}
|
||||
>
|
||||
{genres.map((x, i) => (
|
||||
<Chip key={x ?? i} label={t(`genres.${x}`)} size="small" {...css({ mX: ts(0.5) })} />
|
||||
<Chip
|
||||
key={x ?? i}
|
||||
label={t(`genres.${x}`)}
|
||||
href={"#"}
|
||||
size="small"
|
||||
{...css({ mX: ts(0.5) })}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
)}
|
||||
@ -199,7 +200,9 @@ export const ItemDetails = ({
|
||||
as={Link}
|
||||
href={playHref}
|
||||
{...tooltip(t("show.play"))}
|
||||
{...css({ fover: { self: { transform: "scale(1.2)" as any, mX: ts(0.5) } } })}
|
||||
{...css({
|
||||
fover: { self: { transform: "scale(1.2)" as any, mX: ts(0.5) } },
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
@ -217,7 +220,7 @@ ItemDetails.Loader = (props: object) => {
|
||||
height: ItemDetails.layout.size,
|
||||
flexDirection: "row",
|
||||
bg: (theme) => theme.variant.background,
|
||||
borderRadius: calc(px(imageBorderRadius), "+", ts(0.25)),
|
||||
borderRadius: px(12),
|
||||
overflow: "hidden",
|
||||
borderColor: (theme) => theme.background,
|
||||
borderWidth: ts(0.25),
|
||||
@ -281,31 +284,42 @@ export const Recommended = () => {
|
||||
const { css } = useYoshiki();
|
||||
|
||||
return (
|
||||
<View {...css({ marginX: ItemGrid.layout.gap, marginTop: ItemGrid.layout.gap })}>
|
||||
<View
|
||||
{...css({ marginX: ItemGrid.layout.gap, marginTop: ItemGrid.layout.gap })}
|
||||
>
|
||||
<H3 {...css({ pX: ts(0.5) })}>{t("home.recommended")}</H3>
|
||||
<InfiniteFetch
|
||||
query={Recommended.query()}
|
||||
layout={ItemDetails.layout}
|
||||
placeholderCount={6}
|
||||
fetchMore={false}
|
||||
nested
|
||||
contentContainerStyle={{ padding: 0, paddingHorizontal: 0 }}
|
||||
Render={({ item }) => (
|
||||
<ItemDetails
|
||||
slug={item.slug}
|
||||
kind={item.kind}
|
||||
name={item.name}
|
||||
tagline={item.kind !== "collection" && "tagline" in item ? item.tagline : null}
|
||||
tagline={
|
||||
item.kind !== "collection" && "tagline" in item
|
||||
? item.tagline
|
||||
: null
|
||||
}
|
||||
description={item.description}
|
||||
poster={item.poster}
|
||||
subtitle={item.kind !== "collection" ? getDisplayDate(item) : null}
|
||||
genres={item.kind !== "collection" && "genres" in item ? item.genres : null}
|
||||
genres={
|
||||
item.kind !== "collection" && "genres" in item
|
||||
? item.genres
|
||||
: null
|
||||
}
|
||||
href={item.href}
|
||||
playHref={item.kind !== "collection" ? item.playHref : null}
|
||||
watchStatus={(item.kind !== "collection" && item.watchStatus?.status) || null}
|
||||
watchStatus={
|
||||
(item.kind !== "collection" && item.watchStatus?.status) || null
|
||||
}
|
||||
unseenEpisodesCount={
|
||||
item.kind === "serie"
|
||||
? (item.availableCount - (item.watchStatus?.seenCount ?? 0))
|
||||
? item.availableCount - (item.watchStatus?.seenCount ?? 0)
|
||||
: null
|
||||
}
|
||||
/>
|
||||
@ -323,6 +337,6 @@ Recommended.query = (): QueryIdentifier<Show> => ({
|
||||
params: {
|
||||
sort: "random",
|
||||
limit: 6,
|
||||
fields: ["firstEntry", "watchStatus"],
|
||||
with: ["firstEntry"],
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,28 +1,8 @@
|
||||
/*
|
||||
* 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 { useTranslation } from "react-i18next";
|
||||
import { View } from "react-native";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import { ItemGrid, ItemList, itemMap } from "~/components/items";
|
||||
import type { Show } from "~/models";
|
||||
import { Show } from "~/models";
|
||||
import { H3 } from "~/primitives";
|
||||
import { InfiniteFetch, type QueryIdentifier } from "~/query";
|
||||
|
||||
@ -38,7 +18,6 @@ export const VerticalRecommended = () => {
|
||||
placeholderCount={3}
|
||||
layout={{ ...ItemList.layout, layout: "vertical" }}
|
||||
fetchMore={false}
|
||||
nested
|
||||
Render={({ item }) => <ItemList {...itemMap(item)} />}
|
||||
Loader={() => <ItemList.Loader />}
|
||||
/>
|
||||
@ -51,7 +30,6 @@ VerticalRecommended.query = (): QueryIdentifier<Show> => ({
|
||||
infinite: true,
|
||||
path: ["api", "shows"],
|
||||
params: {
|
||||
fields: ["watchStatus"],
|
||||
sort: "random",
|
||||
limit: 3,
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user