Refactor the code in reusable components and libs
This commit is contained in:
parent
88fc8be462
commit
26499533d3
@ -1,28 +1,24 @@
|
||||
import { configureAxios, request } from "../requests"
|
||||
|
||||
export function refreshSubtitles(type, id, season, episode) {
|
||||
switch (type) {
|
||||
case "movie":
|
||||
var resourceURL = `/movies/${id}`
|
||||
export const searchMovieSubtitles = (imdbId) => {
|
||||
return request(
|
||||
"MOVIE_SUBTITLES_UPDATE",
|
||||
configureAxios().post(`${resourceURL}/subtitles/refresh`),
|
||||
configureAxios().post(`/movies/${imdbId}/subtitles/refresh`),
|
||||
null,
|
||||
{ imdbId: id },
|
||||
{ imdbId: imdbId },
|
||||
)
|
||||
case "episode":
|
||||
var resourceURL = `/shows/${id}/seasons/${season}/episodes/${episode}`
|
||||
}
|
||||
|
||||
export const searchEpisodeSubtitles = (imdbId, season, episode) => {
|
||||
const url = `/shows/${imdbId}/seasons/${season}/episodes/${episode}`;
|
||||
return request(
|
||||
"EPISODE_SUBTITLES_UPDATE",
|
||||
configureAxios().post(`${resourceURL}/subtitles/refresh`),
|
||||
configureAxios().post(`${url}/subtitles/refresh`),
|
||||
null,
|
||||
{
|
||||
imdbId: id,
|
||||
imdbId: imdbId,
|
||||
season: season,
|
||||
episode: episode,
|
||||
},
|
||||
)
|
||||
default:
|
||||
console.warn("refreshSubtitles - Unknown type " + type)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ import MovieList from "./components/movies/list"
|
||||
import MoviesRoute from "./components/movies/route"
|
||||
import NavBar from "./components/navbar"
|
||||
import WsHandler from "./components/websocket"
|
||||
import ShowDetails from "./components/shows/details"
|
||||
import { ShowDetails } from "./components/shows/details"
|
||||
import ShowList from "./components/shows/list"
|
||||
import ShowsRoute from "./components/shows/route"
|
||||
import TorrentList from "./components/torrents/list"
|
||||
|
@ -1,70 +0,0 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
import Dropdown from "react-bootstrap/Dropdown"
|
||||
|
||||
import RefreshIndicator from "./refresh"
|
||||
|
||||
export const WishlistButton = (props) => {
|
||||
const handleClick = (e) => {
|
||||
e.preventDefault();
|
||||
if (props.wishlisted) {
|
||||
props.deleteFromWishlist(props.resourceId);
|
||||
} else {
|
||||
props.addToWishlist(props.resourceId);
|
||||
}
|
||||
}
|
||||
|
||||
if (props.wishlisted) {
|
||||
return (
|
||||
<Dropdown.Item onClick={handleClick}>
|
||||
<span>
|
||||
<i className="fa fa-bookmark"></i> Delete from wishlist
|
||||
</span>
|
||||
</Dropdown.Item>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Dropdown.Item onClick={handleClick}>
|
||||
<span>
|
||||
<i className="fa fa-bookmark-o"></i> Add to wishlist
|
||||
</span>
|
||||
</Dropdown.Item>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const DeleteButton = (props) => {
|
||||
const handleClick = () => {
|
||||
props.deleteFunc(props.resourceId, props.lastFetchUrl);
|
||||
}
|
||||
return (
|
||||
<Dropdown.Item onClick={handleClick}>
|
||||
<span>
|
||||
<i className="fa fa-trash"></i> Delete
|
||||
</span>
|
||||
</Dropdown.Item>
|
||||
);
|
||||
}
|
||||
DeleteButton.propTypes = {
|
||||
resourceId: PropTypes.string.isRequired,
|
||||
lastFetchUrl: PropTypes.string,
|
||||
deleteFunc: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const RefreshButton = (props) => {
|
||||
const handleClick = () => {
|
||||
if (props.fetching) { return; }
|
||||
props.getDetails(props.resourceId);
|
||||
}
|
||||
return (
|
||||
<Dropdown.Item onClick={handleClick}>
|
||||
<RefreshIndicator refresh={props.fetching} />
|
||||
</Dropdown.Item>
|
||||
);
|
||||
}
|
||||
RefreshButton.propTypes = {
|
||||
fetching: PropTypes.bool.isRequired,
|
||||
resourceId: PropTypes.string.isRequired,
|
||||
getDetails: PropTypes.func.isRequired,
|
||||
};
|
@ -8,10 +8,10 @@ export const DownloadAndStream = ({ url, name, subtitles }) => {
|
||||
if (!url || url === "") { return null; }
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<span>
|
||||
<DownloadButton url={url} />
|
||||
<StreamButton url={url} name={name} subtitles={subtitles} />
|
||||
</React.Fragment>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
DownloadAndStream.propTypes = {
|
||||
@ -58,9 +58,8 @@ StreamButton.propTypes = {
|
||||
subtitles: PropTypes.instanceOf(List),
|
||||
};
|
||||
|
||||
|
||||
const Player = ({ url, subtitles }) => {
|
||||
const hasSubtitles = !(subtitles === undefined || subtitles === null || subtitles.size === 0);
|
||||
const hasSubtitles = !(subtitles === null || subtitles.size === 0);
|
||||
return (
|
||||
<div className="embed-responsive embed-responsive-16by9">
|
||||
<video className="embed-responsive-item" controls>
|
||||
@ -82,3 +81,6 @@ Player.propTypes = {
|
||||
subtitles: PropTypes.instanceOf(List),
|
||||
url: PropTypes.string.isRequired,
|
||||
};
|
||||
Player.defaultProps = {
|
||||
subtitles: List(),
|
||||
};
|
||||
|
@ -1,29 +0,0 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
export const PolochonMetadata = (props) => {
|
||||
if (!props.quality || props.quality === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const badgeStyle = (props.light) ? "light" : "secondary";
|
||||
const className = `mx-1 badge badge-pill badge-${badgeStyle}`;
|
||||
return (
|
||||
<React.Fragment>
|
||||
<span className={className}>{props.quality}</span>
|
||||
<span className={className}>{props.container} </span>
|
||||
<span className={className}>{props.videoCodec}</span>
|
||||
<span className={className}>{props.audioCodec}</span>
|
||||
<span className={className}>{props.releaseGroup}</span>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
PolochonMetadata.propTypes = {
|
||||
light: PropTypes.bool,
|
||||
quality: PropTypes.string,
|
||||
container: PropTypes.string,
|
||||
videoCodec: PropTypes.string,
|
||||
audioCodec: PropTypes.string,
|
||||
releaseGroup: PropTypes.string,
|
||||
};
|
||||
|
@ -1,23 +0,0 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
const RefreshIndicator = (props) => {
|
||||
if (!props.refresh) {
|
||||
return (
|
||||
<span>
|
||||
<i className="fa fa-refresh"></i> Refresh
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<span>
|
||||
<i className="fa fa-spin fa-refresh"></i> Refreshing
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
RefreshIndicator.propTypes = {
|
||||
refresh: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default RefreshIndicator;
|
38
frontend/js/components/buttons/showMore.js
Normal file
38
frontend/js/components/buttons/showMore.js
Normal file
@ -0,0 +1,38 @@
|
||||
import React, { useState, useEffect } from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
export const ShowMore = ({ children, id, inLibrary }) => {
|
||||
const [ display, setDisplay ] = useState(!inLibrary)
|
||||
|
||||
useEffect(() => {
|
||||
setDisplay(!inLibrary);
|
||||
}, [id, inLibrary]);
|
||||
|
||||
if (!display) {
|
||||
return (
|
||||
<span>
|
||||
<a
|
||||
onClick={() => setDisplay(true)}
|
||||
className="badge badge-pill badge-info clickable"
|
||||
>
|
||||
More options ...
|
||||
</a>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return (<React.Fragment>{children}</React.Fragment>)
|
||||
}
|
||||
|
||||
ShowMore.propTypes = {
|
||||
id: PropTypes.string,
|
||||
inLibrary: PropTypes.bool.isRequired,
|
||||
children: PropTypes.oneOf(
|
||||
PropTypes.object,
|
||||
PropTypes.array,
|
||||
),
|
||||
}
|
||||
ShowMore.defaultProps = {
|
||||
id: "",
|
||||
inLibrary: false,
|
||||
}
|
@ -4,75 +4,57 @@ import { List } from "immutable"
|
||||
|
||||
import Dropdown from "react-bootstrap/Dropdown"
|
||||
|
||||
import RefreshIndicator from "./refresh"
|
||||
|
||||
const SubtitlesButton = (props) => {
|
||||
const subtitles = props.subtitles;
|
||||
const hasSubtitles = !(subtitles === undefined || subtitles === null || subtitles.size === 0);
|
||||
const size = props.xs ? "sm" : "";
|
||||
export const SubtitlesButton = ({
|
||||
subtitles,
|
||||
inLibrary,
|
||||
searching,
|
||||
search,
|
||||
}) => {
|
||||
if (inLibrary === false) { return null }
|
||||
|
||||
const count = (subtitles && subtitles.size !== 0) ? subtitles.size : 0;
|
||||
return (
|
||||
<span className="mr-1 mb-1">
|
||||
<Dropdown drop="up">
|
||||
<Dropdown.Toggle size={size} variant="success" id="movie-subtitles">
|
||||
<Dropdown.Toggle variant="success">
|
||||
<i className="fa fa-language mr-1" />
|
||||
Subtitles
|
||||
<span className="ml-1 badge badge-pill badge-light">
|
||||
{count}
|
||||
</span>
|
||||
</Dropdown.Toggle>
|
||||
|
||||
<Dropdown.Menu>
|
||||
<RefreshButton
|
||||
type={props.type}
|
||||
resourceID={props.resourceID}
|
||||
season={props.season}
|
||||
episode={props.episode}
|
||||
fetching={props.fetching ? true : false}
|
||||
refreshSubtitles={props.refreshSubtitles}
|
||||
/>
|
||||
{hasSubtitles &&
|
||||
<Dropdown.Divider />
|
||||
}
|
||||
{hasSubtitles && subtitles.toIndexedSeq().map(function(subtitle, index) {
|
||||
return (
|
||||
<Dropdown.Item key={index} href={subtitle.get("url")}>
|
||||
<i className="fa fa-download"></i> {subtitle.get("language").split("_")[1]}
|
||||
<Dropdown.Header>
|
||||
<span className="text-warning">Advanced</span>
|
||||
</Dropdown.Header>
|
||||
<Dropdown.Item onClick={search} >
|
||||
<i className={`fa ${ searching ? "fa-spin" : "" } fa-refresh mr-1`} />
|
||||
Automatic search
|
||||
</Dropdown.Item>
|
||||
);
|
||||
})}
|
||||
{count > 0 &&
|
||||
<React.Fragment>
|
||||
<Dropdown.Divider />
|
||||
<Dropdown.Header>
|
||||
<span className="text-warning">
|
||||
Available subtitles
|
||||
</span>
|
||||
</Dropdown.Header>
|
||||
</React.Fragment>
|
||||
}
|
||||
{count > 0 && subtitles.toIndexedSeq().map((subtitle, index) => (
|
||||
<Dropdown.Item href={subtitle.get("url")} key={index}>
|
||||
{subtitle.get("language").split("_")[1]}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
SubtitlesButton.propTypes = {
|
||||
subtitles: PropTypes.instanceOf(List),
|
||||
xs: PropTypes.bool,
|
||||
fetching: PropTypes.bool,
|
||||
refreshSubtitles: PropTypes.func.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
resourceID: PropTypes.string.isRequired,
|
||||
season: PropTypes.number,
|
||||
episode: PropTypes.number,
|
||||
}
|
||||
|
||||
export default SubtitlesButton;
|
||||
|
||||
const RefreshButton = (props) => {
|
||||
const handleClick = () => {
|
||||
if (props.fetching) {
|
||||
return
|
||||
}
|
||||
props.refreshSubtitles(props.type, props.resourceID,
|
||||
props.season, props.episode);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown.Item onClick={handleClick}>
|
||||
<RefreshIndicator refresh={props.fetching} />
|
||||
</Dropdown.Item>
|
||||
);
|
||||
}
|
||||
RefreshButton.propTypes = {
|
||||
fetching: PropTypes.bool,
|
||||
refreshSubtitles: PropTypes.func.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
resourceID: PropTypes.string.isRequired,
|
||||
season: PropTypes.number,
|
||||
episode: PropTypes.number,
|
||||
inLibrary: PropTypes.bool.isRequired,
|
||||
searching: PropTypes.bool.isRequired,
|
||||
search: PropTypes.func.isRequired,
|
||||
}
|
||||
|
117
frontend/js/components/buttons/torrents.js
Normal file
117
frontend/js/components/buttons/torrents.js
Normal file
@ -0,0 +1,117 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { List } from "immutable"
|
||||
import { connect } from "react-redux"
|
||||
|
||||
import { addTorrent } from "../../actions/torrents"
|
||||
|
||||
import Dropdown from "react-bootstrap/Dropdown"
|
||||
|
||||
function buildMenuItems(torrents) {
|
||||
if (!torrents || torrents.size === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const t = torrents.groupBy((el) => el.get("source"));
|
||||
|
||||
// Build the array of entries
|
||||
let entries = [];
|
||||
let dividerCount = t.size - 1;
|
||||
for (let [source, torrentList] of t.entrySeq()) {
|
||||
// Push the title
|
||||
entries.push({
|
||||
type: "header",
|
||||
value: source,
|
||||
});
|
||||
|
||||
// Push the torrents
|
||||
for (let torrent of torrentList) {
|
||||
entries.push({
|
||||
type: "entry",
|
||||
quality: torrent.get("quality"),
|
||||
url: torrent.get("url"),
|
||||
});
|
||||
}
|
||||
|
||||
// Push the divider
|
||||
if (dividerCount > 0) {
|
||||
dividerCount--;
|
||||
entries.push({ type: "divider" });
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
const torrentsButton = ({
|
||||
torrents,
|
||||
search,
|
||||
searching,
|
||||
addTorrent,
|
||||
url,
|
||||
}) => {
|
||||
const entries = buildMenuItems(torrents);
|
||||
const count = (torrents && torrents.size !== 0) ? torrents.size : 0;
|
||||
return (
|
||||
<span className="mr-1 mb-1">
|
||||
<Dropdown drop="up">
|
||||
<Dropdown.Toggle variant="primary">
|
||||
<i className="fa fa-magnet mr-1" />
|
||||
Torrents
|
||||
<span className="ml-1 badge badge-pill badge-light">
|
||||
{count}
|
||||
</span>
|
||||
</Dropdown.Toggle>
|
||||
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Header>
|
||||
<span className="text-warning">Advanced</span>
|
||||
</Dropdown.Header>
|
||||
<Dropdown.Item onClick={search} >
|
||||
<i className={`fa ${ searching ? "fa-spin" : "" } fa-refresh mr-1`} />
|
||||
Automatic search
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item href={url} >
|
||||
<i className="fa fa-search mr-1" />
|
||||
Manual search
|
||||
</Dropdown.Item>
|
||||
{entries.length > 0 &&
|
||||
<Dropdown.Divider />
|
||||
}
|
||||
{entries.map((e, index) => {
|
||||
switch (e.type) {
|
||||
case "header":
|
||||
return (
|
||||
<Dropdown.Header key={index}>
|
||||
<span className="text-warning">{e.value}</span>
|
||||
</Dropdown.Header>
|
||||
);
|
||||
case "divider":
|
||||
return (
|
||||
<Dropdown.Divider key={index}/>
|
||||
);
|
||||
case "entry":
|
||||
return (
|
||||
<Dropdown.Item key={index} onClick={() => addTorrent(e.url)}>
|
||||
{e.quality}
|
||||
</Dropdown.Item>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
torrentsButton.propTypes = {
|
||||
torrents: PropTypes.instanceOf(List),
|
||||
searching: PropTypes.bool,
|
||||
search: PropTypes.func.isRequired,
|
||||
addTorrent: PropTypes.func.isRequired,
|
||||
url: PropTypes.string,
|
||||
}
|
||||
torrentsButton.defaultProps = {
|
||||
torrents: List(),
|
||||
}
|
||||
|
||||
export const TorrentsButton = connect(null, {addTorrent})(torrentsButton);
|
21
frontend/js/components/details/genres.js
Normal file
21
frontend/js/components/details/genres.js
Normal file
@ -0,0 +1,21 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { List } from "immutable"
|
||||
|
||||
export const Genres = ({ genres }) => {
|
||||
if (genres.size === 0) { return null }
|
||||
|
||||
// Uppercase first genres
|
||||
const prettyGenres = genres.toJS().map(
|
||||
(word) => word[0].toUpperCase() + word.substr(1)
|
||||
).join(", ");
|
||||
|
||||
return (
|
||||
<span>
|
||||
<i className="fa fa-tags mr-1" />
|
||||
{prettyGenres}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Genres.propTypes = { genres: PropTypes.instanceOf(List) };
|
||||
Genres.defaultProps = { genres: List() };
|
12
frontend/js/components/details/plot.js
Normal file
12
frontend/js/components/details/plot.js
Normal file
@ -0,0 +1,12 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
export const Plot = ({ plot }) => {
|
||||
if (plot === "") { return null }
|
||||
|
||||
return (
|
||||
<span className="text text-break plot">{plot}</span>
|
||||
);
|
||||
}
|
||||
Plot.propTypes = { plot: PropTypes.string };
|
||||
Plot.defaultProps = { plot: "" };
|
38
frontend/js/components/details/polochon.js
Normal file
38
frontend/js/components/details/polochon.js
Normal file
@ -0,0 +1,38 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
export const PolochonMetadata = ({
|
||||
quality,
|
||||
container,
|
||||
videoCodec,
|
||||
audioCodec,
|
||||
releaseGroup,
|
||||
}) => {
|
||||
if (!quality || quality === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const metadata = [
|
||||
quality,
|
||||
container,
|
||||
videoCodec,
|
||||
audioCodec,
|
||||
releaseGroup,
|
||||
].
|
||||
filter(m => (m && m !== "")).
|
||||
join(", ")
|
||||
|
||||
return (
|
||||
<span>
|
||||
<i className="fa fa-file-video-o mr-1" />
|
||||
{metadata}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
PolochonMetadata.propTypes = {
|
||||
quality: PropTypes.string,
|
||||
container: PropTypes.string,
|
||||
videoCodec: PropTypes.string,
|
||||
audioCodec: PropTypes.string,
|
||||
releaseGroup: PropTypes.string,
|
||||
};
|
24
frontend/js/components/details/rating.js
Normal file
24
frontend/js/components/details/rating.js
Normal file
@ -0,0 +1,24 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
export const Rating = ({ rating, votes }) => {
|
||||
if (rating === 0) { return null; }
|
||||
|
||||
return (
|
||||
<span>
|
||||
<i className="fa fa-star-o mr-1"></i>
|
||||
{Number(rating).toFixed(1)}
|
||||
{votes !== 0 &&
|
||||
<small className="ml-1">({votes} votes)</small>
|
||||
}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Rating.propTypes = {
|
||||
rating: PropTypes.number,
|
||||
votes: PropTypes.number,
|
||||
};
|
||||
Rating.defaultProps = {
|
||||
rating: 0,
|
||||
votes: 0,
|
||||
};
|
47
frontend/js/components/details/releaseDate.js
Normal file
47
frontend/js/components/details/releaseDate.js
Normal file
@ -0,0 +1,47 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import moment from "moment"
|
||||
|
||||
const prettyDate = (input) => {
|
||||
switch (typeof input) {
|
||||
case "string":
|
||||
if (input === "") { return "" }
|
||||
break;
|
||||
case "number":
|
||||
if (input === 0) { return "" }
|
||||
return input
|
||||
default:
|
||||
return input;
|
||||
}
|
||||
|
||||
const date = moment(input);
|
||||
if (!date.isValid()) { return "" }
|
||||
|
||||
let output = date.format("DD/MM/YYYY");
|
||||
|
||||
if ((date > moment().subtract(1, "month"))
|
||||
&& (date < moment().add(1, "month"))) {
|
||||
output += " (" + date.fromNow() + ")"
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
export const ReleaseDate = ({ date }) => {
|
||||
const formattedDate = prettyDate(date);
|
||||
if (formattedDate === "") { return null }
|
||||
|
||||
return (
|
||||
<span>
|
||||
<i className="fa fa-calendar mr-1" />
|
||||
{formattedDate}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
ReleaseDate.propTypes = {
|
||||
date: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]),
|
||||
};
|
||||
ReleaseDate.defaultProps = { date: "" };
|
17
frontend/js/components/details/runtime.js
Normal file
17
frontend/js/components/details/runtime.js
Normal file
@ -0,0 +1,17 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
import { prettyDurationFromMinutes } from "../../utils"
|
||||
|
||||
export const Runtime = ({ runtime }) => {
|
||||
if (runtime === 0) { return null; }
|
||||
|
||||
return (
|
||||
<span>
|
||||
<i className="fa fa-clock-o"></i>
|
||||
{prettyDurationFromMinutes(runtime)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Runtime.propTypes = { runtime: PropTypes.number };
|
||||
Runtime.defaultProps = { runtime: 0 };
|
25
frontend/js/components/details/title.js
Normal file
25
frontend/js/components/details/title.js
Normal file
@ -0,0 +1,25 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
export const Title = ({ title, xs }) => {
|
||||
if (title === "") { return null; }
|
||||
|
||||
if (xs) {
|
||||
return (<h5>{title}</h5>)
|
||||
} else {
|
||||
return (
|
||||
<span>
|
||||
<h2 className="d-none d-sm-block">{title}</h2>
|
||||
<h4 className="d-block d-sm-none">{title}</h4>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
Title.propTypes = {
|
||||
title: PropTypes.string,
|
||||
xs: PropTypes.bool,
|
||||
};
|
||||
Title.defaultProps = {
|
||||
title: "",
|
||||
xs: false,
|
||||
};
|
@ -1,30 +1,35 @@
|
||||
import React from "react"
|
||||
import { Map, List } from "immutable"
|
||||
import PropTypes from "prop-types"
|
||||
import { Map } from "immutable"
|
||||
|
||||
import { PolochonMetadata } from "../buttons/polochon"
|
||||
import { DownloadAndStream } from "../buttons/download"
|
||||
import { ImdbBadge } from "../buttons/imdb"
|
||||
|
||||
import { Genres } from "../details/genres"
|
||||
import { Plot } from "../details/plot"
|
||||
import { PolochonMetadata } from "../details/polochon"
|
||||
import { Rating } from "../details/rating"
|
||||
import { ReleaseDate } from "../details/releaseDate"
|
||||
import { Runtime } from "../details/runtime"
|
||||
import { Title } from "../details/title"
|
||||
|
||||
const ListDetails = (props) => {
|
||||
if (props.loading) { return null }
|
||||
if (props.data === undefined) { return null }
|
||||
if (props.loading) { return null }
|
||||
|
||||
return (
|
||||
<div className="col-8 col-md-4 list-details pl-1 d-flex align-items-start flex-column">
|
||||
<div className="video-details flex-fill d-flex flex-column">
|
||||
<h2 className="d-none d-sm-block">{props.data.get("title")}</h2>
|
||||
<h4 className="d-block d-sm-none">{props.data.get("title")}</h4>
|
||||
<Title title={props.data.get("title")} />
|
||||
<TrackingLabel
|
||||
wishlisted={props.data.get("wishlisted")}
|
||||
trackedSeason={props.data.get("tracked_season")}
|
||||
trackedEpisode={props.data.get("tracked_episode")}
|
||||
/>
|
||||
<h4 className="d-none d-sm-block">{props.data.get("year")}</h4>
|
||||
<h5 className="d-block d-sm-none">{props.data.get("year")}</h5>
|
||||
<ReleaseDate date={props.data.get("year")} />
|
||||
<Runtime runtime={props.data.get("runtime")} />
|
||||
<Genres genres={props.data.get("genres")} />
|
||||
<Ratings
|
||||
<Rating
|
||||
rating={props.data.get("rating")}
|
||||
votes={props.data.get("votes")}
|
||||
/>
|
||||
@ -36,7 +41,6 @@ const ListDetails = (props) => {
|
||||
subtitles={props.data.get("subtitles")}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<PolochonMetadata
|
||||
quality={props.data.get("quality")}
|
||||
releaseGroup={props.data.get("release_group")}
|
||||
@ -44,10 +48,7 @@ const ListDetails = (props) => {
|
||||
audioCodec={props.data.get("audio_codec")}
|
||||
videoCodec={props.data.get("video_codec")}
|
||||
/>
|
||||
</div>
|
||||
<p className="text text-break plot">{props.data.get("plot")}</p>
|
||||
</div>
|
||||
<div className="pb-1 align-self-end">
|
||||
<Plot plot={props.data.get("plot")} />
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
@ -60,48 +61,6 @@ ListDetails.propTypes = {
|
||||
};
|
||||
export default ListDetails;
|
||||
|
||||
const Runtime = (props) => {
|
||||
if (props.runtime === undefined || props.runtime === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hours = Math.floor(props.runtime / 60);
|
||||
const minutes = (props.runtime % 60);
|
||||
|
||||
let duration = "";
|
||||
if (hours > 0) { duration += hours + "h" }
|
||||
if (minutes > 0) { duration += ("0" + minutes).slice(-2) }
|
||||
if (hours === 0) { duration += " min" }
|
||||
|
||||
return (
|
||||
<p>
|
||||
<i className="fa fa-clock-o"></i>
|
||||
{duration}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
Runtime.propTypes = { runtime: PropTypes.number };
|
||||
|
||||
const Ratings = (props) => {
|
||||
if (props.rating === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<p>
|
||||
<i className="fa fa-star-o"></i>
|
||||
{Number(props.rating).toFixed(1)}
|
||||
{props.votes !== undefined &&
|
||||
<small>({props.votes} counts)</small>
|
||||
}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
Ratings.propTypes = {
|
||||
rating: PropTypes.number,
|
||||
votes: PropTypes.number,
|
||||
};
|
||||
|
||||
const TrackingLabel = (props) => {
|
||||
let wishlistStr = props.wishlisted ? "Wishlisted" : "";
|
||||
|
||||
@ -131,24 +90,3 @@ TrackingLabel.propTypes = {
|
||||
trackedSeason: PropTypes.number,
|
||||
trackedEpisode: PropTypes.number,
|
||||
};
|
||||
|
||||
const Genres = (props) => {
|
||||
if ((props.genres === undefined) || (props.genres.size === 0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Uppercase first genres
|
||||
const prettyGenres = props.genres.toJS().map(
|
||||
(word) => word[0].toUpperCase() + word.substr(1)
|
||||
).join(", ");
|
||||
|
||||
return (
|
||||
<p>
|
||||
<i className="fa fa-tags"></i>
|
||||
{prettyGenres}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
Genres.propTypes = {
|
||||
genres: PropTypes.instanceOf(List),
|
||||
};
|
||||
|
@ -1,20 +1,20 @@
|
||||
import React, { useState, useEffect } from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
const ListFilter = (props) => {
|
||||
const ListFilter = ({ placeHolder, updateFilter }) => {
|
||||
const [filter, setFilter] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
// Start filtering at 3 chars
|
||||
if (filter.length >= 3) {
|
||||
props.updateFilter(filter);
|
||||
updateFilter(filter);
|
||||
}
|
||||
}, [filter]);
|
||||
}, [filter, updateFilter]);
|
||||
|
||||
return (
|
||||
<div className="input-group input-group-sm">
|
||||
<input type="text" className="form-control"
|
||||
placeholder={props.placeHolder}
|
||||
placeholder={placeHolder}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
value={filter} />
|
||||
<div className="input-group-append d-none d-md-block">
|
||||
|
@ -1,25 +1,24 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { Map } from "immutable"
|
||||
|
||||
import EmptyImg from "../../../img/noimage.png"
|
||||
|
||||
const Poster = (props) => {
|
||||
const className = props.selected ? "border-primary thumbnail-selected" : "border-secondary";
|
||||
const src = (props.data.get("poster_url") === "") ? EmptyImg : props.data.get("poster_url");
|
||||
const Poster = ({ url, selected, onClick, onDoubleClick }) => {
|
||||
const className = selected ? "border-primary thumbnail-selected" : "border-secondary";
|
||||
const src = (url === "") ? EmptyImg : url;
|
||||
|
||||
return (
|
||||
<img
|
||||
src={src}
|
||||
onClick={props.onClick}
|
||||
onDoubleClick={props.onDoubleClick}
|
||||
onClick={onClick}
|
||||
onDoubleClick={onDoubleClick}
|
||||
className={`my-1 m-md-2 img-thumbnail object-fit-cover ${className}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Poster.propTypes = {
|
||||
data: PropTypes.instanceOf(Map),
|
||||
selected: PropTypes.bool,
|
||||
url: PropTypes.string,
|
||||
selected: PropTypes.bool.isRequired,
|
||||
onClick: PropTypes.func,
|
||||
onDoubleClick: PropTypes.func,
|
||||
};
|
||||
|
@ -201,8 +201,8 @@ const Posters = (props) => {
|
||||
|
||||
return (
|
||||
<Poster
|
||||
data={el}
|
||||
key={`poster-${imdbId}-${index}`}
|
||||
url={el.get("poster_url")}
|
||||
key={index}
|
||||
selected={selected}
|
||||
onClick={() => props.selectPoster(imdbId)}
|
||||
onDoubleClick={() => props.onDoubleClick(imdbId)}
|
||||
|
@ -1,47 +0,0 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
import { WishlistButton, DeleteButton, RefreshButton } from "../buttons/actions"
|
||||
import Dropdown from "react-bootstrap/Dropdown"
|
||||
|
||||
const ActionsButton = (props) => (
|
||||
<Dropdown drop="up">
|
||||
<Dropdown.Toggle variant="secondary" id="movie-button-actions">
|
||||
Actions
|
||||
</Dropdown.Toggle>
|
||||
|
||||
<Dropdown.Menu>
|
||||
<RefreshButton
|
||||
fetching={props.fetching}
|
||||
resourceId={props.movieId}
|
||||
getDetails={props.getDetails}
|
||||
/>
|
||||
{props.hasMovie &&
|
||||
<DeleteButton
|
||||
resourceId={props.movieId}
|
||||
lastFetchUrl={props.lastFetchUrl}
|
||||
deleteFunc={props.deleteMovie}
|
||||
/>
|
||||
}
|
||||
<WishlistButton
|
||||
resourceId={props.movieId}
|
||||
wishlisted={props.wishlisted}
|
||||
addToWishlist={props.addToWishlist}
|
||||
deleteFromWishlist={props.deleteFromWishlist}
|
||||
/>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
);
|
||||
ActionsButton.propTypes = {
|
||||
hasMovie: PropTypes.bool.isRequired,
|
||||
fetching: PropTypes.bool.isRequired,
|
||||
wishlisted: PropTypes.bool.isRequired,
|
||||
movieId: PropTypes.string.isRequired,
|
||||
getDetails: PropTypes.func.isRequired,
|
||||
addToWishlist: PropTypes.func.isRequired,
|
||||
deleteFromWishlist: PropTypes.func.isRequired,
|
||||
deleteMovie: PropTypes.func.isRequired,
|
||||
lastFetchUrl: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
export default ActionsButton;
|
@ -1,54 +0,0 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { Map } from "immutable"
|
||||
|
||||
import SubtitlesButton from "../buttons/subtitles"
|
||||
import TorrentsButton from "./torrents"
|
||||
import ActionsButton from "./actions"
|
||||
|
||||
import ButtonToolbar from "react-bootstrap/ButtonToolbar"
|
||||
|
||||
const MovieButtons = (props) => (
|
||||
<ButtonToolbar>
|
||||
<ActionsButton
|
||||
fetching={props.movie.get("fetchingDetails")}
|
||||
movieId={props.movie.get("imdb_id")}
|
||||
getDetails={props.getMovieDetails}
|
||||
deleteMovie={props.deleteMovie}
|
||||
hasMovie={(props.movie.get("polochon_url") !== "")}
|
||||
wishlisted={props.movie.get("wishlisted")}
|
||||
addToWishlist={props.addToWishlist}
|
||||
deleteFromWishlist={props.deleteFromWishlist}
|
||||
lastFetchUrl={props.lastFetchUrl}
|
||||
/>
|
||||
|
||||
<TorrentsButton
|
||||
movieTitle={props.movie.get("title")}
|
||||
torrents={props.movie.get("torrents")}
|
||||
addTorrent={props.addTorrent}
|
||||
/>
|
||||
|
||||
{props.movie.get("polochon_url") !== "" &&
|
||||
<SubtitlesButton
|
||||
fetching={props.movie.get("fetchingSubtitles")}
|
||||
subtitles={props.movie.get("subtitles")}
|
||||
refreshSubtitles={props.refreshSubtitles}
|
||||
resourceID={props.movie.get("imdb_id")}
|
||||
type="movie"
|
||||
/>
|
||||
}
|
||||
</ButtonToolbar>
|
||||
);
|
||||
MovieButtons.propTypes = {
|
||||
movie: PropTypes.instanceOf(Map),
|
||||
refreshSubtitles: PropTypes.func.isRequired,
|
||||
addTorrent: PropTypes.func.isRequired,
|
||||
getDetails: PropTypes.func,
|
||||
deleteMovie: PropTypes.func.isRequired,
|
||||
addToWishlist: PropTypes.func.isRequired,
|
||||
deleteFromWishlist: PropTypes.func.isRequired,
|
||||
getMovieDetails: PropTypes.func.isRequired,
|
||||
lastFetchUrl: PropTypes.string,
|
||||
}
|
||||
|
||||
export default MovieButtons;
|
@ -2,16 +2,17 @@ import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { OrderedMap, Map } from "immutable"
|
||||
import { connect } from "react-redux"
|
||||
import { addTorrent } from "../../actions/torrents"
|
||||
import { refreshSubtitles } from "../../actions/subtitles"
|
||||
import { addMovieToWishlist, deleteMovie, deleteMovieFromWishlist,
|
||||
getMovieDetails, selectMovie, updateFilter } from "../../actions/movies"
|
||||
import { selectMovie, updateFilter } from "../../actions/movies"
|
||||
|
||||
import ListPosters from "../list/posters"
|
||||
import ListDetails from "../list/details"
|
||||
import MovieButtons from "./buttons"
|
||||
import ListPosters from "../list/posters"
|
||||
|
||||
import Row from "react-bootstrap/Row"
|
||||
import { inLibrary } from "../../utils"
|
||||
|
||||
import { ShowMore } from "../buttons/showMore"
|
||||
|
||||
import { MovieSubtitlesButton } from "./subtitlesButton"
|
||||
import { MovieTorrentsButton } from "./torrentsButton"
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
@ -19,25 +20,22 @@ function mapStateToProps(state) {
|
||||
movies : state.movieStore.get("movies"),
|
||||
filter : state.movieStore.get("filter"),
|
||||
selectedImdbId : state.movieStore.get("selectedImdbId"),
|
||||
lastFetchUrl : state.movieStore.get("lastFetchUrl"),
|
||||
exploreOptions : state.movieStore.get("exploreOptions"),
|
||||
};
|
||||
}
|
||||
const mapDispatchToProps = {
|
||||
selectMovie, getMovieDetails, addTorrent,
|
||||
addMovieToWishlist, deleteMovie, deleteMovieFromWishlist,
|
||||
refreshSubtitles, updateFilter,
|
||||
selectMovie, updateFilter,
|
||||
};
|
||||
|
||||
const MovieList = (props) => {
|
||||
let selectedMovie = undefined;
|
||||
let selectedMovie = Map();
|
||||
if (props.movies !== undefined &&
|
||||
props.movies.has(props.selectedImdbId)) {
|
||||
selectedMovie = props.movies.get(props.selectedImdbId);
|
||||
}
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<div className="row">
|
||||
<ListPosters
|
||||
data={props.movies}
|
||||
type="movies"
|
||||
@ -53,18 +51,25 @@ const MovieList = (props) => {
|
||||
loading={props.loading}
|
||||
/>
|
||||
<ListDetails data={selectedMovie} loading={props.loading}>
|
||||
<MovieButtons
|
||||
movie={selectedMovie}
|
||||
getMovieDetails={props.getMovieDetails}
|
||||
addTorrent={props.addTorrent}
|
||||
deleteMovie={props.deleteMovie}
|
||||
addToWishlist={props.addMovieToWishlist}
|
||||
deleteFromWishlist={props.deleteMovieFromWishlist}
|
||||
lastFetchUrl={props.lastFetchUrl}
|
||||
refreshSubtitles={props.refreshSubtitles}
|
||||
<ShowMore
|
||||
id={selectedMovie.get("imdb_id")}
|
||||
inLibrary={inLibrary(selectedMovie)}
|
||||
>
|
||||
<MovieTorrentsButton
|
||||
torrents={selectedMovie.get("torrents")}
|
||||
imdbId={selectedMovie.get("imdb_id")}
|
||||
title={selectedMovie.get("title")}
|
||||
searching={selectedMovie.get("fetchingDetails", false)}
|
||||
/>
|
||||
<MovieSubtitlesButton
|
||||
subtitles={selectedMovie.get("subtitles")}
|
||||
inLibrary={inLibrary(selectedMovie)}
|
||||
imdbId={selectedMovie.get("imdb_id")}
|
||||
searching={selectedMovie.get("fetchingSubtitles", false)}
|
||||
/>
|
||||
</ShowMore>
|
||||
</ListDetails>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
MovieList.propTypes = {
|
||||
@ -73,15 +78,8 @@ MovieList.propTypes = {
|
||||
selectedImdbId: PropTypes.string,
|
||||
filter: PropTypes.string,
|
||||
loading: PropTypes.bool,
|
||||
lastFetchUrl: PropTypes.string,
|
||||
updateFilter: PropTypes.func,
|
||||
selectMovie: PropTypes.func,
|
||||
addMovieToWishlist: PropTypes.func,
|
||||
deleteMovieFromWishlist: PropTypes.func,
|
||||
deleteMovie: PropTypes.func,
|
||||
addTorrent: PropTypes.func,
|
||||
refreshSubtitles: PropTypes.func,
|
||||
getMovieDetails: PropTypes.func,
|
||||
match: PropTypes.object,
|
||||
};
|
||||
|
||||
|
33
frontend/js/components/movies/subtitlesButton.js
Normal file
33
frontend/js/components/movies/subtitlesButton.js
Normal file
@ -0,0 +1,33 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { List } from "immutable"
|
||||
import { connect } from "react-redux"
|
||||
|
||||
import { searchMovieSubtitles } from "../../actions/subtitles"
|
||||
|
||||
import { SubtitlesButton } from "../buttons/subtitles"
|
||||
|
||||
const movieSubtitlesButton = ({
|
||||
inLibrary,
|
||||
imdbId,
|
||||
searching,
|
||||
searchMovieSubtitles,
|
||||
subtitles,
|
||||
}) => (
|
||||
<SubtitlesButton
|
||||
inLibrary={inLibrary}
|
||||
searching={searching}
|
||||
subtitles={subtitles}
|
||||
search={() => searchMovieSubtitles(imdbId)}
|
||||
/>
|
||||
)
|
||||
|
||||
movieSubtitlesButton.propTypes = {
|
||||
searching: PropTypes.bool,
|
||||
inLibrary: PropTypes.bool,
|
||||
imdbId: PropTypes.string,
|
||||
searchMovieSubtitles: PropTypes.func,
|
||||
subtitles: PropTypes.instanceOf(List),
|
||||
}
|
||||
|
||||
export const MovieSubtitlesButton = connect(null, {searchMovieSubtitles})(movieSubtitlesButton);
|
@ -1,92 +0,0 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { List } from "immutable"
|
||||
|
||||
import Dropdown from "react-bootstrap/Dropdown"
|
||||
|
||||
function buildMenuItems(torrents) {
|
||||
if (!torrents || torrents.size === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const t = torrents.groupBy((el) => el.get("source"));
|
||||
|
||||
// Build the array of entries
|
||||
let entries = [];
|
||||
let dividerCount = t.size - 1;
|
||||
for (let [source, torrentList] of t.entrySeq()) {
|
||||
// Push the title
|
||||
entries.push({
|
||||
type: "header",
|
||||
value: source,
|
||||
});
|
||||
|
||||
// Push the torrents
|
||||
for (let torrent of torrentList) {
|
||||
entries.push({
|
||||
type: "entry",
|
||||
quality: torrent.get("quality"),
|
||||
url: torrent.get("url"),
|
||||
});
|
||||
}
|
||||
|
||||
// Push the divider
|
||||
if (dividerCount > 0) {
|
||||
dividerCount--;
|
||||
entries.push({ type: "divider" });
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
const TorrentsButton = (props) => {
|
||||
const entries = buildMenuItems(props.torrents);
|
||||
const searchUrl = `#/torrents/search/movies/${encodeURI(props.movieTitle)}`;
|
||||
return (
|
||||
<Dropdown drop="up">
|
||||
<Dropdown.Toggle variant="secondary" id="movie-torrents">
|
||||
Torrents
|
||||
</Dropdown.Toggle>
|
||||
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Header>
|
||||
<span className="text-warning">Advanced</span>
|
||||
</Dropdown.Header>
|
||||
<Dropdown.Item href={searchUrl} >
|
||||
<i className="fa fa-search" aria-hidden="true"></i> Search
|
||||
</Dropdown.Item>
|
||||
{entries.length > 0 &&
|
||||
<Dropdown.Divider />
|
||||
}
|
||||
{entries.map((e, index) => {
|
||||
switch (e.type) {
|
||||
case "header":
|
||||
return (
|
||||
<Dropdown.Header key={index}>
|
||||
<span className="text-warning">{e.value}</span>
|
||||
</Dropdown.Header>
|
||||
);
|
||||
case "divider":
|
||||
return (
|
||||
<Dropdown.Divider key={index}/>
|
||||
);
|
||||
case "entry":
|
||||
return (
|
||||
<Dropdown.Item key={index} onClick={() => props.addTorrent(e.url)}>
|
||||
{e.quality}
|
||||
</Dropdown.Item>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
TorrentsButton.propTypes = {
|
||||
torrents: PropTypes.instanceOf(List),
|
||||
addTorrent: PropTypes.func.isRequired,
|
||||
movieTitle: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
export default TorrentsButton;
|
31
frontend/js/components/movies/torrentsButton.js
Normal file
31
frontend/js/components/movies/torrentsButton.js
Normal file
@ -0,0 +1,31 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { List } from "immutable"
|
||||
import { connect } from "react-redux"
|
||||
|
||||
import { getMovieDetails } from "../../actions/movies"
|
||||
import { TorrentsButton } from "../buttons/torrents"
|
||||
|
||||
const movieTorrentsButton = ({
|
||||
torrents,
|
||||
imdbId,
|
||||
title,
|
||||
searching,
|
||||
getMovieDetails,
|
||||
}) => (
|
||||
<TorrentsButton
|
||||
torrents={torrents}
|
||||
searching={searching}
|
||||
search={() => getMovieDetails(imdbId)}
|
||||
url={`#/torrents/search/movies/${encodeURI(title)}`}
|
||||
/>
|
||||
)
|
||||
movieTorrentsButton.propTypes = {
|
||||
torrents: PropTypes.instanceOf(List),
|
||||
imdbId: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
searching: PropTypes.bool,
|
||||
getMovieDetails: PropTypes.func,
|
||||
};
|
||||
|
||||
export const MovieTorrentsButton = connect(null, {getMovieDetails})(movieTorrentsButton);
|
@ -1,369 +1,36 @@
|
||||
import React, { useState } from "react"
|
||||
import React 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 { DownloadAndStream } from "../buttons/download"
|
||||
import { PolochonMetadata } from "../buttons/polochon"
|
||||
import { ImdbBadge } from "../buttons/imdb"
|
||||
|
||||
import Loader from "../loader/loader"
|
||||
import SubtitlesButton from "../buttons/subtitles"
|
||||
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"
|
||||
import { Fanart } from "./details/fanart"
|
||||
import { Header } from "./details/header"
|
||||
import { SeasonsList } from "./details/seasons"
|
||||
|
||||
// Helper to change 1 to 01
|
||||
const pad = (d) => (d < 10) ? "0" + d.toString() : d.toString();
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
const mapStateToProps = (state) => ({
|
||||
loading: state.showStore.get("loading"),
|
||||
show: state.showStore.get("show"),
|
||||
};
|
||||
}
|
||||
const mapDispatchToProps = {
|
||||
addTorrent, addShowToWishlist, deleteShowFromWishlist,
|
||||
fetchShowDetails, getEpisodeDetails, refreshSubtitles,
|
||||
};
|
||||
})
|
||||
|
||||
const ShowDetails = (props) => {
|
||||
if (props.loading) {
|
||||
const showDetails = ({ show, loading }) => {
|
||||
if (loading === true) {
|
||||
return (<Loader />);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Fanart url={props.show.get("fanart_url")} />
|
||||
<Fanart url={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}
|
||||
/>
|
||||
<Header data={show} />
|
||||
<SeasonsList data={show} />
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
ShowDetails.propTypes = {
|
||||
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>
|
||||
<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>
|
||||
<div className="my-1">
|
||||
<ImdbBadge imdbId={props.data.get("imdb_id")} />
|
||||
</div>
|
||||
<p className="card-text plot">{props.data.get("plot")}</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 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>
|
||||
<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")}
|
||||
light
|
||||
/>
|
||||
<DownloadAndStream
|
||||
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")}
|
||||
/>
|
||||
</span>
|
||||
<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}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
<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);
|
||||
export const ShowDetails = connect(mapStateToProps)(showDetails);
|
||||
|
71
frontend/js/components/shows/details/episode.js
Normal file
71
frontend/js/components/shows/details/episode.js
Normal file
@ -0,0 +1,71 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { Map } from "immutable"
|
||||
|
||||
import { inLibrary, prettyEpisodeName } from "../../../utils"
|
||||
|
||||
import { Plot } from "../../details/plot"
|
||||
import { PolochonMetadata } from "../../details/polochon"
|
||||
import { ReleaseDate } from "../../details/releaseDate"
|
||||
import { Runtime } from "../../details/runtime"
|
||||
import { Title } from "../../details/title"
|
||||
|
||||
import { DownloadAndStream } from "../../buttons/download"
|
||||
import { ShowMore } from "../../buttons/showMore"
|
||||
|
||||
import { EpisodeSubtitlesButton } from "./subtitlesButton"
|
||||
import { EpisodeThumb } from "./episodeThumb"
|
||||
import { EpisodeTorrentsButton } from "./torrentsButton"
|
||||
|
||||
export 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
|
||||
/>
|
||||
<ReleaseDate date={props.data.get("aired")} />
|
||||
<Runtime runtime={props.data.get("runtime")} />
|
||||
<Plot plot={props.data.get("plot")} />
|
||||
<DownloadAndStream
|
||||
name={prettyEpisodeName(props.showName, props.data.get("season"), props.data.get("episode"))}
|
||||
url={props.data.get("polochon_url")}
|
||||
subtitles={props.data.get("subtitles")}
|
||||
/>
|
||||
<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")}
|
||||
light
|
||||
/>
|
||||
<ShowMore
|
||||
id={prettyEpisodeName(props.showName, props.data.get("season"), props.data.get("episode"))}
|
||||
inLibrary={inLibrary(props.data)}
|
||||
>
|
||||
<EpisodeTorrentsButton
|
||||
torrents={props.data.get("torrents")}
|
||||
showName={props.showName}
|
||||
imdbId={props.data.get("show_imdb_id")}
|
||||
season={props.data.get("season")}
|
||||
episode={props.data.get("episode")}
|
||||
searching={props.data.get("fetching")}
|
||||
/>
|
||||
<EpisodeSubtitlesButton
|
||||
subtitles={props.data.get("subtitles")}
|
||||
inLibrary={inLibrary(props.data)}
|
||||
searching={props.data.get("fetchingSubtitles", false)}
|
||||
imdbId={props.data.get("show_imdb_id")}
|
||||
season={props.data.get("season")}
|
||||
episode={props.data.get("episode")}
|
||||
/>
|
||||
</ShowMore>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
Episode.propTypes = {
|
||||
data: PropTypes.instanceOf(Map).isRequired,
|
||||
showName: PropTypes.string.isRequired,
|
||||
};
|
13
frontend/js/components/shows/details/episodeThumb.js
Normal file
13
frontend/js/components/shows/details/episodeThumb.js
Normal file
@ -0,0 +1,13 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
export const EpisodeThumb = ({ url }) => {
|
||||
if (url === "") { return null }
|
||||
return (
|
||||
<div className="mr-0 mr-lg-2 mb-2 mb-lg-0 episode-thumb">
|
||||
<img src={url} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
EpisodeThumb.propTypes = { url: PropTypes.string };
|
||||
EpisodeThumb.defaultProps = { url: "" };
|
14
frontend/js/components/shows/details/fanart.js
Normal file
14
frontend/js/components/shows/details/fanart.js
Normal file
@ -0,0 +1,14 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
export const Fanart = ({ url }) => (
|
||||
<div className="show-fanart mx-n3 mt-n1">
|
||||
<img
|
||||
className="img w-100 object-fit-cover object-position-top mh-40vh"
|
||||
src={url}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
Fanart.propTypes = {
|
||||
url: PropTypes.string,
|
||||
}
|
51
frontend/js/components/shows/details/header.js
Normal file
51
frontend/js/components/shows/details/header.js
Normal file
@ -0,0 +1,51 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { Map } from "immutable"
|
||||
|
||||
import { TrackHeader } from "./track"
|
||||
import { ImdbBadge } from "../../buttons/imdb"
|
||||
import { Plot } from "../../details/plot"
|
||||
import { Rating } from "../../details/rating"
|
||||
import { ReleaseDate } from "../../details/releaseDate"
|
||||
|
||||
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">
|
||||
<img
|
||||
className="overflow-hidden object-fit-cover"
|
||||
src={props.data.get("poster_url")}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="card-body">
|
||||
<h5 className="card-title">{props.data.get("title")}</h5>
|
||||
<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">
|
||||
<ImdbBadge imdbId={props.data.get("imdb_id")} />
|
||||
</span>
|
||||
<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 = {
|
||||
data: PropTypes.instanceOf(Map),
|
||||
deleteFromWishlist: PropTypes.func,
|
||||
addToWishlist: PropTypes.func,
|
||||
};
|
48
frontend/js/components/shows/details/season.js
Normal file
48
frontend/js/components/shows/details/season.js
Normal file
@ -0,0 +1,48 @@
|
||||
import React, { useState } from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { Map } from "immutable"
|
||||
|
||||
import { Episode } from "./episode"
|
||||
|
||||
export 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,
|
||||
};
|
31
frontend/js/components/shows/details/seasons.js
Normal file
31
frontend/js/components/shows/details/seasons.js
Normal file
@ -0,0 +1,31 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { Map } from "immutable"
|
||||
|
||||
import { Season } from "./season"
|
||||
|
||||
export 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,
|
||||
};
|
37
frontend/js/components/shows/details/subtitlesButton.js
Normal file
37
frontend/js/components/shows/details/subtitlesButton.js
Normal file
@ -0,0 +1,37 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { List } from "immutable"
|
||||
import { connect } from "react-redux"
|
||||
|
||||
import { searchEpisodeSubtitles } from "../../../actions/subtitles"
|
||||
|
||||
import { SubtitlesButton } from "../../buttons/subtitles"
|
||||
|
||||
const episodeSubtitlesButton = ({
|
||||
inLibrary,
|
||||
imdbId,
|
||||
season,
|
||||
episode,
|
||||
searching,
|
||||
searchEpisodeSubtitles,
|
||||
subtitles,
|
||||
}) => (
|
||||
<SubtitlesButton
|
||||
subtitles={subtitles}
|
||||
inLibrary={inLibrary}
|
||||
searching={searching}
|
||||
search={() => searchEpisodeSubtitles(imdbId, season, episode)}
|
||||
/>
|
||||
)
|
||||
|
||||
episodeSubtitlesButton.propTypes = {
|
||||
inLibrary: PropTypes.bool,
|
||||
searching: PropTypes.bool,
|
||||
imdbId: PropTypes.string,
|
||||
season: PropTypes.number,
|
||||
episode: PropTypes.number,
|
||||
searchEpisodeSubtitles: PropTypes.func,
|
||||
subtitles: PropTypes.instanceOf(List),
|
||||
}
|
||||
|
||||
export const EpisodeSubtitlesButton = connect(null, {searchEpisodeSubtitles})(episodeSubtitlesButton);
|
38
frontend/js/components/shows/details/torrentsButton.js
Normal file
38
frontend/js/components/shows/details/torrentsButton.js
Normal file
@ -0,0 +1,38 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { connect } from "react-redux"
|
||||
import { List } from "immutable"
|
||||
|
||||
import { getEpisodeDetails } from "../../../actions/shows"
|
||||
import { TorrentsButton } from "../../buttons/torrents"
|
||||
import { prettyEpisodeName } from "../../../utils"
|
||||
|
||||
const episodeTorrentsButton = ({
|
||||
torrents,
|
||||
imdbId,
|
||||
season,
|
||||
episode,
|
||||
showName,
|
||||
searching,
|
||||
getEpisodeDetails,
|
||||
}) => (
|
||||
<TorrentsButton
|
||||
torrents={torrents}
|
||||
searching={searching}
|
||||
search={() => getEpisodeDetails(imdbId, season, episode)}
|
||||
url={`#/torrents/search/shows/${
|
||||
encodeURI(prettyEpisodeName(showName, season, episode))
|
||||
}`}
|
||||
/>
|
||||
)
|
||||
episodeTorrentsButton.propTypes = {
|
||||
torrents: PropTypes.instanceOf(List),
|
||||
showName: PropTypes.string.isRequired,
|
||||
imdbId: PropTypes.string.isRequired,
|
||||
episode: PropTypes.number.isRequired,
|
||||
season: PropTypes.number.isRequired,
|
||||
searching: PropTypes.bool.isRequired,
|
||||
getEpisodeDetails: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const EpisodeTorrentsButton = connect(null, {getEpisodeDetails})(episodeTorrentsButton);
|
82
frontend/js/components/shows/details/track.js
Normal file
82
frontend/js/components/shows/details/track.js
Normal file
@ -0,0 +1,82 @@
|
||||
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);
|
@ -7,7 +7,6 @@ import { selectShow, addShowToWishlist,
|
||||
|
||||
import ListDetails from "../list/details"
|
||||
import ListPosters from "../list/posters"
|
||||
import ShowButtons from "./listButtons"
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
@ -28,7 +27,7 @@ const ShowList = (props) => {
|
||||
props.history.push("/shows/details/" + imdbId);
|
||||
}
|
||||
|
||||
let selectedShow;
|
||||
let selectedShow = Map();
|
||||
if (props.selectedImdbId !== "") {
|
||||
selectedShow = props.shows.get(props.selectedImdbId);
|
||||
}
|
||||
@ -49,17 +48,7 @@ const ShowList = (props) => {
|
||||
params={props.match.params}
|
||||
loading={props.loading}
|
||||
/>
|
||||
{selectedShow &&
|
||||
<ListDetails data={selectedShow} loading={props.loading}>
|
||||
<ShowButtons
|
||||
show={selectedShow}
|
||||
deleteFromWishlist={props.deleteShowFromWishlist}
|
||||
addToWishlist={props.addShowToWishlist}
|
||||
getDetails={props.getShowDetails}
|
||||
updateFilter={props.updateFilter}
|
||||
/>
|
||||
</ListDetails>
|
||||
}
|
||||
<ListDetails data={selectedShow} loading={props.loading} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,62 +0,0 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { Map } from "immutable"
|
||||
|
||||
import { Link } from "react-router-dom"
|
||||
import Dropdown from "react-bootstrap/Dropdown"
|
||||
import ButtonToolbar from "react-bootstrap/ButtonToolbar"
|
||||
|
||||
import { WishlistButton, RefreshButton } from "../buttons/actions"
|
||||
|
||||
const ShowButtons = (props) => (
|
||||
<ButtonToolbar>
|
||||
<ActionsButton
|
||||
show={props.show}
|
||||
addToWishlist={props.addToWishlist}
|
||||
deleteFromWishlist={props.deleteFromWishlist}
|
||||
getDetails={props.getDetails}
|
||||
/>
|
||||
<Link className="btn btn-primary" to={"/shows/details/" + props.show.get("imdb_id")}>
|
||||
<i className="fa fa-external-link"></i> Details
|
||||
</Link>
|
||||
</ButtonToolbar>
|
||||
);
|
||||
ShowButtons.propTypes = {
|
||||
show: PropTypes.instanceOf(Map),
|
||||
addToWishlist: PropTypes.func.isRequired,
|
||||
deleteFromWishlist: PropTypes.func.isRequired,
|
||||
getDetails: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
const ActionsButton = (props) => {
|
||||
let wishlisted = (props.show.get("tracked_season") !== null && props.show.get("tracked_episode") !== null);
|
||||
return (
|
||||
<Dropdown drop="up">
|
||||
<Dropdown.Toggle variant="secondary" id="movie-button-actions">
|
||||
Actions
|
||||
</Dropdown.Toggle>
|
||||
|
||||
<Dropdown.Menu>
|
||||
<RefreshButton
|
||||
fetching={props.show.get("fetchingDetails")}
|
||||
resourceId={props.show.get("imdb_id")}
|
||||
getDetails={props.getDetails}
|
||||
/>
|
||||
<WishlistButton
|
||||
resourceId={props.show.get("imdb_id")}
|
||||
wishlisted={wishlisted}
|
||||
addToWishlist={props.addToWishlist}
|
||||
deleteFromWishlist={props.deleteFromWishlist}
|
||||
/>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
ActionsButton.propTypes = {
|
||||
show: PropTypes.instanceOf(Map),
|
||||
addToWishlist: PropTypes.func.isRequired,
|
||||
deleteFromWishlist: PropTypes.func.isRequired,
|
||||
getDetails: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
export default ShowButtons;
|
19
frontend/js/utils.js
Normal file
19
frontend/js/utils.js
Normal file
@ -0,0 +1,19 @@
|
||||
export const prettyDurationFromMinutes = (runtime) => {
|
||||
const hours = Math.floor(runtime / 60);
|
||||
const minutes = (runtime % 60);
|
||||
|
||||
let duration = "";
|
||||
if (hours > 0) { duration += hours + "h" }
|
||||
if (minutes > 0) { duration += ("0" + minutes).slice(-2) }
|
||||
if (hours === 0) { duration += " min" }
|
||||
|
||||
return duration;
|
||||
}
|
||||
|
||||
const pad = (d) => (d < 10) ? "0" + d.toString() : d.toString();
|
||||
|
||||
export const prettyEpisodeName = (showName, season, episode) =>
|
||||
`${showName} S${pad(season)}E${pad(episode)}`;
|
||||
|
||||
export const inLibrary = (element) =>
|
||||
element.get("polochon_url", "") !== "";
|
@ -76,18 +76,25 @@ div.show.dropdown.nav-item > div {
|
||||
}
|
||||
|
||||
.video-details {
|
||||
font-size: $font-size-sm;
|
||||
div, p {
|
||||
margin-bottom: .3rem;
|
||||
> div, > p, > span {
|
||||
margin-bottom: 1rem;
|
||||
@media (max-width: 330px) {
|
||||
margin-bottom: 0rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.episode-thumb {
|
||||
img {
|
||||
@include media-breakpoint-down(md) {
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
@include media-breakpoint-up(sm) {
|
||||
.video-details {
|
||||
font-size: $font-size-base;
|
||||
div, p {
|
||||
margin-bottom: $paragraph-margin-bottom;
|
||||
}
|
||||
|
||||
display: block;
|
||||
height: auto;
|
||||
max-width:40rem;
|
||||
max-height:22.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user