diff --git a/ui/v2.5/package.json b/ui/v2.5/package.json index 839a4eb27..67d128a9a 100644 --- a/ui/v2.5/package.json +++ b/ui/v2.5/package.json @@ -64,8 +64,10 @@ "react-router-dom": "^5.2.0", "react-router-hash-link": "^2.3.1", "react-select": "^4.0.2", + "react-slick": "^0.29.0", "remark-gfm": "^1.0.0", "sass": "^1.32.5", + "slick-carousel": "^1.8.1", "string.prototype.replaceall": "^1.0.4", "subscriptions-transport-ws": "^0.9.18", "thehandy": "^1.0.3", @@ -99,6 +101,7 @@ "@types/react-router-bootstrap": "^0.24.5", "@types/react-router-dom": "5.1.7", "@types/react-router-hash-link": "^1.2.1", + "@types/react-slick": "^0.23.8", "@types/video.js": "^7.3.28", "@types/videojs-seek-buttons": "^2.1.0", "@typescript-eslint/eslint-plugin": "^4.33.0", diff --git a/ui/v2.5/src/App.tsx b/ui/v2.5/src/App.tsx index 5e5185a70..5b80f5676 100755 --- a/ui/v2.5/src/App.tsx +++ b/ui/v2.5/src/App.tsx @@ -19,6 +19,7 @@ import Galleries from "./components/Galleries/Galleries"; import { MainNavbar } from "./components/MainNavbar"; import { PageNotFound } from "./components/PageNotFound"; import Performers from "./components/Performers/Performers"; +import Recommendations from "./components/Recommendations/Recommendations"; import Scenes from "./components/Scenes/Scenes"; import { Settings } from "./components/Settings/Settings"; import { Stats } from "./components/Stats"; @@ -117,7 +118,7 @@ export const App: React.FC = () => { return ( - + @@ -125,6 +126,7 @@ export const App: React.FC = () => { + { + + + { + function isTouchEnabled() { + return "ontouchstart" in window || navigator.maxTouchPoints > 0; + } + + const isTouch = isTouchEnabled(); + + const intl = useIntl(); + const itemsPerPage = 25; + const scenefilter = new ListFilterModel(GQL.FilterMode.Scenes); + scenefilter.sortBy = "date"; + 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 studiofilter = new ListFilterModel(GQL.FilterMode.Studios); + studiofilter.sortBy = "scenes_count"; + 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 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 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 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 messages = defineMessages({ + emptyServer: { + id: "empty_server", + defaultMessage: + "Add some scenes to your server to view recommendations on this page.", + }, + latestScenes: { + id: "latest_scenes", + defaultMessage: "Latest Scenes", + }, + mostActiveStudios: { + id: "most_active_studios", + defaultMessage: "Most Active Studios", + }, + latestMovies: { + id: "latest_movies", + defaultMessage: "Latest Movies", + }, + latestPerformers: { + id: "latest_performers", + defaultMessage: "Latest Performers", + }, + latestGalleries: { + id: "latest_galleries", + defaultMessage: "Latest Galleries", + }, + }); + + 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); + + 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 && ( +
+
+
+

{intl.formatMessage(messages.mostActiveStudios)}

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

{intl.formatMessage(messages.latestMovies)}

+
+ View all +
+ + {movieResult.data?.findMovies.movies.map((p) => ( + + ))} + +
+ )} + + {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) => ( + + ))} + +
+ )} +
+ )} +
+ ); +}; + +export default Recommendations; diff --git a/ui/v2.5/src/components/Recommendations/styles.scss b/ui/v2.5/src/components/Recommendations/styles.scss new file mode 100644 index 000000000..e47b237fd --- /dev/null +++ b/ui/v2.5/src/components/Recommendations/styles.scss @@ -0,0 +1,370 @@ +.recommendations-container { + padding-left: 20px; + padding-right: 20px; + + @media (max-width: 576px) { + padding-left: 0; + padding-right: 0; + } +} + +.no-recommendations { + font-size: 1.5rem; + padding-top: 2rem; + text-align: center; +} + +.recommendation-row-head { + align-items: center; + border-radius: 0; + -webkit-box-align: center; + -webkit-box-pack: justify; + display: flex; + justify-content: space-between; + padding: 15px 0; +} + +.recommendation-row-head h2 { + display: inline-flex; + font-size: 1.25rem; + font-weight: 600; + text-transform: uppercase; + white-space: normal; +} + +.recommendation-row-head a { + display: inline-flex; + font-size: 1.2rem; + white-space: normal; +} + +.recommendations-container .studio-card hr, +.recommendations-container .movie-card hr, +.recommendations-container .gallery-card hr { + margin-top: auto; +} + +/* Slider */ +.slick-slider { + box-sizing: border-box; + display: block; + position: relative; + -webkit-tap-highlight-color: transparent; + -ms-touch-action: pan-y; + touch-action: pan-y; + -webkit-touch-callout: none; + -khtml-user-select: none; + -ms-user-select: none; + -moz-user-select: none; + -webkit-user-select: none; + user-select: none; +} + +.slick-list { + display: block; + margin: 0; + overflow: hidden; + padding: 0; + position: relative; +} + +.slick-list:focus { + outline: none; +} + +.slick-list.dragging { + cursor: pointer; + cursor: hand; +} + +.slick-slider .slick-track, +.slick-slider .slick-list { + -moz-transform: translate3d(0, 0, 0); + -ms-transform: translate3d(0, 0, 0); + -o-transform: translate3d(0, 0, 0); + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); +} + +.slick-track { + display: block; + left: 0; + margin-left: auto; + margin-right: auto; + position: relative; + top: 0; +} + +.slick-track::before, +.slick-track::after { + content: ""; + display: table; +} + +.slick-track::after { + clear: both; +} + +.slick-loading .slick-track { + visibility: hidden; +} + +.slick-slide { + display: none; + float: left; + + height: 100%; + min-height: 1px; +} + +[dir="rtl"] .slick-slide { + float: right; +} + +.slick-slide img { + display: block; +} + +.slick-slide.slick-loading img { + display: none; +} + +.slick-slide.dragging img { + pointer-events: none; +} + +.slick-initialized .slick-slide { + display: block; +} + +.slick-loading .slick-slide { + visibility: hidden; +} + +.slick-vertical .slick-slide { + border: 1px solid transparent; + display: block; + height: auto; +} + +.slick-arrow.slick-hidden { + display: none; +} + +.slick-loading .slick-list { + background: #fff url("slick-carousel/slick/ajax-loader.gif") center center + no-repeat; +} + +.slick-list .card-check { + display: none; +} + +.container-fluid .slick-track { + display: flex; +} + +.container-fluid .slick-slide { + display: flex; + height: auto; +} + +.slick-slide .card { + height: 100%; +} + +.slick-slide .studio-card-image { + height: 150px; +} + +@media (max-width: 576px) { + .slick-list .scene-card, + .slick-list .studio-card, + .slick-list .gallery-card { + width: 20rem; + } + + .slick-list .movie-card { + width: 16rem; + } + + .slick-list .performer-card { + width: 16rem; + } + + .slick-list .performer-card-image { + height: 24rem; + } +} + +/* Icons */ +@font-face { + font-family: slick; + font-style: normal; + font-weight: normal; + + src: url("slick-carousel/slick/fonts/slick.eot"); + src: url("slick-carousel/slick/fonts/slick.eot?#iefix") + format("embedded-opentype"), + url("slick-carousel/slick/fonts/slick.woff") format("woff"), + url("slick-carousel/slick/fonts/slick.ttf") format("truetype"), + url("slick-carousel/slick/fonts/slick.svg#slick") format("svg"); +} + +/* Arrows */ +.slick-prev, +.slick-next { + background: transparent; + border: none; + color: transparent; + cursor: pointer; + display: block; + font-size: 0; + height: 100%; + line-height: 0; + outline: none; + padding: 0; + position: absolute; + top: 50%; + -webkit-transform: translate(0, -50%); + -ms-transform: translate(0, -50%); + transform: translate(0, -50%); + width: 20px; +} + +.slick-prev:hover, +.slick-prev:focus, +.slick-next:hover, +.slick-next:focus { + background: transparent; + color: transparent; + outline: none; +} + +.slick-prev:hover::before, +.slick-prev:focus::before, +.slick-next:hover::before, +.slick-next:focus::before { + opacity: 1; +} + +.slick-prev.slick-disabled::before, +.slick-next.slick-disabled::before { + opacity: 0.25; +} + +.slick-prev::before, +.slick-next::before { + color: white; + font-family: slick; + font-size: 20px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + line-height: 1; + opacity: 0.75; +} + +.slick-prev { + left: -20px; +} + +[dir="rtl"] .slick-prev { + left: auto; + right: -20px; +} + +.slick-prev::before { + content: "←"; +} + +[dir="rtl"] .slick-prev::before { + content: "→"; +} + +.slick-next { + right: -25px; +} + +[dir="rtl"] .slick-next { + left: -25px; + right: auto; +} + +.slick-next::before { + content: "→"; +} + +[dir="rtl"] .slick-next::before { + content: "←"; +} + +/* Dots */ +.slick-dotted.slick-slider { + margin-bottom: 30px; +} + +.slick-dots { + bottom: -25px; + display: block; + list-style: none; + margin: 0; + padding: 0; + position: absolute; + text-align: center; + width: 100%; +} + +.slick-dots li { + cursor: pointer; + display: inline-block; + height: 20px; + margin: 0 5px; + padding: 0; + position: relative; + width: 20px; +} + +.slick-dots li button { + background: transparent; + border: 0; + color: transparent; + cursor: pointer; + display: block; + font-size: 0; + height: 20px; + line-height: 0; + outline: none; + padding: 5px; + width: 20px; +} + +.slick-dots li button:hover, +.slick-dots li button:focus { + outline: none; +} + +.slick-dots li button:hover::before, +.slick-dots li button:focus::before { + opacity: 1; +} + +.slick-dots li button::before { + color: white; + content: "-"; + font-family: slick; + font-size: 50px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + height: 20px; + left: 0; + opacity: 0.25; + position: absolute; + text-align: center; + top: 0; + width: 20px; +} + +.slick-dots li.slick-active button::before { + color: white; + opacity: 0.75; +} diff --git a/ui/v2.5/src/index.scss b/ui/v2.5/src/index.scss index 3e2651a69..497738e2f 100755 --- a/ui/v2.5/src/index.scss +++ b/ui/v2.5/src/index.scss @@ -8,6 +8,7 @@ @import "src/components/List/styles.scss"; @import "src/components/Movies/styles.scss"; @import "src/components/Performers/styles.scss"; +@import "src/components/Recommendations/styles.scss"; @import "src/components/Scenes/styles.scss"; @import "src/components/SceneDuplicateChecker/styles.scss"; @import "src/components/SceneFilenameParser/styles.scss"; diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index bd9a61b46..110ec9a02 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -121,6 +121,7 @@ "birth_year": "Birth Year", "birthdate": "Birthdate", "bitrate": "Bit Rate", + "captions": "Captions", "career_length": "Career Length", "component_tagger": { "config": { @@ -705,6 +706,7 @@ "scale": "Scale", "warmth": "Warmth" }, + "empty_server": "Add some scenes to your server to view recommendations on this page.", "ethnicity": "Ethnicity", "existing_value": "existing value", "eye_color": "Eye Colour", @@ -747,8 +749,11 @@ "instagram": "Instagram", "interactive": "Interactive", "interactive_speed": "Interactive speed", - "captions": "Captions", "isMissing": "Is Missing", + "latest_galleries": "Latest Galleries", + "latest_movies": "Latest Movies", + "latest_performers": "Latest Performers", + "latest_scenes": "Latest Scenes", "library": "Library", "loading": { "generic": "Loading…" @@ -772,6 +777,7 @@ }, "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", @@ -902,6 +908,7 @@ }, "stash_id": "Stash ID", "stash_ids": "Stash IDs", + "statistics": "Statistics", "stats": { "image_size": "Images size", "scenes_duration": "Scenes duration", diff --git a/ui/v2.5/yarn.lock b/ui/v2.5/yarn.lock index 514deb09e..a0a6a33fa 100644 --- a/ui/v2.5/yarn.lock +++ b/ui/v2.5/yarn.lock @@ -1505,6 +1505,13 @@ "@types/react-dom" "*" "@types/react-transition-group" "*" +"@types/react-slick@^0.23.8": + version "0.23.8" + resolved "https://registry.npmjs.org/@types/react-slick/-/react-slick-0.23.8.tgz" + integrity sha512-SfzSg++/3uyftVZaCgHpW+2fnJFsyJEQ/YdsuqfOWQ5lqUYV/gY/UwAnkw4qksCj5jalto/T5rKXJ8zeFldQeA== + dependencies: + "@types/react" "*" + "@types/react-transition-group@*", "@types/react-transition-group@^4.4.0": version "4.4.1" resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.1.tgz" @@ -2906,6 +2913,11 @@ end-of-stream@^1.1.0: dependencies: once "^1.4.0" +enquire.js@^2.1.6: + version "2.1.6" + resolved "https://registry.npmjs.org/enquire.js/-/enquire.js-2.1.6.tgz" + integrity sha1-PoeAybi4NQhMP2DhZtvDwqPImBQ= + enquirer@^2.3.5: version "2.3.6" resolved "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz" @@ -4664,6 +4676,13 @@ json-to-pretty-yaml@^1.2.2: remedial "^1.0.7" remove-trailing-spaces "^1.0.6" +json2mq@^0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz" + integrity sha1-tje9O6nqvhIsg+lyBIOusQ0skEo= + dependencies: + string-convert "^0.2.0" + json5@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" @@ -4895,6 +4914,11 @@ lodash.clonedeep@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz" + integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= + lodash.get@^4: version "4.4.2" resolved "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz" @@ -6524,6 +6548,17 @@ react-side-effect@^2.1.0: resolved "https://registry.yarnpkg.com/react-side-effect/-/react-side-effect-2.1.1.tgz#66c5701c3e7560ab4822a4ee2742dee215d72eb3" integrity sha512-2FoTQzRNTncBVtnzxFOk2mCpcfxQpenBMbk5kSVBg5UcPqV9fRbgY2zhb7GTWWOlpFmAxhClBDlIq8Rsubz1yQ== +react-slick@^0.29.0: + version "0.29.0" + resolved "https://registry.npmjs.org/react-slick/-/react-slick-0.29.0.tgz" + integrity sha512-TGdOKE+ZkJHHeC4aaoH85m8RnFyWqdqRfAGkhd6dirmATXMZWAxOpTLmw2Ll/jPTQ3eEG7ercFr/sbzdeYCJXA== + dependencies: + classnames "^2.2.5" + enquire.js "^2.1.6" + json2mq "^0.2.0" + lodash.debounce "^4.0.8" + resize-observer-polyfill "^1.5.0" + react-transition-group@^4.3.0, react-transition-group@^4.4.1: version "4.4.1" resolved "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz" @@ -6786,6 +6821,11 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= +resize-observer-polyfill@^1.5.0: + version "1.5.1" + resolved "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz" + integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== + resolve-from@5.0.0, resolve-from@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz" @@ -7043,6 +7083,11 @@ slice-ansi@^4.0.0: astral-regex "^2.0.0" is-fullwidth-code-point "^3.0.0" +slick-carousel@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/slick-carousel/-/slick-carousel-1.8.1.tgz#a4bfb29014887bb66ce528b90bd0cda262cc8f8d" + integrity sha512-XB9Ftrf2EEKfzoQXt3Nitrt/IPbT+f1fgqBdoxO3W/+JYvtEOW6EgxnWfr9GH6nmULv7Y2tPmEX3koxThVmebA== + snake-case@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz" @@ -7169,6 +7214,11 @@ strict-uri-encode@^2.0.0: resolved "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz" integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY= +string-convert@^0.2.0: + version "0.2.1" + resolved "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz" + integrity sha1-aYLMMEn7tM2F+LJFaLnZvznu/5c= + string-env-interpolation@1.0.1, string-env-interpolation@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/string-env-interpolation/-/string-env-interpolation-1.0.1.tgz"