386 lines
12 KiB
JavaScript
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);
|