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"
|
import { configureAxios, request } from "../requests"
|
||||||
|
|
||||||
export function refreshSubtitles(type, id, season, episode) {
|
export const searchMovieSubtitles = (imdbId) => {
|
||||||
switch (type) {
|
|
||||||
case "movie":
|
|
||||||
var resourceURL = `/movies/${id}`
|
|
||||||
return request(
|
return request(
|
||||||
"MOVIE_SUBTITLES_UPDATE",
|
"MOVIE_SUBTITLES_UPDATE",
|
||||||
configureAxios().post(`${resourceURL}/subtitles/refresh`),
|
configureAxios().post(`/movies/${imdbId}/subtitles/refresh`),
|
||||||
null,
|
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(
|
return request(
|
||||||
"EPISODE_SUBTITLES_UPDATE",
|
"EPISODE_SUBTITLES_UPDATE",
|
||||||
configureAxios().post(`${resourceURL}/subtitles/refresh`),
|
configureAxios().post(`${url}/subtitles/refresh`),
|
||||||
null,
|
null,
|
||||||
{
|
{
|
||||||
imdbId: id,
|
imdbId: imdbId,
|
||||||
season: season,
|
season: season,
|
||||||
episode: episode,
|
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 MoviesRoute from "./components/movies/route"
|
||||||
import NavBar from "./components/navbar"
|
import NavBar from "./components/navbar"
|
||||||
import WsHandler from "./components/websocket"
|
import WsHandler from "./components/websocket"
|
||||||
import ShowDetails from "./components/shows/details"
|
import { ShowDetails } from "./components/shows/details"
|
||||||
import ShowList from "./components/shows/list"
|
import ShowList from "./components/shows/list"
|
||||||
import ShowsRoute from "./components/shows/route"
|
import ShowsRoute from "./components/shows/route"
|
||||||
import TorrentList from "./components/torrents/list"
|
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; }
|
if (!url || url === "") { return null; }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<span>
|
||||||
<DownloadButton url={url} />
|
<DownloadButton url={url} />
|
||||||
<StreamButton url={url} name={name} subtitles={subtitles} />
|
<StreamButton url={url} name={name} subtitles={subtitles} />
|
||||||
</React.Fragment>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
DownloadAndStream.propTypes = {
|
DownloadAndStream.propTypes = {
|
||||||
@ -58,9 +58,8 @@ StreamButton.propTypes = {
|
|||||||
subtitles: PropTypes.instanceOf(List),
|
subtitles: PropTypes.instanceOf(List),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const Player = ({ url, subtitles }) => {
|
const Player = ({ url, subtitles }) => {
|
||||||
const hasSubtitles = !(subtitles === undefined || subtitles === null || subtitles.size === 0);
|
const hasSubtitles = !(subtitles === null || subtitles.size === 0);
|
||||||
return (
|
return (
|
||||||
<div className="embed-responsive embed-responsive-16by9">
|
<div className="embed-responsive embed-responsive-16by9">
|
||||||
<video className="embed-responsive-item" controls>
|
<video className="embed-responsive-item" controls>
|
||||||
@ -82,3 +81,6 @@ Player.propTypes = {
|
|||||||
subtitles: PropTypes.instanceOf(List),
|
subtitles: PropTypes.instanceOf(List),
|
||||||
url: PropTypes.string.isRequired,
|
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 Dropdown from "react-bootstrap/Dropdown"
|
||||||
|
|
||||||
import RefreshIndicator from "./refresh"
|
export const SubtitlesButton = ({
|
||||||
|
subtitles,
|
||||||
const SubtitlesButton = (props) => {
|
inLibrary,
|
||||||
const subtitles = props.subtitles;
|
searching,
|
||||||
const hasSubtitles = !(subtitles === undefined || subtitles === null || subtitles.size === 0);
|
search,
|
||||||
const size = props.xs ? "sm" : "";
|
}) => {
|
||||||
|
if (inLibrary === false) { return null }
|
||||||
|
|
||||||
|
const count = (subtitles && subtitles.size !== 0) ? subtitles.size : 0;
|
||||||
return (
|
return (
|
||||||
|
<span className="mr-1 mb-1">
|
||||||
<Dropdown drop="up">
|
<Dropdown drop="up">
|
||||||
<Dropdown.Toggle size={size} variant="success" id="movie-subtitles">
|
<Dropdown.Toggle variant="success">
|
||||||
|
<i className="fa fa-language mr-1" />
|
||||||
Subtitles
|
Subtitles
|
||||||
|
<span className="ml-1 badge badge-pill badge-light">
|
||||||
|
{count}
|
||||||
|
</span>
|
||||||
</Dropdown.Toggle>
|
</Dropdown.Toggle>
|
||||||
|
|
||||||
<Dropdown.Menu>
|
<Dropdown.Menu>
|
||||||
<RefreshButton
|
<Dropdown.Header>
|
||||||
type={props.type}
|
<span className="text-warning">Advanced</span>
|
||||||
resourceID={props.resourceID}
|
</Dropdown.Header>
|
||||||
season={props.season}
|
<Dropdown.Item onClick={search} >
|
||||||
episode={props.episode}
|
<i className={`fa ${ searching ? "fa-spin" : "" } fa-refresh mr-1`} />
|
||||||
fetching={props.fetching ? true : false}
|
Automatic search
|
||||||
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.Item>
|
</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.Menu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
SubtitlesButton.propTypes = {
|
SubtitlesButton.propTypes = {
|
||||||
subtitles: PropTypes.instanceOf(List),
|
subtitles: PropTypes.instanceOf(List),
|
||||||
xs: PropTypes.bool,
|
inLibrary: PropTypes.bool.isRequired,
|
||||||
fetching: PropTypes.bool,
|
searching: PropTypes.bool.isRequired,
|
||||||
refreshSubtitles: PropTypes.func.isRequired,
|
search: 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,
|
|
||||||
}
|
}
|
||||||
|
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 React from "react"
|
||||||
import { Map, List } from "immutable"
|
|
||||||
import PropTypes from "prop-types"
|
import PropTypes from "prop-types"
|
||||||
|
import { Map } from "immutable"
|
||||||
|
|
||||||
import { PolochonMetadata } from "../buttons/polochon"
|
|
||||||
import { DownloadAndStream } from "../buttons/download"
|
import { DownloadAndStream } from "../buttons/download"
|
||||||
import { ImdbBadge } from "../buttons/imdb"
|
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) => {
|
const ListDetails = (props) => {
|
||||||
if (props.loading) { return null }
|
|
||||||
if (props.data === undefined) { return null }
|
if (props.data === undefined) { return null }
|
||||||
|
if (props.loading) { return null }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="col-8 col-md-4 list-details pl-1 d-flex align-items-start flex-column">
|
<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">
|
<div className="video-details flex-fill d-flex flex-column">
|
||||||
<h2 className="d-none d-sm-block">{props.data.get("title")}</h2>
|
<Title title={props.data.get("title")} />
|
||||||
<h4 className="d-block d-sm-none">{props.data.get("title")}</h4>
|
|
||||||
<TrackingLabel
|
<TrackingLabel
|
||||||
wishlisted={props.data.get("wishlisted")}
|
wishlisted={props.data.get("wishlisted")}
|
||||||
trackedSeason={props.data.get("tracked_season")}
|
trackedSeason={props.data.get("tracked_season")}
|
||||||
trackedEpisode={props.data.get("tracked_episode")}
|
trackedEpisode={props.data.get("tracked_episode")}
|
||||||
/>
|
/>
|
||||||
<h4 className="d-none d-sm-block">{props.data.get("year")}</h4>
|
<ReleaseDate date={props.data.get("year")} />
|
||||||
<h5 className="d-block d-sm-none">{props.data.get("year")}</h5>
|
|
||||||
<Runtime runtime={props.data.get("runtime")} />
|
<Runtime runtime={props.data.get("runtime")} />
|
||||||
<Genres genres={props.data.get("genres")} />
|
<Genres genres={props.data.get("genres")} />
|
||||||
<Ratings
|
<Rating
|
||||||
rating={props.data.get("rating")}
|
rating={props.data.get("rating")}
|
||||||
votes={props.data.get("votes")}
|
votes={props.data.get("votes")}
|
||||||
/>
|
/>
|
||||||
@ -36,7 +41,6 @@ const ListDetails = (props) => {
|
|||||||
subtitles={props.data.get("subtitles")}
|
subtitles={props.data.get("subtitles")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<PolochonMetadata
|
<PolochonMetadata
|
||||||
quality={props.data.get("quality")}
|
quality={props.data.get("quality")}
|
||||||
releaseGroup={props.data.get("release_group")}
|
releaseGroup={props.data.get("release_group")}
|
||||||
@ -44,10 +48,7 @@ const ListDetails = (props) => {
|
|||||||
audioCodec={props.data.get("audio_codec")}
|
audioCodec={props.data.get("audio_codec")}
|
||||||
videoCodec={props.data.get("video_codec")}
|
videoCodec={props.data.get("video_codec")}
|
||||||
/>
|
/>
|
||||||
</div>
|
<Plot plot={props.data.get("plot")} />
|
||||||
<p className="text text-break plot">{props.data.get("plot")}</p>
|
|
||||||
</div>
|
|
||||||
<div className="pb-1 align-self-end">
|
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -60,48 +61,6 @@ ListDetails.propTypes = {
|
|||||||
};
|
};
|
||||||
export default ListDetails;
|
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) => {
|
const TrackingLabel = (props) => {
|
||||||
let wishlistStr = props.wishlisted ? "Wishlisted" : "";
|
let wishlistStr = props.wishlisted ? "Wishlisted" : "";
|
||||||
|
|
||||||
@ -131,24 +90,3 @@ TrackingLabel.propTypes = {
|
|||||||
trackedSeason: PropTypes.number,
|
trackedSeason: PropTypes.number,
|
||||||
trackedEpisode: 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 React, { useState, useEffect } from "react"
|
||||||
import PropTypes from "prop-types"
|
import PropTypes from "prop-types"
|
||||||
|
|
||||||
const ListFilter = (props) => {
|
const ListFilter = ({ placeHolder, updateFilter }) => {
|
||||||
const [filter, setFilter] = useState("");
|
const [filter, setFilter] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Start filtering at 3 chars
|
// Start filtering at 3 chars
|
||||||
if (filter.length >= 3) {
|
if (filter.length >= 3) {
|
||||||
props.updateFilter(filter);
|
updateFilter(filter);
|
||||||
}
|
}
|
||||||
}, [filter]);
|
}, [filter, updateFilter]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="input-group input-group-sm">
|
<div className="input-group input-group-sm">
|
||||||
<input type="text" className="form-control"
|
<input type="text" className="form-control"
|
||||||
placeholder={props.placeHolder}
|
placeholder={placeHolder}
|
||||||
onChange={(e) => setFilter(e.target.value)}
|
onChange={(e) => setFilter(e.target.value)}
|
||||||
value={filter} />
|
value={filter} />
|
||||||
<div className="input-group-append d-none d-md-block">
|
<div className="input-group-append d-none d-md-block">
|
||||||
|
@ -1,25 +1,24 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import PropTypes from "prop-types"
|
import PropTypes from "prop-types"
|
||||||
import { Map } from "immutable"
|
|
||||||
|
|
||||||
import EmptyImg from "../../../img/noimage.png"
|
import EmptyImg from "../../../img/noimage.png"
|
||||||
|
|
||||||
const Poster = (props) => {
|
const Poster = ({ url, selected, onClick, onDoubleClick }) => {
|
||||||
const className = props.selected ? "border-primary thumbnail-selected" : "border-secondary";
|
const className = selected ? "border-primary thumbnail-selected" : "border-secondary";
|
||||||
const src = (props.data.get("poster_url") === "") ? EmptyImg : props.data.get("poster_url");
|
const src = (url === "") ? EmptyImg : url;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
src={src}
|
src={src}
|
||||||
onClick={props.onClick}
|
onClick={onClick}
|
||||||
onDoubleClick={props.onDoubleClick}
|
onDoubleClick={onDoubleClick}
|
||||||
className={`my-1 m-md-2 img-thumbnail object-fit-cover ${className}`}
|
className={`my-1 m-md-2 img-thumbnail object-fit-cover ${className}`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Poster.propTypes = {
|
Poster.propTypes = {
|
||||||
data: PropTypes.instanceOf(Map),
|
url: PropTypes.string,
|
||||||
selected: PropTypes.bool,
|
selected: PropTypes.bool.isRequired,
|
||||||
onClick: PropTypes.func,
|
onClick: PropTypes.func,
|
||||||
onDoubleClick: PropTypes.func,
|
onDoubleClick: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
@ -201,8 +201,8 @@ const Posters = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Poster
|
<Poster
|
||||||
data={el}
|
url={el.get("poster_url")}
|
||||||
key={`poster-${imdbId}-${index}`}
|
key={index}
|
||||||
selected={selected}
|
selected={selected}
|
||||||
onClick={() => props.selectPoster(imdbId)}
|
onClick={() => props.selectPoster(imdbId)}
|
||||||
onDoubleClick={() => props.onDoubleClick(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 PropTypes from "prop-types"
|
||||||
import { OrderedMap, Map } from "immutable"
|
import { OrderedMap, Map } from "immutable"
|
||||||
import { connect } from "react-redux"
|
import { connect } from "react-redux"
|
||||||
import { addTorrent } from "../../actions/torrents"
|
import { selectMovie, updateFilter } from "../../actions/movies"
|
||||||
import { refreshSubtitles } from "../../actions/subtitles"
|
|
||||||
import { addMovieToWishlist, deleteMovie, deleteMovieFromWishlist,
|
|
||||||
getMovieDetails, selectMovie, updateFilter } from "../../actions/movies"
|
|
||||||
|
|
||||||
import ListPosters from "../list/posters"
|
|
||||||
import ListDetails from "../list/details"
|
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) {
|
function mapStateToProps(state) {
|
||||||
return {
|
return {
|
||||||
@ -19,25 +20,22 @@ function mapStateToProps(state) {
|
|||||||
movies : state.movieStore.get("movies"),
|
movies : state.movieStore.get("movies"),
|
||||||
filter : state.movieStore.get("filter"),
|
filter : state.movieStore.get("filter"),
|
||||||
selectedImdbId : state.movieStore.get("selectedImdbId"),
|
selectedImdbId : state.movieStore.get("selectedImdbId"),
|
||||||
lastFetchUrl : state.movieStore.get("lastFetchUrl"),
|
|
||||||
exploreOptions : state.movieStore.get("exploreOptions"),
|
exploreOptions : state.movieStore.get("exploreOptions"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
selectMovie, getMovieDetails, addTorrent,
|
selectMovie, updateFilter,
|
||||||
addMovieToWishlist, deleteMovie, deleteMovieFromWishlist,
|
|
||||||
refreshSubtitles, updateFilter,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const MovieList = (props) => {
|
const MovieList = (props) => {
|
||||||
let selectedMovie = undefined;
|
let selectedMovie = Map();
|
||||||
if (props.movies !== undefined &&
|
if (props.movies !== undefined &&
|
||||||
props.movies.has(props.selectedImdbId)) {
|
props.movies.has(props.selectedImdbId)) {
|
||||||
selectedMovie = props.movies.get(props.selectedImdbId);
|
selectedMovie = props.movies.get(props.selectedImdbId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row>
|
<div className="row">
|
||||||
<ListPosters
|
<ListPosters
|
||||||
data={props.movies}
|
data={props.movies}
|
||||||
type="movies"
|
type="movies"
|
||||||
@ -53,18 +51,25 @@ const MovieList = (props) => {
|
|||||||
loading={props.loading}
|
loading={props.loading}
|
||||||
/>
|
/>
|
||||||
<ListDetails data={selectedMovie} loading={props.loading}>
|
<ListDetails data={selectedMovie} loading={props.loading}>
|
||||||
<MovieButtons
|
<ShowMore
|
||||||
movie={selectedMovie}
|
id={selectedMovie.get("imdb_id")}
|
||||||
getMovieDetails={props.getMovieDetails}
|
inLibrary={inLibrary(selectedMovie)}
|
||||||
addTorrent={props.addTorrent}
|
>
|
||||||
deleteMovie={props.deleteMovie}
|
<MovieTorrentsButton
|
||||||
addToWishlist={props.addMovieToWishlist}
|
torrents={selectedMovie.get("torrents")}
|
||||||
deleteFromWishlist={props.deleteMovieFromWishlist}
|
imdbId={selectedMovie.get("imdb_id")}
|
||||||
lastFetchUrl={props.lastFetchUrl}
|
title={selectedMovie.get("title")}
|
||||||
refreshSubtitles={props.refreshSubtitles}
|
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>
|
</ListDetails>
|
||||||
</Row>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
MovieList.propTypes = {
|
MovieList.propTypes = {
|
||||||
@ -73,15 +78,8 @@ MovieList.propTypes = {
|
|||||||
selectedImdbId: PropTypes.string,
|
selectedImdbId: PropTypes.string,
|
||||||
filter: PropTypes.string,
|
filter: PropTypes.string,
|
||||||
loading: PropTypes.bool,
|
loading: PropTypes.bool,
|
||||||
lastFetchUrl: PropTypes.string,
|
|
||||||
updateFilter: PropTypes.func,
|
updateFilter: PropTypes.func,
|
||||||
selectMovie: 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,
|
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 PropTypes from "prop-types"
|
||||||
import { Map } from "immutable"
|
import { Map } from "immutable"
|
||||||
import { connect } from "react-redux"
|
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 Loader from "../loader/loader"
|
||||||
import SubtitlesButton from "../buttons/subtitles"
|
|
||||||
import RefreshIndicator from "../buttons/refresh"
|
|
||||||
|
|
||||||
import Tooltip from "react-bootstrap/Tooltip"
|
import { Fanart } from "./details/fanart"
|
||||||
import OverlayTrigger from "react-bootstrap/OverlayTrigger"
|
import { Header } from "./details/header"
|
||||||
import Dropdown from "react-bootstrap/Dropdown"
|
import { SeasonsList } from "./details/seasons"
|
||||||
import SplitButton from "react-bootstrap/SplitButton"
|
|
||||||
|
|
||||||
// Helper to change 1 to 01
|
const mapStateToProps = (state) => ({
|
||||||
const pad = (d) => (d < 10) ? "0" + d.toString() : d.toString();
|
|
||||||
|
|
||||||
function mapStateToProps(state) {
|
|
||||||
return {
|
|
||||||
loading: state.showStore.get("loading"),
|
loading: state.showStore.get("loading"),
|
||||||
show: state.showStore.get("show"),
|
show: state.showStore.get("show"),
|
||||||
};
|
})
|
||||||
}
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
addTorrent, addShowToWishlist, deleteShowFromWishlist,
|
|
||||||
fetchShowDetails, getEpisodeDetails, refreshSubtitles,
|
|
||||||
};
|
|
||||||
|
|
||||||
const ShowDetails = (props) => {
|
const showDetails = ({ show, loading }) => {
|
||||||
if (props.loading) {
|
if (loading === true) {
|
||||||
return (<Loader />);
|
return (<Loader />);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Fanart url={props.show.get("fanart_url")} />
|
<Fanart url={show.get("fanart_url")} />
|
||||||
<div className="row no-gutters">
|
<div className="row no-gutters">
|
||||||
<Header
|
<Header data={show} />
|
||||||
data={props.show}
|
<SeasonsList data={show} />
|
||||||
addToWishlist={props.addShowToWishlist}
|
|
||||||
deleteFromWishlist={props.deleteShowFromWishlist}
|
|
||||||
/>
|
|
||||||
<SeasonsList
|
|
||||||
data={props.show}
|
|
||||||
addTorrent={props.addTorrent}
|
|
||||||
addToWishlist={props.addShowToWishlist}
|
|
||||||
getEpisodeDetails={props.getEpisodeDetails}
|
|
||||||
refreshSubtitles={props.refreshSubtitles}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
ShowDetails.propTypes = {
|
showDetails.propTypes = {
|
||||||
loading: PropTypes.bool,
|
loading: PropTypes.bool,
|
||||||
show: PropTypes.instanceOf(Map),
|
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);
|
export const ShowDetails = connect(mapStateToProps)(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);
|
|
||||||
|
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 ListDetails from "../list/details"
|
||||||
import ListPosters from "../list/posters"
|
import ListPosters from "../list/posters"
|
||||||
import ShowButtons from "./listButtons"
|
|
||||||
|
|
||||||
function mapStateToProps(state) {
|
function mapStateToProps(state) {
|
||||||
return {
|
return {
|
||||||
@ -28,7 +27,7 @@ const ShowList = (props) => {
|
|||||||
props.history.push("/shows/details/" + imdbId);
|
props.history.push("/shows/details/" + imdbId);
|
||||||
}
|
}
|
||||||
|
|
||||||
let selectedShow;
|
let selectedShow = Map();
|
||||||
if (props.selectedImdbId !== "") {
|
if (props.selectedImdbId !== "") {
|
||||||
selectedShow = props.shows.get(props.selectedImdbId);
|
selectedShow = props.shows.get(props.selectedImdbId);
|
||||||
}
|
}
|
||||||
@ -49,17 +48,7 @@ const ShowList = (props) => {
|
|||||||
params={props.match.params}
|
params={props.match.params}
|
||||||
loading={props.loading}
|
loading={props.loading}
|
||||||
/>
|
/>
|
||||||
{selectedShow &&
|
<ListDetails data={selectedShow} loading={props.loading} />
|
||||||
<ListDetails data={selectedShow} loading={props.loading}>
|
|
||||||
<ShowButtons
|
|
||||||
show={selectedShow}
|
|
||||||
deleteFromWishlist={props.deleteShowFromWishlist}
|
|
||||||
addToWishlist={props.addShowToWishlist}
|
|
||||||
getDetails={props.getShowDetails}
|
|
||||||
updateFilter={props.updateFilter}
|
|
||||||
/>
|
|
||||||
</ListDetails>
|
|
||||||
}
|
|
||||||
</div>
|
</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 {
|
.video-details {
|
||||||
font-size: $font-size-sm;
|
> div, > p, > span {
|
||||||
div, p {
|
margin-bottom: 1rem;
|
||||||
margin-bottom: .3rem;
|
@media (max-width: 330px) {
|
||||||
|
margin-bottom: 0rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.episode-thumb {
|
||||||
|
img {
|
||||||
|
@include media-breakpoint-down(md) {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
display: block;
|
||||||
@include media-breakpoint-up(sm) {
|
height: auto;
|
||||||
.video-details {
|
max-width:40rem;
|
||||||
font-size: $font-size-base;
|
max-height:22.5rem;
|
||||||
div, p {
|
|
||||||
margin-bottom: $paragraph-margin-bottom;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user