diff --git a/ui/v2.5/src/components/Galleries/GalleryRecommendationRow.tsx b/ui/v2.5/src/components/Galleries/GalleryRecommendationRow.tsx new file mode 100644 index 000000000..827e66603 --- /dev/null +++ b/ui/v2.5/src/components/Galleries/GalleryRecommendationRow.tsx @@ -0,0 +1,37 @@ +import React, { FunctionComponent } from "react"; +import { FindGalleriesQueryResult } from "src/core/generated-graphql"; +import Slider from "react-slick"; +import { GalleryCard } from "./GalleryCard"; +import { ListFilterModel } from "src/models/list-filter/filter"; +import { getSlickSliderSettings } from "src/core/recommendations"; + +interface IProps { + isTouch: boolean; + filter: ListFilterModel; + result: FindGalleriesQueryResult; + header: String; + linkText: String; +} + +export const GalleryRecommendationRow: FunctionComponent = ( + props: IProps +) => { + const cardCount = props.result.data?.findGalleries.count; + return ( +
+
+
+

{props.header}

+
+ + {props.linkText} + +
+ + {props.result.data?.findGalleries.galleries.map((gallery) => ( + + ))} + +
+ ); +}; diff --git a/ui/v2.5/src/components/Movies/MovieRecommendationRow.tsx b/ui/v2.5/src/components/Movies/MovieRecommendationRow.tsx new file mode 100644 index 000000000..90b66e3d4 --- /dev/null +++ b/ui/v2.5/src/components/Movies/MovieRecommendationRow.tsx @@ -0,0 +1,37 @@ +import React, { FunctionComponent } from "react"; +import { FindMoviesQueryResult } from "src/core/generated-graphql"; +import Slider from "react-slick"; +import { MovieCard } from "./MovieCard"; +import { ListFilterModel } from "src/models/list-filter/filter"; +import { getSlickSliderSettings } from "src/core/recommendations"; + +interface IProps { + isTouch: boolean; + filter: ListFilterModel; + result: FindMoviesQueryResult; + header: String; + linkText: String; +} + +export const MovieRecommendationRow: FunctionComponent = ( + props: IProps +) => { + const cardCount = props.result.data?.findMovies.count; + return ( +
+
+
+

{props.header}

+
+ + {props.linkText} + +
+ + {props.result.data?.findMovies.movies.map((p) => ( + + ))} + +
+ ); +}; diff --git a/ui/v2.5/src/components/Performers/PerformerRecommendationRow.tsx b/ui/v2.5/src/components/Performers/PerformerRecommendationRow.tsx new file mode 100644 index 000000000..21fa9db0f --- /dev/null +++ b/ui/v2.5/src/components/Performers/PerformerRecommendationRow.tsx @@ -0,0 +1,37 @@ +import React, { FunctionComponent } from "react"; +import { FindPerformersQueryResult } from "src/core/generated-graphql"; +import Slider from "react-slick"; +import { PerformerCard } from "./PerformerCard"; +import { ListFilterModel } from "src/models/list-filter/filter"; +import { getSlickSliderSettings } from "src/core/recommendations"; + +interface IProps { + isTouch: boolean; + filter: ListFilterModel; + result: FindPerformersQueryResult; + header: String; + linkText: String; +} + +export const PerformerRecommendationRow: FunctionComponent = ( + props: IProps +) => { + const cardCount = props.result.data?.findPerformers.count; + return ( +
+
+
+

{props.header}

+
+ + {props.linkText} + +
+ + {props.result.data?.findPerformers.performers.map((p) => ( + + ))} + +
+ ); +}; diff --git a/ui/v2.5/src/components/Recommendations/Recommendations.tsx b/ui/v2.5/src/components/Recommendations/Recommendations.tsx index 4a218206a..e9e0cb448 100644 --- a/ui/v2.5/src/components/Recommendations/Recommendations.tsx +++ b/ui/v2.5/src/components/Recommendations/Recommendations.tsx @@ -8,14 +8,14 @@ import { useFindGalleries, useFindPerformers, } from "src/core/StashService"; -import { SceneCard } from "src/components/Scenes/SceneCard"; -import { StudioCard } from "src/components/Studios/StudioCard"; -import { MovieCard } from "src/components/Movies/MovieCard"; -import { PerformerCard } from "src/components/Performers/PerformerCard"; -import { GalleryCard } from "src/components/Galleries/GalleryCard"; +import { SceneRecommendationRow } from "src/components/Scenes/SceneRecommendationRow"; +import { StudioRecommendationRow } from "src/components/Studios/StudioRecommendationRow"; +import { MovieRecommendationRow } from "src/components/Movies/MovieRecommendationRow"; +import { PerformerRecommendationRow } from "src/components/Performers/PerformerRecommendationRow"; +import { GalleryRecommendationRow } from "src/components/Galleries/GalleryRecommendationRow"; import { SceneQueue } from "src/models/sceneQueue"; import { ListFilterModel } from "src/models/list-filter/filter"; -import Slider from "react-slick"; +import { LoadingIndicator } from "src/components/Shared"; const Recommendations: React.FC = () => { function isTouchEnabled() { @@ -31,50 +31,35 @@ const Recommendations: React.FC = () => { scenefilter.sortDirection = GQL.SortDirectionEnum.Desc; scenefilter.itemsPerPage = itemsPerPage; const sceneResult = useFindScenes(scenefilter); - const hasScenes = - sceneResult.data && - sceneResult.data.findScenes && - sceneResult.data.findScenes.count > 0; + const hasScenes = !!sceneResult?.data?.findScenes?.count; const studiofilter = new ListFilterModel(GQL.FilterMode.Studios); - studiofilter.sortBy = "scenes_count"; + studiofilter.sortBy = "created_at"; studiofilter.sortDirection = GQL.SortDirectionEnum.Desc; studiofilter.itemsPerPage = itemsPerPage; const studioResult = useFindStudios(studiofilter); - const hasStudios = - studioResult.data && - studioResult.data.findStudios && - studioResult.data.findStudios.count > 0; + const hasStudios = !!studioResult?.data?.findStudios?.count; const moviefilter = new ListFilterModel(GQL.FilterMode.Movies); moviefilter.sortBy = "date"; moviefilter.sortDirection = GQL.SortDirectionEnum.Desc; moviefilter.itemsPerPage = itemsPerPage; const movieResult = useFindMovies(moviefilter); - const hasMovies = - movieResult.data && - movieResult.data.findMovies && - movieResult.data.findMovies.count > 0; + const hasMovies = !!movieResult?.data?.findMovies?.count; const performerfilter = new ListFilterModel(GQL.FilterMode.Performers); performerfilter.sortBy = "created_at"; performerfilter.sortDirection = GQL.SortDirectionEnum.Desc; performerfilter.itemsPerPage = itemsPerPage; const performerResult = useFindPerformers(performerfilter); - const hasPerformers = - performerResult.data && - performerResult.data.findPerformers && - performerResult.data.findPerformers.count > 0; + const hasPerformers = !!performerResult?.data?.findPerformers?.count; const galleryfilter = new ListFilterModel(GQL.FilterMode.Galleries); galleryfilter.sortBy = "date"; galleryfilter.sortDirection = GQL.SortDirectionEnum.Desc; galleryfilter.itemsPerPage = itemsPerPage; const galleryResult = useFindGalleries(galleryfilter); - const hasGalleries = - galleryResult.data && - galleryResult.data.findGalleries && - galleryResult.data.findGalleries.count > 0; + const hasGalleries = !!galleryResult?.data?.findGalleries?.count; const messages = defineMessages({ emptyServer: { @@ -82,182 +67,108 @@ const Recommendations: React.FC = () => { defaultMessage: "Add some scenes to your server to view recommendations on this page.", }, - latestScenes: { - id: "latest_scenes", - defaultMessage: "Latest Scenes", + recentlyAddedStudios: { + id: "recently_added_studios", + defaultMessage: "Recently Added Studios", }, - mostActiveStudios: { - id: "most_active_studios", - defaultMessage: "Most Active Studios", + recentlyAddedPerformers: { + id: "recently_added_performers", + defaultMessage: "Recently Added Performers", }, - latestMovies: { - id: "latest_movies", - defaultMessage: "Latest Movies", + recentlyReleasedGalleries: { + id: "recently_released_galleries", + defaultMessage: "Recently Released Galleries", }, - latestPerformers: { - id: "latest_performers", - defaultMessage: "Latest Performers", + recentlyReleasedMovies: { + id: "recently_released_movies", + defaultMessage: "Recently Released Movies", }, - latestGalleries: { - id: "latest_galleries", - defaultMessage: "Latest Galleries", + recentlyReleasedScenes: { + id: "recently_released_scenes", + defaultMessage: "Recently Released Scenes", + }, + viewAll: { + id: "view_all", + defaultMessage: "View All", }, }); - var settings = { - dots: !isTouch, - arrows: !isTouch, - infinite: !isTouch, - speed: 300, - variableWidth: true, - swipeToSlide: true, - slidesToShow: 5, - slidesToScroll: !isTouch ? 5 : 1, - responsive: [ - { - breakpoint: 1909, - settings: { - slidesToShow: 4, - slidesToScroll: !isTouch ? 4 : 1, - }, - }, - { - breakpoint: 1542, - settings: { - slidesToShow: 3, - slidesToScroll: !isTouch ? 3 : 1, - }, - }, - { - breakpoint: 1170, - settings: { - slidesToShow: 2, - slidesToScroll: !isTouch ? 2 : 1, - }, - }, - { - breakpoint: 801, - settings: { - slidesToShow: 1, - slidesToScroll: 1, - dots: false, - }, - }, - ], - }; - const queue = SceneQueue.fromListFilterModel(scenefilter); + if ( + sceneResult.loading || + studioResult.loading || + movieResult.loading || + performerResult.loading || + galleryResult.loading + ) { + return ; + } else { + return ( +
+ {!hasScenes && + !hasStudios && + !hasMovies && + !hasPerformers && + !hasGalleries ? ( +
+ {intl.formatMessage(messages.emptyServer)} +
+ ) : ( +
+ {hasScenes && ( + + )} - return ( -
- {!hasScenes && - !hasStudios && - !hasMovies && - !hasPerformers && - !hasGalleries ? ( -
- {intl.formatMessage(messages.emptyServer)} -
- ) : ( -
- {hasScenes && ( -
-
-
-

{intl.formatMessage(messages.latestScenes)}

-
- View all -
- - {sceneResult.data?.findScenes.scenes.map((scene, index) => ( - - ))} - -
- )} + {hasStudios && ( + + )} - {hasStudios && ( -
-
-
-

{intl.formatMessage(messages.mostActiveStudios)}

-
- View all -
- - {studioResult.data?.findStudios.studios.map((studio) => ( - - ))} - -
- )} + {hasMovies && ( + + )} - {hasMovies && ( -
-
-
-

{intl.formatMessage(messages.latestMovies)}

-
- View all -
- - {movieResult.data?.findMovies.movies.map((p) => ( - - ))} - -
- )} + {hasPerformers && ( + + )} - {hasPerformers && ( -
-
-
-

{intl.formatMessage(messages.latestPerformers)}

-
- - View all - -
- - {performerResult.data?.findPerformers.performers.map((p) => ( - - ))} - -
- )} - - {hasGalleries && ( -
-
-
-

{intl.formatMessage(messages.latestGalleries)}

-
- View all -
- - {galleryResult.data?.findGalleries.galleries.map((gallery) => ( - - ))} - -
- )} -
- )} -
- ); + {hasGalleries && ( + + )} +
+ )} +
+ ); + } }; export default Recommendations; diff --git a/ui/v2.5/src/components/Recommendations/styles.scss b/ui/v2.5/src/components/Recommendations/styles.scss index e47b237fd..ab8fb2ab7 100644 --- a/ui/v2.5/src/components/Recommendations/styles.scss +++ b/ui/v2.5/src/components/Recommendations/styles.scss @@ -28,6 +28,7 @@ display: inline-flex; font-size: 1.25rem; font-weight: 600; + margin-bottom: 0; text-transform: uppercase; white-space: normal; } diff --git a/ui/v2.5/src/components/Scenes/SceneRecommendationRow.tsx b/ui/v2.5/src/components/Scenes/SceneRecommendationRow.tsx new file mode 100644 index 000000000..ecd713d5b --- /dev/null +++ b/ui/v2.5/src/components/Scenes/SceneRecommendationRow.tsx @@ -0,0 +1,45 @@ +import React, { FunctionComponent } from "react"; +import { FindScenesQueryResult } from "src/core/generated-graphql"; +import Slider from "react-slick"; +import { SceneCard } from "./SceneCard"; +import { SceneQueue } from "src/models/sceneQueue"; +import { ListFilterModel } from "src/models/list-filter/filter"; +import { getSlickSliderSettings } from "src/core/recommendations"; + +interface IProps { + isTouch: boolean; + filter: ListFilterModel; + result: FindScenesQueryResult; + queue: SceneQueue; + header: String; + linkText: String; +} + +export const SceneRecommendationRow: FunctionComponent = ( + props: IProps +) => { + const cardCount = props.result.data?.findScenes.count; + return ( +
+
+
+

{props.header}

+
+ + {props.linkText} + +
+ + {props.result.data?.findScenes.scenes.map((scene, index) => ( + + ))} + +
+ ); +}; diff --git a/ui/v2.5/src/components/Studios/StudioRecommendationRow.tsx b/ui/v2.5/src/components/Studios/StudioRecommendationRow.tsx new file mode 100644 index 000000000..ea26e7fbe --- /dev/null +++ b/ui/v2.5/src/components/Studios/StudioRecommendationRow.tsx @@ -0,0 +1,37 @@ +import React, { FunctionComponent } from "react"; +import { FindStudiosQueryResult } from "src/core/generated-graphql"; +import Slider from "react-slick"; +import { StudioCard } from "./StudioCard"; +import { ListFilterModel } from "src/models/list-filter/filter"; +import { getSlickSliderSettings } from "src/core/recommendations"; + +interface IProps { + isTouch: boolean; + filter: ListFilterModel; + result: FindStudiosQueryResult; + header: String; + linkText: String; +} + +export const StudioRecommendationRow: FunctionComponent = ( + props: IProps +) => { + const cardCount = props.result.data?.findStudios.count; + return ( +
+
+
+

{props.header}

+
+ + {props.linkText} + +
+ + {props.result.data?.findStudios.studios.map((studio) => ( + + ))} + +
+ ); +}; diff --git a/ui/v2.5/src/core/recommendations.ts b/ui/v2.5/src/core/recommendations.ts new file mode 100644 index 000000000..b0a1232e4 --- /dev/null +++ b/ui/v2.5/src/core/recommendations.ts @@ -0,0 +1,57 @@ +function determineSlidesToScroll( + cardCount: number, + prefered: number, + isTouch: boolean +) { + if (isTouch) { + return 1; + } else if (cardCount! > prefered) { + return prefered; + } else { + return cardCount; + } +} + +export function getSlickSliderSettings(cardCount: number, isTouch: boolean) { + return { + dots: !isTouch, + arrows: !isTouch, + infinite: !isTouch, + speed: 300, + variableWidth: true, + swipeToSlide: true, + slidesToShow: cardCount! > 5 ? 5 : cardCount, + slidesToScroll: determineSlidesToScroll(cardCount!, 5, isTouch), + responsive: [ + { + breakpoint: 1909, + settings: { + slidesToShow: cardCount! > 4 ? 4 : cardCount, + slidesToScroll: determineSlidesToScroll(cardCount!, 4, isTouch), + }, + }, + { + breakpoint: 1542, + settings: { + slidesToShow: cardCount! > 3 ? 3 : cardCount, + slidesToScroll: determineSlidesToScroll(cardCount!, 3, isTouch), + }, + }, + { + breakpoint: 1170, + settings: { + slidesToShow: cardCount! > 2 ? 2 : cardCount, + slidesToScroll: determineSlidesToScroll(cardCount!, 2, isTouch), + }, + }, + { + breakpoint: 801, + settings: { + slidesToShow: 1, + slidesToScroll: 1, + dots: false, + }, + }, + ], + }; +} diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index b22541510..31ddca247 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -769,10 +769,6 @@ "interactive": "Interactive", "interactive_speed": "Interactive speed", "isMissing": "Is Missing", - "latest_galleries": "Latest Galleries", - "latest_movies": "Latest Movies", - "latest_performers": "Latest Performers", - "latest_scenes": "Latest Scenes", "library": "Library", "loading": { "generic": "Loading…" @@ -796,7 +792,6 @@ }, "megabits_per_second": "{value} megabits per second", "metadata": "Metadata", - "most_active_studios": "Most Active Studios", "movie": "Movie", "movie_scene_number": "Movie Scene Number", "movies": "Movies", @@ -830,6 +825,11 @@ "queue": "Queue", "random": "Random", "rating": "Rating", + "recently_added_performers": "Recently Added Performers", + "recently_added_studios": "Recently Added Studios", + "recently_released_galleries": "Recently Released Galleries", + "recently_released_movies": "Recently Released Movies", + "recently_released_scenes": "Recently Released Scenes", "resolution": "Resolution", "scene": "Scene", "sceneTagger": "Scene Tagger", @@ -969,6 +969,7 @@ "updated_at": "Updated At", "url": "URL", "videos": "Videos", + "view_all": "View All", "weight": "Weight", "years_old": "years old", "stashbox": {