diff --git a/back/src/Kyoo.Abstractions/Models/News.cs b/back/src/Kyoo.Abstractions/Models/News.cs index 5c30915f..f42cf624 100644 --- a/back/src/Kyoo.Abstractions/Models/News.cs +++ b/back/src/Kyoo.Abstractions/Models/News.cs @@ -135,6 +135,12 @@ namespace Kyoo.Abstractions.Models /// public int? AbsoluteNumber { get; set; } + /// + /// A simple summary of informations about the show of this episode + /// (this is specially useful since news can't have includes). + /// + public ShowInfo? Show { get; set; } + /// /// Is the item a a movie or an episode? /// @@ -148,5 +154,23 @@ namespace Kyoo.Abstractions.Models Direct = $"/video/{Kind.ToString().ToLower()}/{Slug}/direct", Hls = $"/video/{Kind.ToString().ToLower()}/{Slug}/master.m3u8", }; + + /// + /// A simple summary of informations about the show of this episode + /// (this is specially useful since news can't have includes). + /// + public class ShowInfo : IResource + { + /// + public int Id { get; set; } + + /// + public string Slug { get; set; } + + /// + /// The title of this show. + /// + public string Name { get; set; } + } } } diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Show.cs b/back/src/Kyoo.Abstractions/Models/Resources/Show.cs index 98dd1d61..17b60ee3 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/Show.cs +++ b/back/src/Kyoo.Abstractions/Models/Resources/Show.cs @@ -19,7 +19,6 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models.Attributes; using Kyoo.Utils; using Newtonsoft.Json; diff --git a/back/src/Kyoo.Postgresql/DatabaseContext.cs b/back/src/Kyoo.Postgresql/DatabaseContext.cs index 557ca197..1c53ae89 100644 --- a/back/src/Kyoo.Postgresql/DatabaseContext.cs +++ b/back/src/Kyoo.Postgresql/DatabaseContext.cs @@ -360,6 +360,9 @@ namespace Kyoo.Postgresql .Ignore(x => x.Links); modelBuilder.Entity() .Ignore(x => x.Links); + + modelBuilder.Entity() + .OwnsOne(x => x.Show); } /// diff --git a/back/src/Kyoo.Postgresql/Migrations/20231029233109_news.Designer.cs b/back/src/Kyoo.Postgresql/Migrations/20231029233109_news.Designer.cs index b34f5094..3464cf84 100644 --- a/back/src/Kyoo.Postgresql/Migrations/20231029233109_news.Designer.cs +++ b/back/src/Kyoo.Postgresql/Migrations/20231029233109_news.Designer.cs @@ -1275,10 +1275,41 @@ namespace Kyoo.Postgresql.Migrations .HasConstraintName("fk_news_news_id"); }); + b.OwnsOne("Kyoo.Abstractions.Models.ShowInfo", "Show", b1 => + { + b1.Property("NewsId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Id") + .HasColumnType("integer") + .HasColumnName("show_info_id"); + + b1.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("show_info_name"); + + b1.Property("Slug") + .IsRequired() + .HasColumnType("text") + .HasColumnName("show_info_slug"); + + b1.HasKey("NewsId"); + + b1.ToTable("news"); + + b1.WithOwner() + .HasForeignKey("NewsId") + .HasConstraintName("fk_news_news_id"); + }); + b.Navigation("Logo"); b.Navigation("Poster"); + b.Navigation("Show"); + b.Navigation("Thumbnail"); }); diff --git a/back/src/Kyoo.Postgresql/Migrations/20231029233109_news.cs b/back/src/Kyoo.Postgresql/Migrations/20231029233109_news.cs index a08ef3a5..926f1f04 100644 --- a/back/src/Kyoo.Postgresql/Migrations/20231029233109_news.cs +++ b/back/src/Kyoo.Postgresql/Migrations/20231029233109_news.cs @@ -38,14 +38,15 @@ namespace Kyoo.Postgresql.Migrations e.id, e.slug, e.name, NULL AS tagline, '{}' AS aliases, e.path, e.overview, '{}' AS tags, '{}' AS genres, NULL AS status, e.release_date AS air_date, e.poster_source, e.poster_blurhash, e.thumbnail_source, e.thumbnail_blurhash, e.logo_source,e.logo_blurhash, NULL AS trailer, e.external_id, e.season_number, e.episode_number, e.absolute_number, - 'episode'::news_kind AS kind, e.added_date + 'episode'::news_kind AS kind, e.added_date, s.id AS show_id, s.slug AS show_slug, s.name AS show_name FROM episodes AS e + LEFT JOIN shows AS s ON e.show_id = s.id UNION ALL SELECT -m.id, m.slug, m.name, m.tagline, m.aliases, m.path, m.overview, m.tags, m.genres, m.status, m.air_date, m.poster_source, m.poster_blurhash, m.thumbnail_source, m.thumbnail_blurhash, m.logo_source, m.logo_blurhash, m.trailer, m.external_id, NULL AS season_number, NULL AS episode_number, NULL as absolute_number, - 'movie'::news_kind AS kind, m.added_date + 'movie'::news_kind AS kind, m.added_date, NULL AS show_id, NULL AS show_slug, NULL AS show_name FROM movies AS m "); } diff --git a/back/src/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs b/back/src/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs index 51a155f7..4f4bb873 100644 --- a/back/src/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs +++ b/back/src/Kyoo.Postgresql/Migrations/PostgresContextModelSnapshot.cs @@ -1,4 +1,4 @@ -// +// using System; using System.Collections.Generic; using Kyoo.Abstractions.Models; @@ -1272,10 +1272,41 @@ namespace Kyoo.Postgresql.Migrations .HasConstraintName("fk_news_news_id"); }); + b.OwnsOne("Kyoo.Abstractions.Models.ShowInfo", "Show", b1 => + { + b1.Property("NewsId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Id") + .HasColumnType("integer") + .HasColumnName("show_info_id"); + + b1.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("show_info_name"); + + b1.Property("Slug") + .IsRequired() + .HasColumnType("text") + .HasColumnName("show_info_slug"); + + b1.HasKey("NewsId"); + + b1.ToTable("news"); + + b1.WithOwner() + .HasForeignKey("NewsId") + .HasConstraintName("fk_news_news_id"); + }); + b.Navigation("Logo"); b.Navigation("Poster"); + b.Navigation("Show"); + b.Navigation("Thumbnail"); }); diff --git a/front/packages/models/src/resources/episode.ts b/front/packages/models/src/resources/episode.ts index ae9bc1b4..282db100 100644 --- a/front/packages/models/src/resources/episode.ts +++ b/front/packages/models/src/resources/episode.ts @@ -24,7 +24,7 @@ import { withImages, imageFn } from "../traits"; import { ResourceP } from "../traits/resource"; import { ShowP } from "./show"; -const BaseEpisodeP = withImages( +export const BaseEpisodeP = withImages( ResourceP.extend({ /** * The season in witch this episode is in. @@ -73,7 +73,10 @@ const BaseEpisodeP = withImages( }), }), "episodes", -); +).transform((x) => ({ + ...x, + href: `/watch/${x.slug}`, +})); export const EpisodeP = BaseEpisodeP.and( z.object({ diff --git a/front/packages/models/src/resources/index.ts b/front/packages/models/src/resources/index.ts index 5f06357f..310781fb 100644 --- a/front/packages/models/src/resources/index.ts +++ b/front/packages/models/src/resources/index.ts @@ -19,6 +19,7 @@ */ export * from "./library-item"; +export * from "./news"; export * from "./show"; export * from "./movie"; export * from "./collection"; diff --git a/front/packages/models/src/resources/news.ts b/front/packages/models/src/resources/news.ts new file mode 100644 index 00000000..521b0b00 --- /dev/null +++ b/front/packages/models/src/resources/news.ts @@ -0,0 +1,55 @@ +/* + * 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 . + */ + +import { z } from "zod"; +import { MovieP } from "./movie"; +import { BaseEpisodeP } from "./episode"; +import { ResourceP } from "../traits/resource"; + +/** + * The type of item, ether a a movie or an episode. + */ +export enum NewsKind { + Episode = "Episode", + Movie = "Movie", +} + +export const NewsP = z.union([ + /* + * Either an episode + */ + BaseEpisodeP.and( + z.object({ + kind: z.literal(NewsKind.Episode), + show: ResourceP.extend({ + name: z.string(), + }), + }), + ), + /* + * Or a Movie + */ + MovieP.and(z.object({ kind: z.literal(NewsKind.Movie) })), +]); + +/** + * A new item added to kyoo. + */ +export type News = z.infer; diff --git a/front/packages/ui/src/home/genre.tsx b/front/packages/ui/src/home/genre.tsx index 6c45cbe4..c0b90d6c 100644 --- a/front/packages/ui/src/home/genre.tsx +++ b/front/packages/ui/src/home/genre.tsx @@ -77,7 +77,6 @@ export const GenreGrid = ({ genre }: { genre: Genre }) => { query={query} layout={{ ...ItemGrid.layout, layout: "horizontal" }} empty={displayEmpty.current ? t("home.none") : undefined} - headerProps={{ title: genre, displayEmpty: displayEmpty.current }} > {(x, i) => { // only display empty list if a loading as been displayed (not durring ssr) diff --git a/front/packages/ui/src/home/index.tsx b/front/packages/ui/src/home/index.tsx index ac909b76..88f9c8eb 100644 --- a/front/packages/ui/src/home/index.tsx +++ b/front/packages/ui/src/home/index.tsx @@ -22,10 +22,11 @@ import { Genre, ItemKind, QueryPage } from "@kyoo/models"; import { Fetch } from "../fetch"; import { Header } from "./header"; import { DefaultLayout } from "../layout"; -import { ScrollView, View } from "react-native"; +import { ScrollView } from "react-native"; import { GenreGrid } from "./genre"; import { Recommanded } from "./recommanded"; import { VerticalRecommanded } from "./vertical"; +import { NewsList } from "./news"; export const HomePage: QueryPage<{}, Genre> = ({ randomItems }) => { return ( @@ -43,7 +44,7 @@ export const HomePage: QueryPage<{}, Genre> = ({ randomItems }) => { /> )} - {/* */} + {randomItems .filter((_, i) => i < 2) .map((x) => ( @@ -71,6 +72,7 @@ HomePage.getLayout = { Layout: DefaultLayout, props: { transparent: true } }; HomePage.getFetchUrls = () => [ Header.query(), + NewsList.query(), ...Object.values(Genre).map((x) => GenreGrid.query(x)), Recommanded.query(), VerticalRecommanded.query(), diff --git a/front/packages/ui/src/home/news.tsx b/front/packages/ui/src/home/news.tsx new file mode 100644 index 00000000..75ece79a --- /dev/null +++ b/front/packages/ui/src/home/news.tsx @@ -0,0 +1,84 @@ +/* + * 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 . + */ + +import { + Genre, + ItemKind, + News, + NewsKind, + NewsP, + QueryIdentifier, + getDisplayDate, +} from "@kyoo/models"; +import { H3, IconButton, ts } from "@kyoo/primitives"; +import { ReactElement, forwardRef, useRef } from "react"; +import { View } from "react-native"; +import { px, useYoshiki } from "yoshiki/native"; +import { ItemGrid } from "../browse/grid"; +import ChevronLeft from "@material-symbols/svg-400/rounded/chevron_left-fill.svg"; +import ChevronRight from "@material-symbols/svg-400/rounded/chevron_right-fill.svg"; +import { InfiniteFetch, InfiniteFetchList } from "../fetch-infinite"; +import { useTranslation } from "react-i18next"; +import { Header } from "./genre"; +import { EpisodeBox } from "../details/episode"; + +export const NewsList = () => { + const { t } = useTranslation(); + + return ( + <> +
+ + {(x, i) => + x.kind === NewsKind.Movie || (x.isLoading && i % 2) ? ( + + ) : ( + + ) + } + + + ); +}; + +NewsList.query = (): QueryIdentifier => ({ + parser: NewsP, + infinite: true, + path: ["news"], + params: { + // Limit the inital numbers of items + limit: 10, + }, +}); diff --git a/front/translations/en.json b/front/translations/en.json index e18bb374..e4bac99c 100644 --- a/front/translations/en.json +++ b/front/translations/en.json @@ -1,6 +1,7 @@ { "home": { "recommanded": "Recommanded", + "news": "News", "info": "See more", "none": "No episodes" }, diff --git a/front/translations/fr.json b/front/translations/fr.json index bd26690e..47b85d2c 100644 --- a/front/translations/fr.json +++ b/front/translations/fr.json @@ -1,6 +1,7 @@ { "home": { "recommanded": "Recommandé", + "news": "Nouveautés", "info": "Voir plus", "none": "Aucun episode" },