386 lines
12 KiB
JavaScript

import React, { useState } from "react"
import PropTypes from "prop-types"
import { Map } from "immutable"
import { connect } from "react-redux"
import { withRouter } from "react-router"
import { addTorrent } from "../../actions/torrents"
import { refreshSubtitles } from "../../actions/subtitles"
import { addShowToWishlist, deleteShowFromWishlist, getEpisodeDetails, fetchShowDetails } from "../../actions/shows"
import Loader from "../loader/loader"
import DownloadButton from "../buttons/download"
import SubtitlesButton from "../buttons/subtitles"
import ImdbButton from "../buttons/imdb"
import RefreshIndicator from "../buttons/refresh"
import Tooltip from "react-bootstrap/Tooltip"
import OverlayTrigger from "react-bootstrap/OverlayTrigger"
import Dropdown from "react-bootstrap/Dropdown"
import SplitButton from "react-bootstrap/SplitButton"
// Helper to change 1 to 01
const pad = (d) => (d < 10) ? "0" + d.toString() : d.toString();
function mapStateToProps(state) {
return {
loading: state.showStore.get("loading"),
show: state.showStore.get("show"),
};
}
const mapDispatchToProps = {
addTorrent, addShowToWishlist, deleteShowFromWishlist,
fetchShowDetails, getEpisodeDetails, refreshSubtitles,
};
const ShowDetails = (props) => {
if (props.loading) {
return (<Loader />);
}
return (
<React.Fragment>
<Fanart url={props.show.get("fanart_url")} />
<div className="row no-gutters">
<Header
data={props.show}
addToWishlist={props.addShowToWishlist}
deleteFromWishlist={props.deleteShowFromWishlist}
/>
<SeasonsList
data={props.show}
addTorrent={props.addTorrent}
addToWishlist={props.addShowToWishlist}
getEpisodeDetails={props.getEpisodeDetails}
refreshSubtitles={props.refreshSubtitles}
/>
</div>
</React.Fragment>
);
}
ShowDetails.propTypes = {
loading: PropTypes.bool,
show: PropTypes.instanceOf(Map),
deleteShowFromWishlist: PropTypes.func,
addShowToWishlist: PropTypes.func,
addTorrent: PropTypes.func,
refreshSubtitles: PropTypes.func,
getEpisodeDetails: PropTypes.func,
};
export default connect(mapStateToProps, mapDispatchToProps)(ShowDetails);
const Fanart = (props) => (
<div className="show-fanart mx-n3 mt-n1">
<img
className="img w-100 object-fit-cover object-position-top mh-40vh"
src={props.url}
/>
</div>
)
Fanart.propTypes = {
url: PropTypes.string,
}
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">
<img
className="overflow-hidden object-fit-cover"
src={props.data.get("poster_url")}
/>
</div>
<div className="">
<div className="card-body">
<h5 className="card-title">{props.data.get("title")}</h5>
<p className="card-text">{props.data.get("year")}</p>
<p className="card-text">{props.data.get("rating")}</p>
<p className="card-text plot">{props.data.get("plot")}</p>
<p className="card-text">
<ImdbButton imdbId={props.data.get("imdb_id")} xs/>
</p>
<TrackHeader
data={props.data}
addToWishlist={props.addToWishlist}
deleteFromWishlist={props.deleteFromWishlist}
/>
</div>
</div>
</div>
</div>
);
Header.propTypes = {
data: PropTypes.instanceOf(Map),
deleteFromWishlist: PropTypes.func,
addToWishlist: PropTypes.func,
};
const SeasonsList = (props) => (
<div className="col col-12 col-md-10 offset-md-1">
{props.data.get("seasons").entrySeq().map(function([season, data]) {
return (
<Season
key={`season-list-key-${season}`}
data={data}
season={season}
showName={props.data.get("title")}
addTorrent={props.addTorrent}
addToWishlist={props.addToWishlist}
getEpisodeDetails={props.getEpisodeDetails}
refreshSubtitles={props.refreshSubtitles}
/>
);
})}
</div>
);
SeasonsList.propTypes = {
data: PropTypes.instanceOf(Map),
addToWishlist: PropTypes.func,
addTorrent: PropTypes.func,
refreshSubtitles: PropTypes.func,
getEpisodeDetails: PropTypes.func,
};
const Season = (props) => {
const [show, setShow] = useState(false);
const visibility = show ? "d-flex flex-column" : "d-none";
const icon = show ? "down" : "left"
return (
<div className="card mb-3">
<div className="card-header clickable" onClick={() => setShow(!show)}>
<h5 className="m-0">
Season {props.season}
<small className="text-primary"> ({props.data.toList().size} episodes)</small>
<i className={`float-right fa fa-chevron-${icon}`}></i>
</h5>
</div>
<div className={`card-body ${visibility}`}>
{props.data.toList().map(function(episode) {
let key = `${episode.get("season")}-${episode.get("episode")}`;
return (
<Episode
key={key}
data={episode}
showName={props.showName}
addTorrent={props.addTorrent}
addToWishlist={props.addToWishlist}
getEpisodeDetails={props.getEpisodeDetails}
refreshSubtitles={props.refreshSubtitles}
/>
)
})}
</div>
</div>
)
}
Season.propTypes = {
data: PropTypes.instanceOf(Map),
season: PropTypes.number,
showName: PropTypes.string,
addToWishlist: PropTypes.func,
addTorrent: PropTypes.func,
refreshSubtitles: PropTypes.func,
getEpisodeDetails: PropTypes.func,
};
const PolochonMetadata = (props) => {
if (!props.quality || props.quality === "") { return null }
return (
<span className="my-2 d-flex d-wrap">
<span className="badge badge-pill badge-light mx-1">{props.quality}</span>
<span className="badge badge-pill badge-light mx-1">{props.container} </span>
<span className="badge badge-pill badge-light mx-1">{props.videoCodec}</span>
<span className="badge badge-pill badge-light mx-1">{props.audioCodec}</span>
<span className="badge badge-pill badge-light mx-1">{props.releaseGroup}</span>
</span>
);
}
PolochonMetadata.propTypes = {
quality: PropTypes.string,
container: PropTypes.string,
videoCodec: PropTypes.string,
audioCodec: PropTypes.string,
releaseGroup: PropTypes.string,
};
const Episode = (props) => (
<div className="d-flex flex-wrap flex-md-nowrap align-items-center">
<TrackButton data={props.data} addToWishlist={props.addToWishlist} />
<span className="mx-2 text">{props.data.get("episode")}</span>
<span className="mx-2 text text-truncate flex-fill">{props.data.get("title")}</span>
<PolochonMetadata
quality={props.data.get("quality")}
releaseGroup={props.data.get("release_group")}
container={props.data.get("container")}
audioCodec={props.data.get("audio_codec")}
videoCodec={props.data.get("video_codec")}
/>
<div className="align-self-end btn-toolbar">
{props.data.get("polochon_url") !== "" &&
<SubtitlesButton
fetching={props.data.get("fetchingSubtitles")}
subtitles={props.data.get("subtitles")}
refreshSubtitles={props.refreshSubtitles}
resourceID={props.data.get("show_imdb_id")}
season={props.data.get("season")}
episode={props.data.get("episode")}
type="episode"
xs
/>
}
{props.data.get("torrents") && props.data.get("torrents").toList().map(function(torrent) {
let key = `${props.data.get("season")}-${props.data.get("episode")}-${torrent.get("source")}-${torrent.get("quality")}`;
return (
<Torrent
data={torrent}
key={key}
addTorrent={props.addTorrent}
/>
)
})}
<DownloadButton
name={`${props.showName} - S${pad(props.data.get("season"))}E${pad(props.data.get("episode"))}`}
url={props.data.get("polochon_url")}
subtitles={props.data.get("subtitles")}
xs
/>
<GetDetailsButtonWithRouter
showName={props.showName}
data={props.data}
getEpisodeDetails={props.getEpisodeDetails}
/>
</div>
</div>
)
Episode.propTypes = {
data: PropTypes.instanceOf(Map),
season: PropTypes.number,
showName: PropTypes.string,
addToWishlist: PropTypes.func,
addTorrent: PropTypes.func,
refreshSubtitles: PropTypes.func,
getEpisodeDetails: PropTypes.func,
};
const Torrent = (props) => (
<button type="button"
className="btn btn-primary btn-sm"
onClick={() => props.addTorrent(props.data.get("url"))}
href={props.data.url} >
<i className="fa fa-download"></i> {props.data.get("quality")}
</button>
)
Torrent.propTypes = {
data: PropTypes.instanceOf(Map),
addTorrent: PropTypes.func,
};
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.deleteFromWishlist(imdbId);
} else {
props.addToWishlist(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),
addToWishlist: PropTypes.func,
deleteFromWishlist: PropTypes.func,
};
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.addToWishlist(imdbId, season, episode)}>
<i className="fa fa-bookmark"></i>
</span>
</OverlayTrigger>
);
}
TrackButton.propTypes = {
data: PropTypes.instanceOf(Map),
addToWishlist: PropTypes.func,
};
const GetDetailsButton = (props) => {
const imdbId = props.data.get("show_imdb_id");
const season = props.data.get("season");
const episode = props.data.get("episode");
const id = `${imdbId}-${season}-${episode}-refresh-dropdown`;
const handleFetchClick = () => {
if (props.data.get("fetching")) { return }
props.getEpisodeDetails(imdbId, season, episode);
}
const handleAdvanceTorrentSearchClick = () => {
const search = `${props.showName} S${pad(season)}E${pad(episode)}`;
const url = `/torrents/search/shows/${encodeURI(search)}`;
props.history.push(url);
}
return (
<SplitButton
drop="up"
variant="info"
title={<RefreshIndicator refresh={props.data.get("fetching")} />}
size="sm"
id={`refresh-${id}`}
onClick={handleFetchClick}>
<Dropdown.Item onClick={handleAdvanceTorrentSearchClick}>
<span>
<i className="fa fa-magnet"></i> Advanced torrent search
</span>
</Dropdown.Item>
</SplitButton>
);
}
GetDetailsButton.propTypes = {
data: PropTypes.instanceOf(Map).isRequired,
history: PropTypes.object.isRequired,
showName: PropTypes.string.isRequired,
getEpisodeDetails: PropTypes.func.isRequired,
};
const GetDetailsButtonWithRouter = withRouter(GetDetailsButton);