diff --git a/frontend/js/actions/movies.js b/frontend/js/actions/movies.js index 128234e..fb27b98 100644 --- a/frontend/js/actions/movies.js +++ b/frontend/js/actions/movies.js @@ -58,6 +58,14 @@ export function deleteMovie(imdbId, lastFetchUrl) { ) } +export function movieWishlistToggle(imdbId, currentState) { + if (currentState == true) { + return deleteMovieFromWishlist(imdbId) + } else { + return addMovieToWishlist(imdbId) + } +} + export function addMovieToWishlist(imdbId) { return request( "MOVIE_ADD_TO_WISHLIST", diff --git a/frontend/js/actions/shows.js b/frontend/js/actions/shows.js index 6d097ef..b5321c1 100644 --- a/frontend/js/actions/shows.js +++ b/frontend/js/actions/shows.js @@ -65,11 +65,18 @@ export function deleteShowFromWishlist(imdbId) { ) } +export function showWishlistToggle(currentState, imdbId, season = 0, episode = 0) { + if (currentState === true) { + return deleteShowFromWishlist(imdbId); + } else { + return addShowToWishlist(imdbId, season, episode); + } +} + export function updateShowWishlistStore(imdbId, wishlisted, season = null, episode = null) { return { type: "SHOW_UPDATE_STORE_WISHLIST", payload: { - wishlisted: wishlisted, imdbId, season, episode, diff --git a/frontend/js/components/buttons/showMore.js b/frontend/js/components/buttons/showMore.js index c2a247f..d2a3964 100644 --- a/frontend/js/components/buttons/showMore.js +++ b/frontend/js/components/buttons/showMore.js @@ -13,7 +13,7 @@ export const ShowMore = ({ children, id, inLibrary }) => { setDisplay(true)} - className="badge badge-pill badge-secondary clickable" + className="badge badge-pill badge-secondary clickable mb-1" > More options ... diff --git a/frontend/js/components/buttons/wishlist.js b/frontend/js/components/buttons/wishlist.js new file mode 100644 index 0000000..7bba31a --- /dev/null +++ b/frontend/js/components/buttons/wishlist.js @@ -0,0 +1,17 @@ +import React from "react" +import PropTypes from "prop-types" + +export const WishlistButton = ({ wishlisted, wishlist }) => { + return ( + + + + ); +} +WishlistButton.propTypes = { + wishlisted: PropTypes.bool.isRequired, + wishlist: PropTypes.func.isRequired, +} diff --git a/frontend/js/components/details/rating.js b/frontend/js/components/details/rating.js index 7e99d4e..b1e4f5a 100644 --- a/frontend/js/components/details/rating.js +++ b/frontend/js/components/details/rating.js @@ -6,7 +6,7 @@ export const Rating = ({ rating, votes }) => { return ( - + {Number(rating).toFixed(1)} {votes !== 0 && ({votes} votes) diff --git a/frontend/js/components/details/title.js b/frontend/js/components/details/title.js index b03f67b..3c29fde 100644 --- a/frontend/js/components/details/title.js +++ b/frontend/js/components/details/title.js @@ -1,25 +1,23 @@ import React from "react" import PropTypes from "prop-types" -export const Title = ({ title, xs }) => { +import { WishlistButton } from "../buttons/wishlist" + +export const Title = ({ title, wishlist, wishlisted }) => { if (title === "") { return null; } - if (xs) { - return (
{title}
) - } else { - return ( - -

{title}

-

{title}

-
- ); - } + return ( + + + {title} + + ); } Title.propTypes = { title: PropTypes.string, - xs: PropTypes.bool, + wishlist: PropTypes.func, + wishlisted: PropTypes.bool, }; Title.defaultProps = { title: "", - xs: false, }; diff --git a/frontend/js/components/details/tracking.js b/frontend/js/components/details/tracking.js new file mode 100644 index 0000000..935213f --- /dev/null +++ b/frontend/js/components/details/tracking.js @@ -0,0 +1,41 @@ +import React from "react" +import PropTypes from "prop-types" + +export const TrackingLabel = ({ wishlisted, inLibrary, trackedSeason, trackedEpisode }) => { + if (wishlisted === false) { + return null; + } + + if (wishlisted === true && inLibrary === true) { + return null; + } + + if (trackedEpisode === null && trackedSeason === null) { + return null; + } + + let str = "" + if (trackedSeason === 0 && trackedEpisode === 0) { + str = "All the episodes will be downloaded automatically"; + } else if (trackedSeason > 0 && trackedEpisode > 0) { + str = `All the episodes will be downloaded automatically starting from + season ${trackedSeason} episode ${trackedEpisode}`; + } else { + str = "This movie will be downloaded automatically" + } + + return ( + + + + {str} + + + ) +} +TrackingLabel.propTypes = { + wishlisted: PropTypes.bool, + inLibrary: PropTypes.bool, + trackedSeason: PropTypes.number, + trackedEpisode: PropTypes.number, +}; diff --git a/frontend/js/components/list/details.js b/frontend/js/components/list/details.js index 2111ffe..f996447 100644 --- a/frontend/js/components/list/details.js +++ b/frontend/js/components/list/details.js @@ -2,9 +2,12 @@ import React from "react" import PropTypes from "prop-types" import { Map } from "immutable" +import { inLibrary, isWishlisted } from "../../utils" + import { DownloadAndStream } from "../buttons/download" import { ImdbBadge } from "../buttons/imdb" +import { TrackingLabel } from "../details/tracking" import { Genres } from "../details/genres" import { Plot } from "../details/plot" import { PolochonMetadata } from "../details/polochon" @@ -20,11 +23,10 @@ const ListDetails = (props) => { return (
- - <TrackingLabel - wishlisted={props.data.get("wishlisted")} - trackedSeason={props.data.get("tracked_season")} - trackedEpisode={props.data.get("tracked_episode")} + <Title + title={props.data.get("title")} + wishlist={props.wishlist} + wishlisted={isWishlisted(props.data)} /> <ReleaseDate date={props.data.get("year")} /> <Runtime runtime={props.data.get("runtime")} /> @@ -41,6 +43,12 @@ const ListDetails = (props) => { subtitles={props.data.get("subtitles")} /> </div> + <TrackingLabel + wishlisted={props.data.get("wishlisted")} + inLibrary={inLibrary(props.data)} + trackedSeason={props.data.get("tracked_season")} + trackedEpisode={props.data.get("tracked_episode")} + /> <PolochonMetadata quality={props.data.get("quality")} releaseGroup={props.data.get("release_group")} @@ -56,37 +64,8 @@ const ListDetails = (props) => { } ListDetails.propTypes = { data: PropTypes.instanceOf(Map), + wishlist: PropTypes.func, loading: PropTypes.bool, children: PropTypes.object, }; export default ListDetails; - -const TrackingLabel = (props) => { - let wishlistStr = props.wishlisted ? "Wishlisted" : ""; - - if (props.trackedEpisode !== null && props.trackedSeason !== null - && props.trackedEpisode !== undefined && props.trackedSeason !== undefined) { - if ((props.trackedSeason === 0) && (props.trackedEpisode === 0)) { - wishlistStr = "Whole show tracked"; - } else { - wishlistStr = `Tracked from season ${props.trackedSeason} episode ${props.trackedEpisode}`; - } - } - - if (wishlistStr === "") { - return null; - } - - return ( - <p> - <span className="badge badge-secondary"> - <i className="fa fa-bookmark"></i> {wishlistStr} - </span> - </p> - ); -} -TrackingLabel.propTypes = { - wishlisted: PropTypes.bool, - trackedSeason: PropTypes.number, - trackedEpisode: PropTypes.number, -}; diff --git a/frontend/js/components/movies/list.js b/frontend/js/components/movies/list.js index 6a4cb6f..dbb3674 100644 --- a/frontend/js/components/movies/list.js +++ b/frontend/js/components/movies/list.js @@ -2,12 +2,12 @@ import React from "react" import PropTypes from "prop-types" import { OrderedMap, Map } from "immutable" import { connect } from "react-redux" -import { selectMovie, updateFilter } from "../../actions/movies" +import { selectMovie, updateFilter, movieWishlistToggle } from "../../actions/movies" import ListDetails from "../list/details" import ListPosters from "../list/posters" -import { inLibrary } from "../../utils" +import { inLibrary, isWishlisted } from "../../utils" import { ShowMore } from "../buttons/showMore" @@ -24,7 +24,7 @@ function mapStateToProps(state) { }; } const mapDispatchToProps = { - selectMovie, updateFilter, + selectMovie, updateFilter, movieWishlistToggle, }; const MovieList = (props) => { @@ -50,7 +50,11 @@ const MovieList = (props) => { params={props.match.params} loading={props.loading} /> - <ListDetails data={selectedMovie} loading={props.loading}> + <ListDetails + data={selectedMovie} + loading={props.loading} + wishlist={() => props.movieWishlistToggle(selectedMovie.get("imdb_id"), isWishlisted(selectedMovie))} + > <ShowMore id={selectedMovie.get("imdb_id")} inLibrary={inLibrary(selectedMovie)} @@ -79,6 +83,7 @@ MovieList.propTypes = { filter: PropTypes.string, loading: PropTypes.bool, updateFilter: PropTypes.func, + movieWishlistToggle: PropTypes.func, selectMovie: PropTypes.func, match: PropTypes.object, }; diff --git a/frontend/js/components/shows/details/episode.js b/frontend/js/components/shows/details/episode.js index eda02bc..ef15257 100644 --- a/frontend/js/components/shows/details/episode.js +++ b/frontend/js/components/shows/details/episode.js @@ -1,8 +1,11 @@ import React from "react" import PropTypes from "prop-types" import { Map } from "immutable" +import { connect } from "react-redux" -import { inLibrary, prettyEpisodeName } from "../../../utils" +import { showWishlistToggle } from "../../../actions/shows" + +import { inLibrary, isEpisodeWishlisted, prettyEpisodeName } from "../../../utils" import { Plot } from "../../details/plot" import { PolochonMetadata } from "../../details/polochon" @@ -17,13 +20,28 @@ import { EpisodeSubtitlesButton } from "./subtitlesButton" import { EpisodeThumb } from "./episodeThumb" import { EpisodeTorrentsButton } from "./torrentsButton" -export const Episode = (props) => ( +const mapStateToProps = (state) => ({ + trackedSeason: state.showStore.getIn(["show", "tracked_season"], null), + trackedEpisode: state.showStore.getIn(["show", "tracked_episode"], null), +}) + +const episode = (props) => ( <div className="d-flex flex-column flex-lg-row mb-3 pb-3 border-bottom border-light"> <EpisodeThumb url={props.data.get("thumb")} /> <div className="d-flex flex-column"> <Title title={`${props.data.get("episode")}. ${props.data.get("title")}`} - xs + wishlisted={isEpisodeWishlisted( + props.data, + props.trackedSeason, + props.trackedEpisode, + )} + wishlist={() => props.showWishlistToggle( + isEpisodeWishlisted(props.data), + props.data.get("show_imdb_id"), + props.data.get("season"), + props.data.get("episode"), + )} /> <ReleaseDate date={props.data.get("aired")} /> <Runtime runtime={props.data.get("runtime")} /> @@ -65,7 +83,12 @@ export const Episode = (props) => ( </div> </div> ) -Episode.propTypes = { +episode.propTypes = { data: PropTypes.instanceOf(Map).isRequired, + trackedSeason: PropTypes.number, + trackedEpisode: PropTypes.number, showName: PropTypes.string.isRequired, + showWishlistToggle: PropTypes.func, }; + +export const Episode = connect(mapStateToProps, {showWishlistToggle})(episode); diff --git a/frontend/js/components/shows/details/header.js b/frontend/js/components/shows/details/header.js index b5b5c5f..b17b71c 100644 --- a/frontend/js/components/shows/details/header.js +++ b/frontend/js/components/shows/details/header.js @@ -1,14 +1,21 @@ import React from "react" import PropTypes from "prop-types" import { Map } from "immutable" +import { connect } from "react-redux" + +import { isWishlisted } from "../../../utils" + +import { showWishlistToggle } from "../../../actions/shows" -import { TrackHeader } from "./track" -import { ImdbBadge } from "../../buttons/imdb" import { Plot } from "../../details/plot" import { Rating } from "../../details/rating" import { ReleaseDate } from "../../details/releaseDate" +import { Title } from "../../details/title" +import { TrackingLabel } from "../../details/tracking" -export const Header = (props) => ( +import { ImdbBadge } from "../../buttons/imdb" + +export const header = (props) => ( <div className="card col-12 col-md-10 offset-md-1 mt-n3 mb-3"> <div className="d-flex flex-column flex-md-row"> <div className="d-flex justify-content-center"> @@ -19,33 +26,42 @@ export const Header = (props) => ( </div> <div> <div className="card-body"> - <h5 className="card-title">{props.data.get("title")}</h5> + <p className="card-title"> + <Title + title={props.data.get("title")} + wishlisted={isWishlisted(props.data)} + wishlist={() => props.showWishlistToggle( + isWishlisted(props.data), props.data.get("imdb_id"), + )} + /> + </p> <p className="card-text"> <ReleaseDate date={props.data.get("year")} /> </p> <p className="card-text"> <Rating rating={props.data.get("rating")} /> </p> - <span className="card-text"> + <p className="card-text"> <ImdbBadge imdbId={props.data.get("imdb_id")} /> - </span> + </p> + <p className="card-text"> + <TrackingLabel + inLibrary={false} + trackedSeason={props.data.get("tracked_season")} + trackedEpisode={props.data.get("tracked_episode")} + /> + </p> <p className="card-text"> <Plot plot={props.data.get("plot")} /> </p> - <span className="card-text"> - <TrackHeader - data={props.data} - addToWishlist={props.addToWishlist} - deleteFromWishlist={props.deleteFromWishlist} - /> - </span> </div> </div> </div> </div> ); -Header.propTypes = { +header.propTypes = { data: PropTypes.instanceOf(Map), - deleteFromWishlist: PropTypes.func, - addToWishlist: PropTypes.func, + showWishlistToggle: PropTypes.func, }; + +export const Header = connect(null, {showWishlistToggle})(header); diff --git a/frontend/js/components/shows/details/track.js b/frontend/js/components/shows/details/track.js deleted file mode 100644 index 24c1fcb..0000000 --- a/frontend/js/components/shows/details/track.js +++ /dev/null @@ -1,82 +0,0 @@ -import React from "react" -import PropTypes from "prop-types" -import { Map } from "immutable" -import { connect } from "react-redux" - -import { addShowToWishlist, deleteShowFromWishlist } from "../../../actions/shows" - -import Tooltip from "react-bootstrap/Tooltip" -import OverlayTrigger from "react-bootstrap/OverlayTrigger" - -export const trackHeader = (props) => { - const trackedSeason = props.data.get("tracked_season"); - const trackedEpisode = props.data.get("tracked_episode"); - const imdbId = props.data.get("imdb_id"); - const wishlisted = (trackedSeason !== null && trackedEpisode !== null); - - const handleClick = () => { - if (wishlisted) { - props.deleteShowFromWishlist(imdbId); - } else { - props.addShowToWishlist(imdbId); - } - } - - if (wishlisted) { - const msg = (trackedSeason !== 0 && trackedEpisode !== 0) - ? (<p>Show tracked from <strong>season {trackedSeason} episode {trackedEpisode}</strong></p>) - : (<p>Whole show tracked</p>); - - return ( - <span className="card-text"> - {msg} - <a className="btn btn-sm btn-danger" onClick={handleClick}> - <i className="fa fa-bookmark"></i> Untrack the show - </a> - </span> - ); - } - - return ( - <span className="card-text"> - <p>Tracking inactive</p> - <a className="btn btn-sm btn-info" onClick={(e) => handleClick(e)}> - <i className="fa fa-bookmark-o"></i> Track the whole show - </a> - </span> - ); -} -trackHeader.propTypes = { - data: PropTypes.instanceOf(Map), - addShowToWishlist: PropTypes.func, - deleteShowFromWishlist: PropTypes.func, -}; -export const TrackHeader = connect(null, { - addShowToWishlist, - deleteShowFromWishlist, -})(trackHeader); - -export const trackButton = (props) => { - const imdbId = props.data.get("show_imdb_id"); - const season = props.data.get("season"); - const episode = props.data.get("episode"); - - const tooltipId = `tooltip-${props.data.season}-${props.data.episode}`; - const tooltip = ( - <Tooltip id={tooltipId}>Track show from here</Tooltip> - ); - - return ( - <OverlayTrigger placement="top" overlay={tooltip}> - <span className="btn clickable" - onClick={() => props.addShowToWishlist(imdbId, season, episode)}> - <i className="fa fa-bookmark"></i> - </span> - </OverlayTrigger> - ); -} -trackButton.propTypes = { - data: PropTypes.instanceOf(Map), - addShowToWishlist: PropTypes.func, -}; -export const TrackButton = connect(null, {addShowToWishlist})(trackButton); diff --git a/frontend/js/components/shows/list.js b/frontend/js/components/shows/list.js index a1a2d9a..2361f2c 100644 --- a/frontend/js/components/shows/list.js +++ b/frontend/js/components/shows/list.js @@ -2,8 +2,10 @@ import React from "react" import PropTypes from "prop-types" import { Map } from "immutable" import { connect } from "react-redux" -import { selectShow, addShowToWishlist, - deleteShowFromWishlist, getShowDetails, updateFilter } from "../../actions/shows" +import { selectShow, showWishlistToggle, + getShowDetails, updateFilter } from "../../actions/shows" + +import { isWishlisted } from "../../utils" import ListDetails from "../list/details" import ListPosters from "../list/posters" @@ -18,7 +20,7 @@ function mapStateToProps(state) { }; } const mapDispatchToProps = { - selectShow, addShowToWishlist, deleteShowFromWishlist, + selectShow, showWishlistToggle, getShowDetails, updateFilter, }; @@ -48,7 +50,22 @@ const ShowList = (props) => { params={props.match.params} loading={props.loading} /> - <ListDetails data={selectedShow} loading={props.loading} /> + <ListDetails + data={selectedShow} + loading={props.loading} + wishlist={() => props.showWishlistToggle( + isWishlisted(selectedShow), selectedShow.get("imdb_id"), + )} + > + <span> + <button onClick={ + () => showDetails(selectedShow.get("imdb_id"))} + className="btn btn-primary btn-sm w-md-100"> + <i className="fa fa-external-link mr-1" /> + Details + </button> + </span> + </ListDetails> </div> ); } @@ -60,8 +77,7 @@ ShowList.propTypes = { selectedImdbId: PropTypes.string, filter: PropTypes.string, loading: PropTypes.bool, - deleteShowFromWishlist: PropTypes.func, - addShowToWishlist: PropTypes.func, + showWishlistToggle: PropTypes.func, selectShow: PropTypes.func, getShowDetails: PropTypes.func, updateFilter: PropTypes.func, diff --git a/frontend/js/reducers/show.js b/frontend/js/reducers/show.js index 2eef6b6..f748d3c 100644 --- a/frontend/js/reducers/show.js +++ b/frontend/js/reducers/show.js @@ -11,16 +11,10 @@ const handlers = { "SHOW_FETCH_DETAILS_PENDING": state => state.set("loading", true), "SHOW_FETCH_DETAILS_FULFILLED": (state, action) => sortEpisodes(state, action.payload.response.data), "SHOW_UPDATE_STORE_WISHLIST": (state, action) => { - let season = action.payload.season; - let episode = action.payload.episode; - if (action.payload.wishlisted && season === null) { - season = 0; - episode = 0; - } return state.mergeDeep(fromJS({ "show": { - "tracked_season": season, - "tracked_episode": episode, + "tracked_season": action.payload.season, + "tracked_episode": action.payload.episode, } }))}, "EPISODE_GET_DETAILS_PENDING": (state, action) => state.setIn(["show", "seasons", action.payload.main.season, action.payload.main.episode, "fetching"], true), diff --git a/frontend/js/utils.js b/frontend/js/utils.js index f94dec1..f0d4ef4 100644 --- a/frontend/js/utils.js +++ b/frontend/js/utils.js @@ -17,3 +17,38 @@ export const prettyEpisodeName = (showName, season, episode) => export const inLibrary = (element) => element.get("polochon_url", "") !== ""; + +export const isWishlisted = (element) => { + const wishlisted = element.get("wishlisted", undefined) + if (wishlisted !== undefined) { + return wishlisted; + } + + const trackedSeason = element.get("tracked_season", null); + const trackedEpisode = element.get("tracked_episode", null); + if ((trackedSeason !== null) && (trackedEpisode !== null)) { + return true; + } + + return false +} + +export const isEpisodeWishlisted = (element, trackedSeason, trackedEpisode) => { + if ((trackedSeason === null) && (trackedEpisode === null)) { + return false; + } + + if ((trackedSeason === 0) && (trackedEpisode === 0)) { + return true + } + + const season = element.get("season", 0); + const episode = element.get("episode", 0); + if (season < trackedSeason) { + return false + } else if (season > trackedSeason) { + return true + } else { + return (episode >= trackedEpisode) + } +} diff --git a/frontend/scss/app.scss b/frontend/scss/app.scss index bca2dfb..fb2a248 100644 --- a/frontend/scss/app.scss +++ b/frontend/scss/app.scss @@ -60,7 +60,7 @@ div.show.dropdown.nav-item > div { .plot { text-align: justify; - max-height: 50%; + max-height: 65%; overflow: auto; } @@ -119,3 +119,14 @@ div.sweet-alert > h2 { .player-modal { width: 90%; } + +.title { + font-size: 1.5rem; + @include media-breakpoint-down(md) { + font-size: 1.2rem; + } +} + +.wishlist-button:hover > i { + color: $primary; +}