Update to bootstrap 4
This commit is contained in:
parent
2bd90e5cb5
commit
b23e311238
@ -1,9 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" href="/favicon-32x32.png" sizes="32x32">
|
||||
<link rel="icon" type="image/png" href="/favicon-16x16.png" sizes="16x16">
|
||||
|
@ -12,13 +12,14 @@ import "file-loader?name=[name].[ext]!../img/favicon.ico"
|
||||
import "file-loader?name=[name].[ext]!../img/safari-pinned-tab.svg"
|
||||
|
||||
// Styles
|
||||
import "../less/app.less"
|
||||
import "../scss/app.scss"
|
||||
|
||||
// React
|
||||
import React from "react"
|
||||
import ReactDOM from "react-dom"
|
||||
import { Provider } from "react-redux"
|
||||
import { Router, Route, Switch, Redirect } from "react-router-dom"
|
||||
import Container from "react-bootstrap/Container"
|
||||
|
||||
// Auth
|
||||
import { ProtectedRoute, AdminRoute } from "./auth"
|
||||
@ -50,7 +51,7 @@ const App = () => (
|
||||
<WsHandler />
|
||||
<NavBar />
|
||||
<Alert />
|
||||
<div className="container-fluid">
|
||||
<Container fluid>
|
||||
<Switch>
|
||||
<AdminRoute path="/admin" exact component={AdminPanel} />
|
||||
<Route path="/users/profile" exact component={UserProfile} />
|
||||
@ -71,7 +72,7 @@ const App = () => (
|
||||
<Redirect to="/movies/explore/yts/seeds" />
|
||||
}/>
|
||||
</Switch>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
@ -2,8 +2,7 @@ import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
export const Stats = props => (
|
||||
<div>
|
||||
<h2 className="hidden-xs">Stats</h2>
|
||||
<div className="row d-flex flex-wrap">
|
||||
<Stat
|
||||
name="Movies"
|
||||
count={props.stats.get("movies_count")}
|
||||
@ -27,15 +26,15 @@ export const Stats = props => (
|
||||
Stats.propTypes = { stats: PropTypes.object }
|
||||
|
||||
const Stat = props => (
|
||||
<div className="col-xs-4">
|
||||
<div className="panel panel-default">
|
||||
<div className="panel-heading">
|
||||
<h3 className="panel-title">
|
||||
<div className="col-12 col-md-4 my-2">
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h3>
|
||||
{props.name}
|
||||
<span className="label label-info pull-right">{props.count}</span>
|
||||
<span className="badge badge-pill badge-info pull-right">{props.count}</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div className="panel-body">
|
||||
<div className="card-body">
|
||||
<TorrentsStat data={props} />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -6,12 +6,10 @@ import { Button, Modal } from "react-bootstrap"
|
||||
import Toggle from "react-bootstrap-toggle";
|
||||
|
||||
export const UserList = props => (
|
||||
<div>
|
||||
<h2 className="hidden-xs">Users</h2>
|
||||
<h3 className="visible-xs">Users</h3>
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr className="active">
|
||||
<div className="table-responsive my-2">
|
||||
<table className="table table-striped">
|
||||
<thead className="table-secondary">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
<th>Activated</th>
|
||||
@ -100,16 +98,18 @@ function UserEdit(props) {
|
||||
Edit user - {props.data.get("Name")}
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body bsClass="modal-body admin-edit-user-modal">
|
||||
<Modal.Body bsPrefix="modal-body admin-edit-user-modal">
|
||||
<form className="form-horizontal" onSubmit={(ev) => handleSubmit(ev)}>
|
||||
<div className="form-group">
|
||||
<label>Account status</label>
|
||||
<Toggle className="pull-right" on="Activated" off="Deactivated" active={activated} onClick={() => setActivated(!activated)}
|
||||
<Toggle className="pull-right" on="Activated" off="Deactivated" active={activated}
|
||||
offstyle="danger" handlestyle="secondary" onClick={() => setActivated(!activated)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Admin status</label>
|
||||
<Toggle className="pull-right" on="Admin" off="User" active={admin} onClick={() => setAdmin(!admin)} />
|
||||
<Toggle className="pull-right" on="Admin" off="User" active={admin}
|
||||
offstyle="info" handlestyle="secondary" onClick={() => setAdmin(!admin)} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="control-label">Polochon URL</label>
|
||||
@ -122,7 +122,7 @@ function UserEdit(props) {
|
||||
</form>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button bsStyle="success" onClick={handleSubmit}>Apply</Button>
|
||||
<Button variant="success" onClick={handleSubmit}>Apply</Button>
|
||||
<Button onClick={() => setModal(false)}>Close</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
|
@ -1,80 +1,70 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
import { MenuItem } from "react-bootstrap"
|
||||
import Dropdown from "react-bootstrap/Dropdown"
|
||||
|
||||
import RefreshIndicator from "./refresh"
|
||||
|
||||
export class WishlistButton extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
}
|
||||
handleClick(e) {
|
||||
export const WishlistButton = (props) => {
|
||||
const handleClick = (e) => {
|
||||
e.preventDefault();
|
||||
if (this.props.wishlisted) {
|
||||
this.props.deleteFromWishlist(this.props.resourceId);
|
||||
if (props.wishlisted) {
|
||||
props.deleteFromWishlist(props.resourceId);
|
||||
} else {
|
||||
this.props.addToWishlist(this.props.resourceId);
|
||||
props.addToWishlist(props.resourceId);
|
||||
}
|
||||
}
|
||||
render() {
|
||||
if (this.props.wishlisted) {
|
||||
|
||||
if (props.wishlisted) {
|
||||
return (
|
||||
<MenuItem onClick={this.handleClick}>
|
||||
<Dropdown.Item onClick={handleClick}>
|
||||
<span>
|
||||
<i className="fa fa-bookmark"></i> Delete from wishlist
|
||||
</span>
|
||||
</MenuItem>
|
||||
</Dropdown.Item>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<MenuItem onClick={this.handleClick}>
|
||||
<Dropdown.Item onClick={handleClick}>
|
||||
<span>
|
||||
<i className="fa fa-bookmark-o"></i> Add to wishlist
|
||||
</span>
|
||||
</MenuItem>
|
||||
</Dropdown.Item>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class DeleteButton extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
export const DeleteButton = (props) => {
|
||||
const handleClick = () => {
|
||||
props.deleteFunc(props.resourceId, props.lastFetchUrl);
|
||||
}
|
||||
handleClick(e) {
|
||||
e.preventDefault();
|
||||
this.props.deleteFunc(this.props.resourceId, this.props.lastFetchUrl);
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<MenuItem onClick={this.handleClick}>
|
||||
<Dropdown.Item onClick={handleClick}>
|
||||
<span>
|
||||
<i className="fa fa-trash"></i> Delete
|
||||
</span>
|
||||
</MenuItem>
|
||||
</Dropdown.Item>
|
||||
);
|
||||
}
|
||||
}
|
||||
DeleteButton.propTypes = {
|
||||
resourceId: PropTypes.string.isRequired,
|
||||
lastFetchUrl: PropTypes.string,
|
||||
deleteFunc: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export class RefreshButton extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
export const RefreshButton = (props) => {
|
||||
const handleClick = () => {
|
||||
if (props.fetching) { return; }
|
||||
props.getDetails(props.resourceId);
|
||||
}
|
||||
handleClick(e) {
|
||||
e.preventDefault();
|
||||
if (this.props.fetching) {
|
||||
return
|
||||
}
|
||||
this.props.getDetails(this.props.resourceId);
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<MenuItem onClick={this.handleClick}>
|
||||
<RefreshIndicator refresh={this.props.fetching} />
|
||||
</MenuItem>
|
||||
<Dropdown.Item onClick={handleClick}>
|
||||
<RefreshIndicator refresh={props.fetching} />
|
||||
</Dropdown.Item>
|
||||
);
|
||||
}
|
||||
}
|
||||
RefreshButton.propTypes = {
|
||||
fetching: PropTypes.bool.isRequired,
|
||||
resourceId: PropTypes.string.isRequired,
|
||||
getDetails: PropTypes.func.isRequired,
|
||||
};
|
||||
|
@ -2,53 +2,42 @@ import React, { useState } from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { List } from "immutable"
|
||||
|
||||
import { Button, Dropdown, MenuItem, Modal } from "react-bootstrap"
|
||||
import Modal from "react-bootstrap/Modal"
|
||||
import Dropdown from "react-bootstrap/Dropdown"
|
||||
import SplitButton from "react-bootstrap/SplitButton"
|
||||
|
||||
const DownloadButton = (props) => {
|
||||
if (props.url === "") { return null; }
|
||||
const size = props.xs ? "sm" : "";
|
||||
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
||||
let btnSize = "btn-sm";
|
||||
if (props.xs) {
|
||||
btnSize = "btn-xs";
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown id="streaming-buttons" className={props.customClassName} dropup>
|
||||
<Button bsStyle="danger" className={btnSize} href={props.url}>
|
||||
<span>
|
||||
<i className="fa fa-download" aria-hidden="true"></i> Download
|
||||
</span>
|
||||
</Button>
|
||||
<Dropdown.Toggle bsStyle="danger" className={btnSize}/>
|
||||
<Dropdown.Menu>
|
||||
<MenuItem eventKey="2" onClick={() => setShowModal(true)}>
|
||||
<React.Fragment>
|
||||
<SplitButton
|
||||
drop="up"
|
||||
variant="danger"
|
||||
title="Download"
|
||||
size={size}
|
||||
id="download-button-id">
|
||||
<Dropdown.Item eventKey="1" onClick={() => setShowModal(true)}>
|
||||
<span>
|
||||
<i className="fa fa-globe"></i> Stream in browser
|
||||
</span>
|
||||
</MenuItem>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown.Item>
|
||||
</SplitButton>
|
||||
|
||||
<Modal show={showModal} onHide={() => setShowModal(false)} dialogClassName="player-modal">
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>
|
||||
<i className="fa fa-globe"></i>
|
||||
Browser streaming
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal show={showModal} onHide={() => setShowModal(false)} size="lg" centered>
|
||||
<Modal.Header closeButton>{props.name}</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Player
|
||||
url={props.url}
|
||||
subtitles={props.subtitles}
|
||||
/>
|
||||
<Player url={props.url} subtitles={props.subtitles} />
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
</Dropdown>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
DownloadButton.propTypes = {
|
||||
customClassName: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
xs: PropTypes.bool,
|
||||
url: PropTypes.string.isRequired,
|
||||
subtitles: PropTypes.instanceOf(List),
|
||||
@ -60,7 +49,7 @@ const Player = (props) => {
|
||||
const hasSubtitles = !(subtitles === undefined || subtitles === null || subtitles.size === 0);
|
||||
return (
|
||||
<div className="embed-responsive embed-responsive-16by9">
|
||||
<video controls>
|
||||
<video className="embed-responsive-item" controls>
|
||||
<source src={props.url} type="video/mp4"/>
|
||||
{hasSubtitles && subtitles.toIndexedSeq().map((el, index) => (
|
||||
<track
|
||||
|
@ -1,15 +1,22 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
export default function ImdbLink(props) {
|
||||
const link = `http://www.imdb.com/title/${props.imdbId}`;
|
||||
let className = "btn btn-warning";
|
||||
if (props.size) {
|
||||
className += " btn-" + props.size;
|
||||
const ImdbLink = (props) => {
|
||||
if (!props.imdbId || props.imdbId === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return(
|
||||
<a type="button" className={className} href={link}>
|
||||
<a
|
||||
className={`btn btn-warning ${props.xs ? "btn-sm" : ""}`}
|
||||
href={`http://www.imdb.com/title/${props.imdbId}`}>
|
||||
<i className="fa fa-external-link"></i> IMDB
|
||||
</a>
|
||||
);
|
||||
}
|
||||
};
|
||||
ImdbLink.propTypes = {
|
||||
imdbId: PropTypes.string,
|
||||
xs: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default ImdbLink;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
export default function RefreshIndicator(props) {
|
||||
const RefreshIndicator = (props) => {
|
||||
if (!props.refresh) {
|
||||
return (
|
||||
<span>
|
||||
@ -15,4 +16,8 @@ export default function RefreshIndicator(props) {
|
||||
);
|
||||
}
|
||||
}
|
||||
RefreshIndicator.propTypes = {
|
||||
refresh: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default RefreshIndicator;
|
||||
|
@ -1,60 +1,78 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { List } from "immutable"
|
||||
|
||||
import { DropdownButton, MenuItem } from "react-bootstrap"
|
||||
import Dropdown from "react-bootstrap/Dropdown"
|
||||
|
||||
import RefreshIndicator from "./refresh"
|
||||
|
||||
export default function SubtitlesButton(props) {
|
||||
const btnSize = props.xs ? "xsmall" : "small";
|
||||
const SubtitlesButton = (props) => {
|
||||
const subtitles = props.subtitles;
|
||||
const hasSubtitles = !(subtitles === undefined || subtitles === null || subtitles.size === 0);
|
||||
const size = props.xs ? "sm" : "";
|
||||
|
||||
return (
|
||||
<DropdownButton
|
||||
bsStyle="success"
|
||||
bsSize={btnSize}
|
||||
title="Subtitles"
|
||||
id="download-subtitles-button"
|
||||
dropup>
|
||||
<Dropdown drop="up">
|
||||
<Dropdown.Toggle size={size} variant="success" id="movie-subtitles">
|
||||
Subtitles
|
||||
</Dropdown.Toggle>
|
||||
|
||||
<Dropdown.Menu>
|
||||
<RefreshButton
|
||||
type={props.type}
|
||||
resourceID={props.resourceID}
|
||||
season={props.season}
|
||||
episode={props.episode}
|
||||
fetching={props.fetching}
|
||||
fetching={props.fetching ? true : false}
|
||||
refreshSubtitles={props.refreshSubtitles}
|
||||
/>
|
||||
{hasSubtitles &&
|
||||
<MenuItem divider></MenuItem>
|
||||
<Dropdown.Divider />
|
||||
}
|
||||
{hasSubtitles && subtitles.toIndexedSeq().map(function(subtitle, index) {
|
||||
return (
|
||||
<MenuItem key={index} href={subtitle.get("url")}>
|
||||
<Dropdown.Item key={index} href={subtitle.get("url")}>
|
||||
<i className="fa fa-download"></i> {subtitle.get("language").split("_")[1]}
|
||||
</MenuItem>
|
||||
</Dropdown.Item>
|
||||
);
|
||||
})}
|
||||
</DropdownButton>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
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,
|
||||
}
|
||||
|
||||
class RefreshButton extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
}
|
||||
handleClick(e) {
|
||||
e.preventDefault();
|
||||
if (this.props.fetching) {
|
||||
export default SubtitlesButton;
|
||||
|
||||
const RefreshButton = (props) => {
|
||||
const handleClick = () => {
|
||||
if (props.fetching) {
|
||||
return
|
||||
}
|
||||
this.props.refreshSubtitles(this.props.type, this.props.resourceID,
|
||||
this.props.season, this.props.episode);
|
||||
props.refreshSubtitles(props.type, props.resourceID,
|
||||
props.season, props.episode);
|
||||
}
|
||||
render() {
|
||||
|
||||
return (
|
||||
<MenuItem onClick={this.handleClick}>
|
||||
<RefreshIndicator refresh={this.props.fetching} />
|
||||
</MenuItem>
|
||||
<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,
|
||||
}
|
||||
|
@ -2,17 +2,22 @@ import React from "react"
|
||||
import { Map, List } from "immutable"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
const ListDetails = (props) => (
|
||||
<div className="col-xs-7 col-md-4">
|
||||
<div className="affix">
|
||||
<h1 className="hidden-xs">{props.data.get("title")}</h1>
|
||||
<h3 className="visible-xs">{props.data.get("title")}</h3>
|
||||
const ListDetails = (props) => {
|
||||
if (props.loading) { return null }
|
||||
if (props.data === undefined) { 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>
|
||||
<TrackingLabel
|
||||
wishlisted={props.data.get("wishlisted")}
|
||||
trackedSeason={props.data.get("tracked_season")}
|
||||
trackedEpisode={props.data.get("tracked_episode")}
|
||||
/>
|
||||
<h4>{props.data.get("year")}</h4>
|
||||
<h4 className="d-none d-sm-block">{props.data.get("year")}</h4>
|
||||
<h5 className="d-block d-sm-none">{props.data.get("year")}</h5>
|
||||
<Runtime runtime={props.data.get("runtime")} />
|
||||
<Genres genres={props.data.get("genres")} />
|
||||
<Ratings
|
||||
@ -26,13 +31,17 @@ const ListDetails = (props) => (
|
||||
audioCodec={props.data.get("audio_codec")}
|
||||
videoCodec={props.data.get("video_codec")}
|
||||
/>
|
||||
<p className="plot">{props.data.get("plot")}</p>
|
||||
<p className="text text-break plot">{props.data.get("plot")}</p>
|
||||
</div>
|
||||
<div className="pb-1 align-self-end">
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
</div>
|
||||
);
|
||||
}
|
||||
ListDetails.propTypes = {
|
||||
data: PropTypes.instanceOf(Map).isRequired,
|
||||
data: PropTypes.instanceOf(Map),
|
||||
loading: PropTypes.bool,
|
||||
children: PropTypes.object,
|
||||
};
|
||||
export default ListDetails;
|
||||
@ -88,9 +97,11 @@ const TrackingLabel = (props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="label label-default">
|
||||
<p>
|
||||
<span className="badge badge-secondary">
|
||||
<i className="fa fa-bookmark"></i> {wishlistStr}
|
||||
</span>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
TrackingLabel.propTypes = {
|
||||
@ -106,11 +117,11 @@ const PolochonMetadata = (props) => {
|
||||
|
||||
return (
|
||||
<p className="spaced-icons">
|
||||
<span className="label label-default">{props.quality}</span>
|
||||
<span className="label label-default">{props.container} </span>
|
||||
<span className="label label-default">{props.videoCodec}</span>
|
||||
<span className="label label-default">{props.audioCodec}</span>
|
||||
<span className="label label-default">{props.releaseGroup}</span>
|
||||
<span className="mx-1 badge badge-pill badge-secondary">{props.quality}</span>
|
||||
<span className="mx-1 badge badge-pill badge-secondary">{props.container} </span>
|
||||
<span className="mx-1 badge badge-pill badge-secondary">{props.videoCodec}</span>
|
||||
<span className="mx-1 badge badge-pill badge-secondary">{props.audioCodec}</span>
|
||||
<span className="mx-1 badge badge-pill badge-secondary">{props.releaseGroup}</span>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from "react"
|
||||
import { withRouter } from "react-router-dom"
|
||||
import { Form, FormGroup, FormControl, ControlLabel } from "react-bootstrap"
|
||||
import { Form, FormGroup, FormControl, FormLabel } from "react-bootstrap"
|
||||
|
||||
class ExplorerOptions extends React.PureComponent {
|
||||
constructor(props) {
|
||||
@ -61,10 +61,10 @@ class ExplorerOptions extends React.PureComponent {
|
||||
<div className="row">
|
||||
<div className="col-xs-12 col-md-6">
|
||||
<FormGroup>
|
||||
<ControlLabel>Source</ControlLabel>
|
||||
<FormLabel>Source</FormLabel>
|
||||
<FormControl
|
||||
bsClass="form-control input-sm"
|
||||
componentClass="select"
|
||||
bsPrefix="form-control input-sm"
|
||||
as="select"
|
||||
onChange={this.handleSourceChange}
|
||||
value={source}
|
||||
>
|
||||
@ -77,10 +77,10 @@ class ExplorerOptions extends React.PureComponent {
|
||||
</div>
|
||||
<div className="col-xs-12 col-md-6">
|
||||
<FormGroup>
|
||||
<ControlLabel>Category</ControlLabel>
|
||||
<FormLabel>Category</FormLabel>
|
||||
<FormControl
|
||||
bsClass="form-control input-sm"
|
||||
componentClass="select"
|
||||
bsPrefix="form-control input-sm"
|
||||
as="select"
|
||||
onChange={this.handleCategoryChange}
|
||||
value={category}
|
||||
>
|
||||
|
@ -1,42 +1,30 @@
|
||||
import React from "react"
|
||||
import React, { useState, useEffect } from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
export default class ListFilter extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { filter: "" };
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
}
|
||||
handleChange(ev) {
|
||||
if (ev) { ev.preventDefault(); }
|
||||
const value = this.input.value;
|
||||
if (this.state.filter === value) { return }
|
||||
this.setState({ filter: value });
|
||||
const ListFilter = (props) => {
|
||||
const [filter, setFilter] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
// Start filtering at 3 chars
|
||||
if (value.length >= 3) {
|
||||
this.props.updateFilter(value);
|
||||
} else {
|
||||
this.props.updateFilter("");
|
||||
if (filter.length >= 3) {
|
||||
props.updateFilter(filter);
|
||||
}
|
||||
}
|
||||
render() {
|
||||
}, [filter]);
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-xs-12 col-md-12 list-filter">
|
||||
<form className="input-group" onSubmit={(ev) => this.handleChange(ev)}>
|
||||
<input
|
||||
className="form-control input-sm"
|
||||
placeholder={this.props.placeHolder}
|
||||
onChange={this.handleChange}
|
||||
ref={(input) => this.input = input}
|
||||
value={this.state.filter}
|
||||
/>
|
||||
<span className="input-group-btn hidden-xs">
|
||||
<button className="btn btn-default btn-sm" type="button">Filter</button>
|
||||
</span>
|
||||
</form>
|
||||
<div className="input-group input-group-sm">
|
||||
<input type="text" className="form-control"
|
||||
placeholder={props.placeHolder}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
value={filter} />
|
||||
<div className="input-group-append d-none d-md-block">
|
||||
<span className="input-group-text">Filter</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
ListFilter.propTypes = {
|
||||
updateFilter: PropTypes.func.isRequired,
|
||||
placeHolder: PropTypes.string.isRequired,
|
||||
};
|
||||
export default ListFilter;
|
||||
|
@ -1,19 +1,23 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { Map } from "immutable"
|
||||
|
||||
export default function Poster(props) {
|
||||
const selected = props.selected ? " thumbnail-selected" : "";
|
||||
const imgClass = "thumbnail" + selected;
|
||||
const Poster = (props) => {
|
||||
const className = props.selected ? "border-primary thumbnail-selected" : "border-secondary";
|
||||
return (
|
||||
<div>
|
||||
<div className="col-xs-12 col-sm-6 col-md-3 col-lg-2">
|
||||
<a className={imgClass}>
|
||||
<img
|
||||
src={props.data.get("poster_url")}
|
||||
onClick={props.onClick}
|
||||
onDoubleClick={props.onDoubleClick}
|
||||
className={`my-1 m-md-2 img-thumbnail ${className}`}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Poster.propTypes = {
|
||||
data: PropTypes.instanceOf(Map),
|
||||
selected: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
onDoubleClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Poster;
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React from "react"
|
||||
|
||||
import { Map } from "immutable"
|
||||
import React, { useState, useEffect } from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { OrderedMap, Map } from "immutable"
|
||||
import fuzzy from "fuzzy";
|
||||
import InfiniteScroll from "react-infinite-scroller";
|
||||
import InfiniteScroll from "react-infinite-scroll-component";
|
||||
|
||||
import ListFilter from "./filter"
|
||||
import ExplorerOptions from "./explorerOptions"
|
||||
@ -10,69 +10,24 @@ import Poster from "./poster"
|
||||
|
||||
import Loader from "../loader/loader"
|
||||
|
||||
const DEFAULT_ADD_EXTRA_ITEMS = 30;
|
||||
|
||||
export default class ListPosters extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.loadMore = this.loadMore.bind(this);
|
||||
this.state = this.getNextState(props);
|
||||
}
|
||||
loadMore() {
|
||||
// Nothing to do if the app is loading
|
||||
if (this.props.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.props.data === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState(this.getNextState(this.props));
|
||||
}
|
||||
getNextState(props) {
|
||||
let totalListSize = props.data !== undefined ? props.data.size : 0;
|
||||
let currentListSize = (this.state && this.state.items) ? this.state.items : 0;
|
||||
let nextListSize = currentListSize + DEFAULT_ADD_EXTRA_ITEMS;
|
||||
let hasMore = true;
|
||||
|
||||
if (nextListSize >= totalListSize) {
|
||||
nextListSize = totalListSize;
|
||||
hasMore = false;
|
||||
}
|
||||
|
||||
return {
|
||||
items: nextListSize,
|
||||
hasMore: hasMore,
|
||||
};
|
||||
}
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.data === undefined) { return }
|
||||
if (nextProps.data === undefined) { return }
|
||||
|
||||
if (this.props.data.size !== nextProps.data.size) {
|
||||
this.setState(this.getNextState(nextProps));
|
||||
}
|
||||
}
|
||||
render() {
|
||||
let elmts = this.props.data;
|
||||
const ListPosters = (props) => {
|
||||
let elmts = props.data;
|
||||
const listSize = elmts !== undefined ? elmts.size : 0;
|
||||
const colSize = (listSize !== 0) ? "col-xs-5 col-md-8" : "col-xs-12";
|
||||
|
||||
// Filter the list of elements
|
||||
if (this.props.filter !== "") {
|
||||
elmts = elmts.filter((v) => fuzzy.test(this.props.filter, v.get("title")), this);
|
||||
if (props.filter !== "") {
|
||||
elmts = elmts.filter((v) => fuzzy.test(props.filter, v.get("title")));
|
||||
} else {
|
||||
elmts = elmts.slice(0, this.state.items);
|
||||
elmts = elmts.slice(0, listSize.items);
|
||||
}
|
||||
|
||||
// Chose when to display filter / explore options
|
||||
let displayFilter = true;
|
||||
if ((this.props.params
|
||||
&& this.props.params.category
|
||||
&& this.props.params.category !== ""
|
||||
&& this.props.params.source
|
||||
&& this.props.params.source !== "")
|
||||
if ((props.params
|
||||
&& props.params.category
|
||||
&& props.params.category !== ""
|
||||
&& props.params.source
|
||||
&& props.params.source !== "")
|
||||
|| (listSize === 0)) {
|
||||
displayFilter = false;
|
||||
}
|
||||
@ -84,89 +39,53 @@ export default class ListPosters extends React.PureComponent {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={colSize}>
|
||||
<div className="col px-1">
|
||||
{displayFilter &&
|
||||
<ListFilter
|
||||
updateFilter={this.props.updateFilter}
|
||||
placeHolder={this.props.placeHolder}
|
||||
updateFilter={props.updateFilter}
|
||||
placeHolder={props.placeHolder}
|
||||
/>
|
||||
}
|
||||
<ExplorerOptions
|
||||
type={this.props.type}
|
||||
type={props.type}
|
||||
display={displayExplorerOptions}
|
||||
params={this.props.params}
|
||||
options={this.props.exploreOptions}
|
||||
params={props.params}
|
||||
options={props.exploreOptions}
|
||||
/>
|
||||
<Posters
|
||||
elmts={elmts}
|
||||
loading={this.props.loading}
|
||||
hasMore={this.state.hasMore}
|
||||
loadMore={this.loadMore}
|
||||
selectedImdbId={this.props.selectedImdbId}
|
||||
selectPoster={this.props.onClick}
|
||||
onDoubleClick={this.props.onDoubleClick}
|
||||
onKeyEnter={this.props.onKeyEnter}
|
||||
loading={props.loading}
|
||||
selectedImdbId={props.selectedImdbId}
|
||||
selectPoster={props.onClick}
|
||||
onDoubleClick={props.onDoubleClick}
|
||||
onKeyEnter={props.onKeyEnter}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
ListPosters.propTypes = {
|
||||
data: PropTypes.instanceOf(OrderedMap),
|
||||
onClick: PropTypes.func,
|
||||
onDoubleClick: PropTypes.func,
|
||||
onKeyEnter: PropTypes.func,
|
||||
selectedImdbId: PropTypes.string,
|
||||
loading: PropTypes.bool.isRequired,
|
||||
params: PropTypes.object.isRequired,
|
||||
exploreOptions: PropTypes.instanceOf(Map),
|
||||
type: PropTypes.string.isRequired,
|
||||
placeHolder: PropTypes.string.isRequired,
|
||||
updateFilter: PropTypes.func.isRequired,
|
||||
filter: PropTypes.string,
|
||||
};
|
||||
|
||||
class Posters extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
export default ListPosters;
|
||||
|
||||
move(event) {
|
||||
// Detect which direction to go
|
||||
const keyToDiff = Map({
|
||||
"ArrowRight": 1,
|
||||
"l": 1,
|
||||
"ArrowLeft": -1,
|
||||
"h": -1,
|
||||
"ArrowUp": -6,
|
||||
"k": -6,
|
||||
"ArrowDown": 6,
|
||||
"j": 6,
|
||||
});
|
||||
|
||||
if (event.key === "Enter") {
|
||||
this.props.onKeyEnter(this.props.selectedImdbId);
|
||||
}
|
||||
|
||||
if (! keyToDiff.has(event.key) ) {
|
||||
return;
|
||||
}
|
||||
var diff = keyToDiff.get(event.key);
|
||||
|
||||
// Don't scroll when changing poster
|
||||
event.preventDefault();
|
||||
|
||||
// Get the index of the currently selected item
|
||||
const idx = this.props.elmts.keySeq().findIndex(k => k === this.props.selectedImdbId);
|
||||
|
||||
var newIdx = idx + diff;
|
||||
|
||||
// Handle edge cases
|
||||
if (newIdx > this.props.elmts.size -1) {
|
||||
newIdx = this.props.elmts.size -1;
|
||||
} else if (newIdx < 0) {
|
||||
newIdx = 0;
|
||||
}
|
||||
|
||||
// Get the imdbID of the newly selected item
|
||||
var selectedImdb = Object.keys(this.props.elmts.toJS())[newIdx];
|
||||
|
||||
// Select the movie
|
||||
this.props.selectPoster(selectedImdb);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.loading) {
|
||||
const Posters = (props) => {
|
||||
if (props.loading) {
|
||||
return (<Loader />);
|
||||
}
|
||||
|
||||
if (this.props.elmts.size === 0) {
|
||||
if (props.elmts.size === 0) {
|
||||
return (
|
||||
<div className="jumbotron">
|
||||
<h2>No result</h2>
|
||||
@ -174,45 +93,53 @@ class Posters extends React.PureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
const addMoreCount = 20;
|
||||
const [size, setSize] = useState(0);
|
||||
useEffect(() => {
|
||||
loadMore()
|
||||
}, [props.elmts.size]);
|
||||
const hasMore = () => (size !== props.elmts.size);
|
||||
|
||||
const loadMore = () => {
|
||||
if (!hasMore()) { return }
|
||||
|
||||
const newSize = (((size + addMoreCount) >= props.elmts.size)
|
||||
? props.elmts.size
|
||||
: size + addMoreCount);
|
||||
|
||||
setSize(newSize);
|
||||
}
|
||||
|
||||
return (
|
||||
<div tabIndex="0"
|
||||
onKeyDown={(event) => this.move(event)}
|
||||
className="poster-list"
|
||||
>
|
||||
<InfiniteScroll
|
||||
hasMore={this.props.hasMore}
|
||||
loadMore={this.props.loadMore}
|
||||
className="row"
|
||||
className="poster-list d-block flex-row flex-wrap justify-content-around"
|
||||
dataLength={size}
|
||||
next={loadMore}
|
||||
hasMore={hasMore()}
|
||||
loader={<Loader />}
|
||||
>
|
||||
{this.props.elmts.toIndexedSeq().map(function(movie, index) {
|
||||
const imdbId = movie.get("imdb_id");
|
||||
const selected = (imdbId === this.props.selectedImdbId) ? true : false;
|
||||
|
||||
let clearFixes = [];
|
||||
if ((index % 6) === 0) { clearFixes.push("clearfix visible-lg") };
|
||||
if ((index % 4) === 0) { clearFixes.push("clearfix visible-md") };
|
||||
if ((index % 2) === 0) { clearFixes.push("clearfix visible-sm") };
|
||||
{props.elmts.slice(0, size).toIndexedSeq().map(function(el, index) {
|
||||
const imdbId = el.get("imdb_id");
|
||||
const selected = (imdbId === props.selectedImdbId) ? true : false;
|
||||
|
||||
return (
|
||||
<div key={imdbId}>
|
||||
{clearFixes.length > 0 && clearFixes.map(function(el, i) {
|
||||
return (
|
||||
<div key={`clearfix-${imdbId}-${i}`} className={el}></div>
|
||||
);
|
||||
})}
|
||||
<Poster
|
||||
data={movie}
|
||||
key={`poster-${imdbId}`}
|
||||
data={el}
|
||||
key={`poster-${imdbId}-${index}`}
|
||||
selected={selected}
|
||||
onClick={() => this.props.selectPoster(imdbId)}
|
||||
onDoubleClick={() => this.props.onDoubleClick(imdbId)}
|
||||
onClick={() => props.selectPoster(imdbId)}
|
||||
onDoubleClick={() => props.onDoubleClick(imdbId)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
} ,this)}
|
||||
</InfiniteScroll>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Posters.propTypes = {
|
||||
elmts: PropTypes.instanceOf(OrderedMap),
|
||||
selectedImdbId: PropTypes.string,
|
||||
loading: PropTypes.bool.isRequired,
|
||||
onDoubleClick: PropTypes.func,
|
||||
onKeyEnter: PropTypes.func,
|
||||
selectPoster: PropTypes.func,
|
||||
};
|
||||
|
@ -3,7 +3,7 @@ import Loading from "react-loading"
|
||||
|
||||
const Loader = () => (
|
||||
<div className="row" id="container">
|
||||
<div className="col-md-6 col-md-offset-3">
|
||||
<div className="col-12 col-md-6 offset-md-3">
|
||||
<Loading
|
||||
type="bars"
|
||||
height={"100%"}
|
||||
|
@ -3,6 +3,7 @@ import Loader from "../loader/loader"
|
||||
import PropTypes from "prop-types"
|
||||
import { Map, List } from "immutable"
|
||||
|
||||
// TODO: udpate this
|
||||
import { OverlayTrigger, Tooltip } from "react-bootstrap"
|
||||
|
||||
const Modules = (props) => {
|
||||
@ -11,8 +12,7 @@ const Modules = (props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Modules</h2>
|
||||
<div className="row">
|
||||
{props.modules && props.modules.keySeq().map((value, key) => (
|
||||
<ModulesByVideoType
|
||||
key={key}
|
||||
@ -33,14 +33,12 @@ const capitalize = (string) =>
|
||||
string.charAt(0).toUpperCase() + string.slice(1);
|
||||
|
||||
const ModulesByVideoType = (props) => (
|
||||
<div className="col-md-6 col-xs-12">
|
||||
<div className="panel panel-default">
|
||||
<div className="panel-heading">
|
||||
<h3 className="panel-title">
|
||||
{capitalize(props.videoType)} {/* Movie or Show */}
|
||||
</h3>
|
||||
<div className="col-12 col-md-6">
|
||||
<div className="card mb-3">
|
||||
<div className="card-header">
|
||||
<h3>{`${capitalize(props.videoType)} modules`}</h3>
|
||||
</div>
|
||||
<div className="panel-body">
|
||||
<div className="card-body">
|
||||
{props.data.keySeq().map((value, key) => (
|
||||
<ModuleByType
|
||||
key={key}
|
||||
@ -58,8 +56,8 @@ ModulesByVideoType.propTypes = {
|
||||
};
|
||||
|
||||
const ModuleByType = (props) => (
|
||||
<div className="col-md-3 col-xs-6">
|
||||
<h4>{props.type} {/* Detailer / Explorer / ... */}</h4>
|
||||
<div>
|
||||
<h4>{capitalize(props.type)}</h4>
|
||||
<table className="table">
|
||||
<tbody>
|
||||
{props.data.map(function(value, key) {
|
||||
@ -81,35 +79,37 @@ ModuleByType.propTypes = {
|
||||
};
|
||||
|
||||
const Module = (props) => {
|
||||
let iconClass, prettyStatus, labelClass;
|
||||
let iconClass, prettyStatus, badgeClass;
|
||||
const name = props.data.get("name");
|
||||
|
||||
switch(props.data.get("status")) {
|
||||
case "ok":
|
||||
iconClass = "fa fa-check-circle"
|
||||
labelClass = "label label-success"
|
||||
badgeClass = "badge badge-pill badge-success"
|
||||
prettyStatus = "OK"
|
||||
break;
|
||||
case "fail":
|
||||
iconClass = "fa fa-times-circle"
|
||||
labelClass = "label label-danger"
|
||||
badgeClass = "badge badge-pill badge-danger"
|
||||
prettyStatus = "Fail"
|
||||
break;
|
||||
case "not_implemented":
|
||||
iconClass = "fa fa-question-circle"
|
||||
labelClass = "label label-default"
|
||||
badgeClass = "badge badge-pill badge-default"
|
||||
prettyStatus = "Not implemented"
|
||||
break;
|
||||
default:
|
||||
iconClass = "fa fa-question-circle"
|
||||
labelClass = "label label-warning"
|
||||
badgeClass = "badge badge-pill badge-warning"
|
||||
prettyStatus = "Unknown"
|
||||
}
|
||||
|
||||
const tooltip = (
|
||||
<Tooltip id={`tooltip-status-${name}`}>
|
||||
<p><span className={labelClass}>Status: {prettyStatus}</span></p>
|
||||
<p><span className={badgeClass}>Status: {prettyStatus}</span></p>
|
||||
{props.data.get("error") !== "" &&
|
||||
<p>Error: {props.data.get("error")}</p>
|
||||
}
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@ -118,7 +118,7 @@ const Module = (props) => {
|
||||
<th>{name}</th>
|
||||
<td>
|
||||
<OverlayTrigger placement="right" overlay={tooltip}>
|
||||
<span className={labelClass}>
|
||||
<span className={badgeClass}>
|
||||
<i className={iconClass}></i> {prettyStatus}
|
||||
</span>
|
||||
</OverlayTrigger>
|
||||
|
@ -1,11 +1,16 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
import { WishlistButton, DeleteButton, RefreshButton } from "../buttons/actions"
|
||||
import { DropdownButton } from "react-bootstrap"
|
||||
import Dropdown from "react-bootstrap/Dropdown"
|
||||
|
||||
export default function ActionsButton(props) {
|
||||
return (
|
||||
<DropdownButton className="btn btn-default btn-sm" title="Actions" id="actions-button" dropup>
|
||||
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}
|
||||
@ -16,7 +21,6 @@ export default function ActionsButton(props) {
|
||||
resourceId={props.movieId}
|
||||
lastFetchUrl={props.lastFetchUrl}
|
||||
deleteFunc={props.deleteMovie}
|
||||
isUserAdmin={props.isUserAdmin}
|
||||
/>
|
||||
}
|
||||
<WishlistButton
|
||||
@ -25,6 +29,19 @@ export default function ActionsButton(props) {
|
||||
addToWishlist={props.addToWishlist}
|
||||
deleteFromWishlist={props.deleteFromWishlist}
|
||||
/>
|
||||
</DropdownButton>
|
||||
</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;
|
||||
|
64
frontend/js/components/movies/buttons.js
Normal file
64
frontend/js/components/movies/buttons.js
Normal file
@ -0,0 +1,64 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { Map } from "immutable"
|
||||
|
||||
import DownloadButton from "../buttons/download"
|
||||
import SubtitlesButton from "../buttons/subtitles"
|
||||
import ImdbButton from "../buttons/imdb"
|
||||
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}
|
||||
/>
|
||||
|
||||
<DownloadButton
|
||||
name={props.movie.get("title")}
|
||||
url={props.movie.get("polochon_url")}
|
||||
subtitles={props.movie.get("subtitles")}
|
||||
/>
|
||||
|
||||
{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"
|
||||
/>
|
||||
}
|
||||
|
||||
<ImdbButton imdbId={props.movie.get("imdb_id")} size="sm"/>
|
||||
</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;
|
@ -1,17 +1,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 DownloadButton from "../buttons/download"
|
||||
import SubtitlesButton from "../buttons/subtitles"
|
||||
import ImdbButton from "../buttons/imdb"
|
||||
import TorrentsButton from "./torrents"
|
||||
import ActionsButton from "./actions"
|
||||
import ListPosters from "../list/posters"
|
||||
import ListDetails from "../list/details"
|
||||
import MovieButtons from "./buttons"
|
||||
|
||||
import Row from "react-bootstrap/Row"
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
@ -29,90 +29,60 @@ const mapDispatchToProps = {
|
||||
refreshSubtitles, updateFilter,
|
||||
};
|
||||
|
||||
function MovieButtons(props) {
|
||||
const hasMovie = (props.movie.get("polochon_url") !== "");
|
||||
return (
|
||||
<div className="list-details-buttons btn-toolbar">
|
||||
<ActionsButton
|
||||
fetching={props.movie.get("fetchingDetails")}
|
||||
movieId={props.movie.get("imdb_id")}
|
||||
getDetails={props.getMovieDetails}
|
||||
deleteMovie={props.deleteMovie}
|
||||
hasMovie={hasMovie}
|
||||
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}
|
||||
/>
|
||||
|
||||
<DownloadButton
|
||||
url={props.movie.get("polochon_url")}
|
||||
subtitles={props.movie.get("subtitles")}
|
||||
/>
|
||||
|
||||
{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"
|
||||
/>
|
||||
}
|
||||
|
||||
<ImdbButton imdbId={props.movie.get("imdb_id")} size="sm"/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
class MovieList extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
render() {
|
||||
const MovieList = (props) => {
|
||||
let selectedMovie = undefined;
|
||||
if (this.props.movies !== undefined && this.props.movies.has(this.props.selectedImdbId)) {
|
||||
selectedMovie = this.props.movies.get(this.props.selectedImdbId);
|
||||
if (props.movies !== undefined &&
|
||||
props.movies.has(props.selectedImdbId)) {
|
||||
selectedMovie = props.movies.get(props.selectedImdbId);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="row" id="container">
|
||||
<Row>
|
||||
<ListPosters
|
||||
data={this.props.movies}
|
||||
data={props.movies}
|
||||
type="movies"
|
||||
placeHolder="Filter movies..."
|
||||
exploreOptions={this.props.exploreOptions}
|
||||
selectedImdbId={this.props.selectedImdbId}
|
||||
updateFilter={this.props.updateFilter}
|
||||
filter={this.props.filter}
|
||||
onClick={this.props.selectMovie}
|
||||
exploreOptions={props.exploreOptions}
|
||||
selectedImdbId={props.selectedImdbId}
|
||||
updateFilter={props.updateFilter}
|
||||
filter={props.filter}
|
||||
onClick={props.selectMovie}
|
||||
onDoubleClick={function() { return; }}
|
||||
onKeyEnter={function() { return; }}
|
||||
params={this.props.match.params}
|
||||
loading={this.props.loading}
|
||||
params={props.match.params}
|
||||
loading={props.loading}
|
||||
/>
|
||||
{selectedMovie !== undefined &&
|
||||
<ListDetails data={selectedMovie}>
|
||||
<ListDetails data={selectedMovie} loading={props.loading}>
|
||||
<MovieButtons
|
||||
movie={selectedMovie}
|
||||
getMovieDetails={this.props.getMovieDetails}
|
||||
addTorrent={this.props.addTorrent}
|
||||
deleteMovie={this.props.deleteMovie}
|
||||
addToWishlist={this.props.addMovieToWishlist}
|
||||
deleteFromWishlist={this.props.deleteMovieFromWishlist}
|
||||
lastFetchUrl={this.props.lastFetchUrl}
|
||||
refreshSubtitles={this.props.refreshSubtitles}
|
||||
getMovieDetails={props.getMovieDetails}
|
||||
addTorrent={props.addTorrent}
|
||||
deleteMovie={props.deleteMovie}
|
||||
addToWishlist={props.addMovieToWishlist}
|
||||
deleteFromWishlist={props.deleteMovieFromWishlist}
|
||||
lastFetchUrl={props.lastFetchUrl}
|
||||
refreshSubtitles={props.refreshSubtitles}
|
||||
/>
|
||||
</ListDetails>
|
||||
}
|
||||
</div>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
}
|
||||
MovieList.propTypes = {
|
||||
movies: PropTypes.instanceOf(OrderedMap),
|
||||
exploreOptions: PropTypes.instanceOf(Map),
|
||||
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,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(MovieList);
|
||||
|
@ -1,55 +1,11 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { List } from "immutable"
|
||||
|
||||
import { DropdownButton, MenuItem } from "react-bootstrap"
|
||||
|
||||
export default class TorrentsButton extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
}
|
||||
handleClick(e, url) {
|
||||
e.preventDefault();
|
||||
this.props.addTorrent(url);
|
||||
}
|
||||
render() {
|
||||
const entries = buildMenuItems(this.props.torrents);
|
||||
const searchUrl = `#/torrents/search/movies/${encodeURI(this.props.movieTitle)}`;
|
||||
return (
|
||||
<DropdownButton className="btn btn-default btn-sm" title="Torrents" id="download-torrents-button" dropup>
|
||||
<MenuItem className="text-warning" header>Advanced</MenuItem>
|
||||
<MenuItem href={searchUrl} >
|
||||
<i className="fa fa-search" aria-hidden="true"></i> Search
|
||||
</MenuItem>
|
||||
{entries.length > 0 &&
|
||||
<MenuItem divider></MenuItem>
|
||||
}
|
||||
{entries.map(function(e, index) {
|
||||
switch (e.type) {
|
||||
case "header":
|
||||
return (
|
||||
<MenuItem key={index} className="text-warning" header>
|
||||
{e.value}
|
||||
</MenuItem>
|
||||
);
|
||||
case "divider":
|
||||
return (
|
||||
<MenuItem key={index} divider></MenuItem>
|
||||
);
|
||||
case "entry":
|
||||
return (
|
||||
<MenuItem key={index} href={e.url} onClick={(event) => this.handleClick(event, e.url)}>
|
||||
{e.quality}
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
}, this)}
|
||||
</DropdownButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
import Dropdown from "react-bootstrap/Dropdown"
|
||||
|
||||
function buildMenuItems(torrents) {
|
||||
if (!torrents) {
|
||||
if (!torrents || torrents.size === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@ -83,3 +39,54 @@ function buildMenuItems(torrents) {
|
||||
|
||||
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;
|
||||
|
@ -1,10 +1,13 @@
|
||||
import React, { useState } from "react"
|
||||
import { Route, Link } from "react-router-dom"
|
||||
import { Route } from "react-router-dom"
|
||||
import { connect } from "react-redux"
|
||||
import { Nav, Navbar, NavItem, NavDropdown, MenuItem } from "react-bootstrap"
|
||||
import { LinkContainer } from "react-router-bootstrap"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
import Nav from "react-bootstrap/Nav"
|
||||
import Navbar from "react-bootstrap/Navbar"
|
||||
import NavDropdown from "react-bootstrap/NavDropdown"
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
let torrentCount = 0;
|
||||
if (state.torrentStore.has("torrents") && state.torrentStore.get("torrents") !== undefined) {
|
||||
@ -21,24 +24,20 @@ const AppNavBar = (props) => {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
<Navbar
|
||||
fluid fixedTop collapseOnSelect
|
||||
expanded={expanded} onToggle={() => setExpanded(!expanded)}>
|
||||
<Navbar.Header>
|
||||
<Navbar fixed="top" collapseOnSelect bg="dark" variant="dark"
|
||||
expanded={expanded} expand="sm" onToggle={() => setExpanded(!expanded)}>
|
||||
<LinkContainer to="/">
|
||||
<Navbar.Brand><Link to="/">Canapé</Link></Navbar.Brand>
|
||||
<Navbar.Brand>Canapé</Navbar.Brand>
|
||||
</LinkContainer>
|
||||
<Navbar.Toggle />
|
||||
</Navbar.Header>
|
||||
<Navbar.Collapse>
|
||||
<Nav className="mr-auto">
|
||||
<MoviesDropdown />
|
||||
<ShowsDropdown />
|
||||
<WishlistDropdown />
|
||||
<TorrentsDropdown torrentsCount={props.torrentCount} />
|
||||
<UserDropdown
|
||||
username={props.username}
|
||||
isAdmin={props.isAdmin}
|
||||
/>
|
||||
</Nav>
|
||||
<Nav>
|
||||
<Route path="/movies" render={(props) =>
|
||||
<Search
|
||||
placeholder="Search movies"
|
||||
@ -53,9 +52,14 @@ const AppNavBar = (props) => {
|
||||
history={props.history}
|
||||
/>
|
||||
}/>
|
||||
<UserDropdown
|
||||
username={props.username}
|
||||
isAdmin={props.isAdmin}
|
||||
/>
|
||||
</Nav>
|
||||
</Navbar.Collapse>
|
||||
</Navbar>
|
||||
)
|
||||
);
|
||||
}
|
||||
AppNavBar.propTypes = {
|
||||
torrentCount: PropTypes.number.isRequired,
|
||||
@ -94,84 +98,74 @@ Search.propTypes = {
|
||||
};
|
||||
|
||||
const MoviesDropdown = () => (
|
||||
<Nav>
|
||||
<NavDropdown title="Movies" id="navbar-movies-dropdown">
|
||||
<LinkContainer to="/movies/explore/yts/seeds">
|
||||
<MenuItem>Discover</MenuItem>
|
||||
<NavDropdown.Item>Discover</NavDropdown.Item>
|
||||
</LinkContainer>
|
||||
<LinkContainer to="/movies/polochon">
|
||||
<MenuItem>My movies</MenuItem>
|
||||
<NavDropdown.Item>My movies</NavDropdown.Item>
|
||||
</LinkContainer>
|
||||
</NavDropdown>
|
||||
)
|
||||
|
||||
const ShowsDropdown = () => (
|
||||
<NavDropdown title="Shows" id="navbar-shows-dropdown">
|
||||
<LinkContainer to="/shows/explore/eztv/rating">
|
||||
<NavDropdown.Item>Discover</NavDropdown.Item>
|
||||
</LinkContainer>
|
||||
<LinkContainer to="/shows/polochon">
|
||||
<NavDropdown.Item>My shows</NavDropdown.Item>
|
||||
</LinkContainer>
|
||||
</NavDropdown>
|
||||
);
|
||||
|
||||
const UserDropdown = (props) => (
|
||||
<Nav>
|
||||
<NavDropdown title={props.username} alignRight>
|
||||
{props.isAdmin &&
|
||||
<LinkContainer to="/admin">
|
||||
<NavDropdown.Item>Admin Panel</NavDropdown.Item>
|
||||
</LinkContainer>
|
||||
}
|
||||
<LinkContainer to="/users/profile">
|
||||
<NavDropdown.Item>Profile</NavDropdown.Item>
|
||||
</LinkContainer>
|
||||
<LinkContainer to="/users/tokens">
|
||||
<NavDropdown.Item>Tokens</NavDropdown.Item>
|
||||
</LinkContainer>
|
||||
<LinkContainer to="/users/logout">
|
||||
<NavDropdown.Item>Logout</NavDropdown.Item>
|
||||
</LinkContainer>
|
||||
</NavDropdown>
|
||||
</Nav>
|
||||
)
|
||||
|
||||
const ShowsDropdown = () => (
|
||||
<Nav>
|
||||
<NavDropdown title="Shows" id="navbar-shows-dropdown">
|
||||
<LinkContainer to="/shows/explore/eztv/rating">
|
||||
<NavItem>Discover</NavItem>
|
||||
</LinkContainer>
|
||||
<LinkContainer to="/shows/polochon">
|
||||
<NavItem>My shows</NavItem>
|
||||
</LinkContainer>
|
||||
</NavDropdown>
|
||||
</Nav>
|
||||
);
|
||||
|
||||
function UserDropdown(props) {
|
||||
return (
|
||||
<Nav pullRight>
|
||||
<NavDropdown title={props.username} id="navbar-dropdown-right">
|
||||
{props.isAdmin &&
|
||||
<LinkContainer to="/admin">
|
||||
<MenuItem>Admin Panel</MenuItem>
|
||||
</LinkContainer>
|
||||
}
|
||||
<LinkContainer to="/users/profile">
|
||||
<MenuItem>Profile</MenuItem>
|
||||
</LinkContainer>
|
||||
<LinkContainer to="/users/tokens">
|
||||
<MenuItem>Tokens</MenuItem>
|
||||
</LinkContainer>
|
||||
<LinkContainer to="/users/logout">
|
||||
<MenuItem>Logout</MenuItem>
|
||||
</LinkContainer>
|
||||
</NavDropdown>
|
||||
</Nav>
|
||||
);
|
||||
}
|
||||
UserDropdown.propTypes = {
|
||||
username: PropTypes.string.isRequired,
|
||||
isAdmin: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
const WishlistDropdown = () => (
|
||||
<Nav>
|
||||
<NavDropdown title="Wishlist" id="navbar-wishlit-dropdown">
|
||||
<LinkContainer to="/movies/wishlist">
|
||||
<MenuItem>Movies</MenuItem>
|
||||
<NavDropdown.Item>Movies</NavDropdown.Item>
|
||||
</LinkContainer>
|
||||
<LinkContainer to="/shows/wishlist">
|
||||
<MenuItem>Shows</MenuItem>
|
||||
<NavDropdown.Item>Shows</NavDropdown.Item>
|
||||
</LinkContainer>
|
||||
</NavDropdown>
|
||||
</Nav>
|
||||
);
|
||||
|
||||
const TorrentsDropdown = (props) => {
|
||||
const title = (<TorrentsDropdownTitle torrentsCount={props.torrentsCount} />)
|
||||
return(
|
||||
<Nav>
|
||||
<NavDropdown title={title} id="navbar-wishlit-dropdown">
|
||||
<LinkContainer to="/torrents/list">
|
||||
<MenuItem>Downloads</MenuItem>
|
||||
<NavDropdown.Item>Downloads</NavDropdown.Item>
|
||||
</LinkContainer>
|
||||
<LinkContainer to="/torrents/search">
|
||||
<MenuItem>Search</MenuItem>
|
||||
<NavDropdown.Item>Search</NavDropdown.Item>
|
||||
</LinkContainer>
|
||||
</NavDropdown>
|
||||
</Nav>
|
||||
);
|
||||
}
|
||||
TorrentsDropdown.propTypes = { torrentsCount: PropTypes.number.isRequired };
|
||||
@ -186,7 +180,7 @@ const TorrentsDropdownTitle = (props) => {
|
||||
return (
|
||||
<span> Torrents
|
||||
<span>
|
||||
<span className="label label-info">{props.torrentsCount}</span>
|
||||
<span className="badge badge-info badge-pill">{props.torrentsCount}</span>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
|
@ -1,5 +1,8 @@
|
||||
import React from "react"
|
||||
import React, { useState } from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { Map } from "immutable"
|
||||
import { connect } from "react-redux"
|
||||
import { withRouter } from "react-router"
|
||||
import { addTorrent } from "../../actions/torrents"
|
||||
import { refreshSubtitles } from "../../actions/subtitles"
|
||||
import { addShowToWishlist, deleteShowFromWishlist, getEpisodeDetails, fetchShowDetails } from "../../actions/shows"
|
||||
@ -10,12 +13,17 @@ import SubtitlesButton from "../buttons/subtitles"
|
||||
import ImdbButton from "../buttons/imdb"
|
||||
import RefreshIndicator from "../buttons/refresh"
|
||||
|
||||
import { OverlayTrigger, Tooltip } from "react-bootstrap"
|
||||
import { Button, Dropdown, MenuItem } from "react-bootstrap"
|
||||
import Tooltip from "react-bootstrap/Tooltip"
|
||||
import OverlayTrigger from "react-bootstrap/OverlayTrigger"
|
||||
import Dropdown from "react-bootstrap/Dropdown"
|
||||
import SplitButton from "react-bootstrap/SplitButton"
|
||||
|
||||
// Helper to change 1 to 01
|
||||
const pad = (d) => (d < 10) ? "0" + d.toString() : d.toString();
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
loading: state.showStore.loading,
|
||||
loading: state.showStore.get("loading"),
|
||||
show: state.showStore.get("show"),
|
||||
};
|
||||
}
|
||||
@ -24,188 +32,164 @@ const mapDispatchToProps = {
|
||||
fetchShowDetails, getEpisodeDetails, refreshSubtitles,
|
||||
};
|
||||
|
||||
class ShowDetails extends React.Component {
|
||||
render() {
|
||||
// Loading
|
||||
if (this.props.loading) {
|
||||
const ShowDetails = (props) => {
|
||||
if (props.loading) {
|
||||
return (<Loader />);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="row" id="container">
|
||||
<div className="row no-gutters">
|
||||
<Header
|
||||
data={this.props.show}
|
||||
addToWishlist={this.props.addShowToWishlist}
|
||||
deleteFromWishlist={this.props.deleteShowFromWishlist}
|
||||
data={props.show}
|
||||
addToWishlist={props.addShowToWishlist}
|
||||
deleteFromWishlist={props.deleteShowFromWishlist}
|
||||
/>
|
||||
<SeasonsList
|
||||
data={this.props.show}
|
||||
router={this.props.router}
|
||||
addTorrent={this.props.addTorrent}
|
||||
addToWishlist={this.props.addShowToWishlist}
|
||||
getEpisodeDetails={this.props.getEpisodeDetails}
|
||||
refreshSubtitles={this.props.refreshSubtitles}
|
||||
data={props.show}
|
||||
addTorrent={props.addTorrent}
|
||||
addToWishlist={props.addShowToWishlist}
|
||||
getEpisodeDetails={props.getEpisodeDetails}
|
||||
refreshSubtitles={props.refreshSubtitles}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
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);
|
||||
|
||||
function Header(props){
|
||||
return (
|
||||
<div className="col-xs-12 col-sm-10 col-sm-offset-1 col-md-10 col-md-offset-1">
|
||||
<div className="panel panel-default">
|
||||
<div className="panel-body">
|
||||
<HeaderThumbnail data={props.data} />
|
||||
<HeaderDetails
|
||||
data={props.data}
|
||||
addToWishlist={props.addToWishlist}
|
||||
deleteFromWishlist={props.deleteFromWishlist}
|
||||
/>
|
||||
const Header = (props) => (
|
||||
<div className="card col-12 col-md-10 offset-md-1 mb-3">
|
||||
<div className="row no-gutters">
|
||||
<div className="col-4">
|
||||
<img className="img-fluid" src={props.data.get("poster_url")} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HeaderThumbnail(props){
|
||||
return (
|
||||
<div className="col-xs-12 col-sm-2 text-center">
|
||||
<img src={props.data.get("poster_url")}
|
||||
className="show-thumbnail thumbnail-selected img-thumbnail img-responsive"/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HeaderDetails(props){
|
||||
return (
|
||||
<div className="col-xs-12 col-sm-10">
|
||||
<dl className="dl-horizontal">
|
||||
<dt>Title</dt>
|
||||
<dd>{props.data.get("title")}</dd>
|
||||
<dt>Plot</dt>
|
||||
<dd className="plot">{props.data.get("plot")}</dd>
|
||||
<dt>IMDB</dt>
|
||||
<dd>
|
||||
<ImdbButton imdbId={props.data.get("imdb_id")} size="xs"/>
|
||||
</dd>
|
||||
<dt>Year</dt>
|
||||
<dd>{props.data.get("year")}</dd>
|
||||
<dt>Rating</dt>
|
||||
<dd>{props.data.get("rating")}</dd>
|
||||
</dl>
|
||||
<div className="col-8">
|
||||
<div className="card-body">
|
||||
<h5 className="card-title">{props.data.get("title")}</h5>
|
||||
<p className="card-text">{props.data.get("year")}</p>
|
||||
<p className="card-text">{props.data.get("rating")}</p>
|
||||
<p className="card-text">{props.data.get("plot")}</p>
|
||||
<p className="card-text">
|
||||
<ImdbButton imdbId={props.data.get("imdb_id")} xs/>
|
||||
</p>
|
||||
<TrackHeader
|
||||
data={props.data}
|
||||
addToWishlist={props.addToWishlist}
|
||||
deleteFromWishlist={props.deleteFromWishlist}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Header.propTypes = {
|
||||
data: PropTypes.instanceOf(Map),
|
||||
deleteFromWishlist: PropTypes.func,
|
||||
addToWishlist: PropTypes.func,
|
||||
};
|
||||
|
||||
function SeasonsList(props){
|
||||
return (
|
||||
<div>
|
||||
const SeasonsList = (props) => (
|
||||
<div className="col col-12 col-md-10 offset-md-1">
|
||||
{props.data.get("seasons").entrySeq().map(function([season, data]) {
|
||||
return (
|
||||
|
||||
<div className="col-xs-12 col-sm-10 col-sm-offset-1 col-md-10 col-md-offset-1" key={season.toString()}>
|
||||
<Season
|
||||
key={`season-list-key-${season}`}
|
||||
data={data}
|
||||
season={season}
|
||||
showName={props.data.get("title")}
|
||||
router={props.router}
|
||||
addTorrent={props.addTorrent}
|
||||
addToWishlist={props.addToWishlist}
|
||||
getEpisodeDetails={props.getEpisodeDetails}
|
||||
refreshSubtitles={props.refreshSubtitles}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</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"
|
||||
|
||||
class Season extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
this.state = { colapsed: true };
|
||||
}
|
||||
handleClick(e) {
|
||||
e.preventDefault();
|
||||
this.setState({ colapsed: !this.state.colapsed });
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div className="panel panel-default">
|
||||
<div className="panel-heading clickable" onClick={(e) => this.handleClick(e)}>
|
||||
Season {this.props.season}
|
||||
<small className="text-primary"> — ({this.props.data.toList().size} episodes)</small>
|
||||
<span className="pull-right">
|
||||
{this.state.colapsed ||
|
||||
<i className="fa fa-chevron-down"></i>
|
||||
}
|
||||
{this.state.colapsed &&
|
||||
<i className="fa fa-chevron-left"></i>
|
||||
}
|
||||
</span>
|
||||
<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>
|
||||
{this.state.colapsed ||
|
||||
<table className="table table-striped table-hover">
|
||||
<tbody>
|
||||
{this.props.data.toList().map(function(episode) {
|
||||
<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={this.props.showName}
|
||||
router={this.props.router}
|
||||
addTorrent={this.props.addTorrent}
|
||||
addToWishlist={this.props.addToWishlist}
|
||||
getEpisodeDetails={this.props.getEpisodeDetails}
|
||||
refreshSubtitles={this.props.refreshSubtitles}
|
||||
showName={props.showName}
|
||||
addTorrent={props.addTorrent}
|
||||
addToWishlist={props.addToWishlist}
|
||||
getEpisodeDetails={props.getEpisodeDetails}
|
||||
refreshSubtitles={props.refreshSubtitles}
|
||||
/>
|
||||
)
|
||||
}, this)}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function PolochonMetadata(props) {
|
||||
if (!props.quality || props.quality === "") {
|
||||
return null;
|
||||
}
|
||||
Season.propTypes = {
|
||||
data: PropTypes.instanceOf(Map),
|
||||
season: PropTypes.number,
|
||||
showName: PropTypes.string,
|
||||
addToWishlist: PropTypes.func,
|
||||
addTorrent: PropTypes.func,
|
||||
refreshSubtitles: PropTypes.func,
|
||||
getEpisodeDetails: PropTypes.func,
|
||||
};
|
||||
|
||||
const PolochonMetadata = (props) => {
|
||||
if (!props.quality || props.quality === "") { return null }
|
||||
return (
|
||||
<span>
|
||||
<span className="badge badge-pill badge-secondary">{props.quality}</span>
|
||||
<span className="badge badge-pill badge-secondary">{props.container} </span>
|
||||
<span className="badge badge-pill badge-secondary">{props.videoCodec}</span>
|
||||
<span className="badge badge-pill badge-secondary">{props.audioCodec}</span>
|
||||
<span className="badge badge-pill badge-secondary">{props.releaseGroup}</span>
|
||||
<span className="my-2 d-flex d-wrap">
|
||||
<span className="badge badge-pill badge-light mx-1">{props.quality}</span>
|
||||
<span className="badge badge-pill badge-light mx-1">{props.container} </span>
|
||||
<span className="badge badge-pill badge-light mx-1">{props.videoCodec}</span>
|
||||
<span className="badge badge-pill badge-light mx-1">{props.audioCodec}</span>
|
||||
<span className="badge badge-pill badge-light mx-1">{props.releaseGroup}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
PolochonMetadata.propTypes = {
|
||||
quality: PropTypes.string,
|
||||
container: PropTypes.string,
|
||||
videoCodec: PropTypes.string,
|
||||
audioCodec: PropTypes.string,
|
||||
releaseGroup: PropTypes.string,
|
||||
};
|
||||
|
||||
function Episode(props) {
|
||||
return (
|
||||
<tr>
|
||||
<th scope="row" className="col-xs-2">
|
||||
<TrackButton
|
||||
data={props.data}
|
||||
addToWishlist={props.addToWishlist}
|
||||
/>
|
||||
{props.data.get("episode")}
|
||||
</th>
|
||||
<td className="col-xs-12">
|
||||
{props.data.get("title")}
|
||||
const Episode = (props) => (
|
||||
<div className="d-flex flex-wrap flex-md-nowrap align-items-center">
|
||||
<TrackButton data={props.data} addToWishlist={props.addToWishlist} />
|
||||
<span className="mx-2 text">{props.data.get("episode")}</span>
|
||||
<span className="mx-2 text text-truncate flex-fill">{props.data.get("title")}</span>
|
||||
<PolochonMetadata
|
||||
quality={props.data.get("quality")}
|
||||
releaseGroup={props.data.get("release_group")}
|
||||
@ -213,7 +197,7 @@ function Episode(props) {
|
||||
audioCodec={props.data.get("audio_codec")}
|
||||
videoCodec={props.data.get("video_codec")}
|
||||
/>
|
||||
<span className="pull-right episode-buttons">
|
||||
<div className="align-self-end btn-toolbar">
|
||||
{props.data.get("polochon_url") !== "" &&
|
||||
<SubtitlesButton
|
||||
fetching={props.data.get("fetchingSubtitles")}
|
||||
@ -237,168 +221,147 @@ function Episode(props) {
|
||||
)
|
||||
})}
|
||||
<DownloadButton
|
||||
name={`${props.showName} - S${pad(props.data.get("season"))}E${pad(props.data.get("episode"))}`}
|
||||
url={props.data.get("polochon_url")}
|
||||
subtitles={props.data.get("subtitles")}
|
||||
xs
|
||||
/>
|
||||
<GetDetailsButton
|
||||
<GetDetailsButtonWithRouter
|
||||
showName={props.showName}
|
||||
router={props.router}
|
||||
data={props.data}
|
||||
getEpisodeDetails={props.getEpisodeDetails}
|
||||
/>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</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,
|
||||
};
|
||||
|
||||
class Torrent extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
}
|
||||
handleClick(e, url) {
|
||||
e.preventDefault();
|
||||
this.props.addTorrent(url);
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<span>
|
||||
<a type="button"
|
||||
className="btn btn-primary btn-xs"
|
||||
onClick={(e) => this.handleClick(e, this.props.data.get("url"))}
|
||||
href={this.props.data.url} >
|
||||
<i className="fa fa-download"></i> {this.props.data.get("quality")}
|
||||
</a>
|
||||
</span>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
class TrackHeader extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
}
|
||||
handleClick(e) {
|
||||
e.preventDefault();
|
||||
const trackedSeason = this.props.data.get("tracked_season");
|
||||
const trackedEpisode = this.props.data.get("tracked_episode");
|
||||
const imdbId = this.props.data.get("imdb_id");
|
||||
const wishlisted = (trackedSeason !== null && trackedEpisode !== null);
|
||||
if (wishlisted) {
|
||||
this.props.deleteFromWishlist(imdbId);
|
||||
} else {
|
||||
this.props.addToWishlist(imdbId);
|
||||
}
|
||||
}
|
||||
render() {
|
||||
const trackedSeason = this.props.data.get("tracked_season");
|
||||
const trackedEpisode = this.props.data.get("tracked_episode");
|
||||
const wishlisted = (trackedSeason !== null && trackedEpisode !== null);
|
||||
let msg;
|
||||
if (wishlisted) {
|
||||
if (trackedSeason !== 0 && trackedEpisode !== 0) {
|
||||
msg = (
|
||||
<dd>Show tracked from <strong>season {trackedSeason} episode {trackedEpisode}</strong></dd>
|
||||
);
|
||||
} else {
|
||||
msg = (
|
||||
<dd>Whole show tracked</dd>
|
||||
);
|
||||
}
|
||||
const msg = (trackedSeason !== 0 && trackedEpisode !== 0)
|
||||
? (<p>Show tracked from <strong>season {trackedSeason} episode {trackedEpisode}</strong></p>)
|
||||
: (<p>Whole show tracked</p>);
|
||||
|
||||
return (
|
||||
<dl className="dl-horizontal">
|
||||
<dt>Tracking active</dt>
|
||||
<span className="card-text">
|
||||
{msg}
|
||||
<dt></dt>
|
||||
<dd>
|
||||
<a className="btn btn-xs btn-danger" onClick={(e) => this.handleClick(e)}>
|
||||
<a className="btn btn-sm btn-danger" onClick={handleClick}>
|
||||
<i className="fa fa-bookmark"></i> Untrack the show
|
||||
</a>
|
||||
</dd>
|
||||
</dl>
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<dl className="dl-horizontal">
|
||||
<dt>Tracking inactive</dt>
|
||||
<dd>
|
||||
<a className="btn btn-xs btn-info" onClick={(e) => this.handleClick(e)}>
|
||||
<i className="fa fa-bookmark-o"></i> Track the whole show
|
||||
</a>
|
||||
</dd>
|
||||
</dl>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TrackButton extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
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>
|
||||
);
|
||||
}
|
||||
handleClick(e) {
|
||||
e.preventDefault();
|
||||
const imdbId = this.props.data.get("show_imdb_id");
|
||||
const season = this.props.data.get("season");
|
||||
const episode = this.props.data.get("episode");
|
||||
this.props.addToWishlist(imdbId, season, episode);
|
||||
}
|
||||
render() {
|
||||
const tooltipId = `tooltip-${this.props.data.season}-${this.props.data.episode}`;
|
||||
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}>
|
||||
<a type="button" className="btn btn-default btn-xs" onClick={(e) => this.handleClick(e)}>
|
||||
<span className="btn clickable"
|
||||
onClick={() => props.addToWishlist(imdbId, season, episode)}>
|
||||
<i className="fa fa-bookmark"></i>
|
||||
</a>
|
||||
</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);
|
||||
}
|
||||
|
||||
class GetDetailsButton extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleFetchClick = this.handleFetchClick.bind(this);
|
||||
this.handleAdvanceTorrentSearchClick = this.handleAdvanceTorrentSearchClick.bind(this);
|
||||
this.state = {
|
||||
imdbId: this.props.data.get("show_imdb_id"),
|
||||
season: this.props.data.get("season"),
|
||||
episode: this.props.data.get("episode"),
|
||||
};
|
||||
}
|
||||
handleFetchClick() {
|
||||
if (this.props.data.get("fetching")) { return }
|
||||
this.props.getEpisodeDetails(this.state.imdbId, this.state.season, this.state.episode);
|
||||
}
|
||||
handleAdvanceTorrentSearchClick() {
|
||||
const pad = (d) => (d < 10) ? "0" + d.toString() : d.toString();
|
||||
const search = `${this.props.showName} S${pad(this.state.season)}E${pad(this.state.episode)}`;
|
||||
const handleAdvanceTorrentSearchClick = () => {
|
||||
const search = `${props.showName} S${pad(season)}E${pad(episode)}`;
|
||||
const url = `/torrents/search/shows/${encodeURI(search)}`;
|
||||
this.props.router.push(url);
|
||||
props.history.push(url);
|
||||
}
|
||||
render() {
|
||||
const id = `${this.state.imdbId}-${this.state.season}-${this.state.episode}-refresh-dropdown`;
|
||||
|
||||
return (
|
||||
<Dropdown id={id} dropup>
|
||||
<Button className="btn-xs" bsStyle="info" onClick={this.handleFetchClick}>
|
||||
<RefreshIndicator refresh={this.props.data.get("fetching")} />
|
||||
</Button>
|
||||
<Dropdown.Toggle className="btn-xs" bsStyle="info"/>
|
||||
<Dropdown.Menu>
|
||||
<MenuItem onClick={this.handleAdvanceTorrentSearchClick}>
|
||||
<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>
|
||||
</MenuItem>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</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);
|
||||
|
@ -1,4 +1,6 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { Map } from "immutable"
|
||||
import { connect } from "react-redux"
|
||||
import { selectShow, addShowToWishlist,
|
||||
deleteShowFromWishlist, getShowDetails, updateFilter } from "../../actions/shows"
|
||||
@ -13,7 +15,6 @@ function mapStateToProps(state) {
|
||||
shows : state.showsStore.get("shows"),
|
||||
filter : state.showsStore.get("filter"),
|
||||
selectedImdbId : state.showsStore.get("selectedImdbId"),
|
||||
lastFetchUrl : state.showsStore.get("lastFetchUrl"),
|
||||
exploreOptions : state.showsStore.get("exploreOptions"),
|
||||
};
|
||||
}
|
||||
@ -22,51 +23,58 @@ const mapDispatchToProps = {
|
||||
getShowDetails, updateFilter,
|
||||
};
|
||||
|
||||
class ShowList extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.showDetails = this.showDetails.bind(this);
|
||||
const ShowList = (props) => {
|
||||
const showDetails = (imdbId) => {
|
||||
props.history.push("/shows/details/" + imdbId);
|
||||
}
|
||||
|
||||
showDetails(imdbId) {
|
||||
return this.props.history.push("/shows/details/" + imdbId);
|
||||
}
|
||||
|
||||
render() {
|
||||
let selectedShow;
|
||||
if (this.props.selectedImdbId !== "") {
|
||||
selectedShow = this.props.shows.get(this.props.selectedImdbId);
|
||||
if (props.selectedImdbId !== "") {
|
||||
selectedShow = props.shows.get(props.selectedImdbId);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="row" id="container">
|
||||
<ListPosters
|
||||
data={this.props.shows}
|
||||
data={props.shows}
|
||||
type="shows"
|
||||
placeHolder="Filter shows..."
|
||||
exploreOptions={this.props.exploreOptions}
|
||||
updateFilter={this.props.updateFilter}
|
||||
selectedImdbId={this.props.selectedImdbId}
|
||||
filter={this.props.filter}
|
||||
onClick={this.props.selectShow}
|
||||
onDoubleClick={this.showDetails}
|
||||
onKeyEnter={this.showDetails}
|
||||
params={this.props.match.params}
|
||||
loading={this.props.loading}
|
||||
exploreOptions={props.exploreOptions}
|
||||
updateFilter={props.updateFilter}
|
||||
selectedImdbId={props.selectedImdbId}
|
||||
filter={props.filter}
|
||||
onClick={props.selectShow}
|
||||
onDoubleClick={showDetails}
|
||||
onKeyEnter={showDetails}
|
||||
params={props.match.params}
|
||||
loading={props.loading}
|
||||
/>
|
||||
{selectedShow &&
|
||||
<ListDetails data={selectedShow} >
|
||||
<ListDetails data={selectedShow} loading={props.loading}>
|
||||
<ShowButtons
|
||||
show={selectedShow}
|
||||
deleteFromWishlist={this.props.deleteShowFromWishlist}
|
||||
addToWishlist={this.props.addShowToWishlist}
|
||||
getDetails={this.props.getShowDetails}
|
||||
updateFilter={this.props.updateFilter}
|
||||
deleteFromWishlist={props.deleteShowFromWishlist}
|
||||
addToWishlist={props.addShowToWishlist}
|
||||
getDetails={props.getShowDetails}
|
||||
updateFilter={props.updateFilter}
|
||||
/>
|
||||
</ListDetails>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
ShowList.propTypes = {
|
||||
match: PropTypes.object,
|
||||
history: PropTypes.object,
|
||||
shows: PropTypes.instanceOf(Map),
|
||||
exploreOptions: PropTypes.instanceOf(Map),
|
||||
selectedImdbId: PropTypes.string,
|
||||
filter: PropTypes.string,
|
||||
loading: PropTypes.bool,
|
||||
deleteShowFromWishlist: PropTypes.func,
|
||||
addShowToWishlist: PropTypes.func,
|
||||
selectShow: PropTypes.func,
|
||||
getShowDetails: PropTypes.func,
|
||||
updateFilter: PropTypes.func,
|
||||
};
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ShowList);
|
||||
|
@ -1,14 +1,16 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { Map } from "immutable"
|
||||
|
||||
import { Link } from "react-router-dom"
|
||||
import { DropdownButton } from "react-bootstrap"
|
||||
import Dropdown from "react-bootstrap/Dropdown"
|
||||
import ButtonToolbar from "react-bootstrap/ButtonToolbar"
|
||||
|
||||
import { WishlistButton, RefreshButton } from "../buttons/actions"
|
||||
import ImdbButton from "../buttons/imdb"
|
||||
|
||||
export default function ShowButtons(props) {
|
||||
return (
|
||||
<div className="list-details-buttons btn-toolbar">
|
||||
const ShowButtons = (props) => (
|
||||
<ButtonToolbar>
|
||||
<ActionsButton
|
||||
show={props.show}
|
||||
addToWishlist={props.addToWishlist}
|
||||
@ -19,14 +21,24 @@ export default function ShowButtons(props) {
|
||||
<Link type="button" className="btn btn-primary btn-sm" to={"/shows/details/" + props.show.get("imdb_id")}>
|
||||
<i className="fa fa-external-link"></i> Details
|
||||
</Link>
|
||||
</div>
|
||||
</ButtonToolbar>
|
||||
);
|
||||
ShowButtons.propTypes = {
|
||||
show: PropTypes.instanceOf(Map),
|
||||
addToWishlist: PropTypes.func.isRequired,
|
||||
deleteFromWishlist: PropTypes.func.isRequired,
|
||||
getDetails: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
function ActionsButton(props) {
|
||||
const ActionsButton = (props) => {
|
||||
let wishlisted = (props.show.get("tracked_season") !== null && props.show.get("tracked_episode") !== null);
|
||||
return (
|
||||
<DropdownButton className="btn btn-default btn-sm" title="Actions" id="actions-button" dropup>
|
||||
<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")}
|
||||
@ -38,6 +50,15 @@ function ActionsButton(props) {
|
||||
addToWishlist={props.addToWishlist}
|
||||
deleteFromWishlist={props.deleteFromWishlist}
|
||||
/>
|
||||
</DropdownButton>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
ActionsButton.propTypes = {
|
||||
show: PropTypes.instanceOf(Map),
|
||||
addToWishlist: PropTypes.func.isRequired,
|
||||
deleteFromWishlist: PropTypes.func.isRequired,
|
||||
getDetails: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
export default ShowButtons;
|
||||
|
@ -12,13 +12,15 @@ const mapDispatchToProps = {
|
||||
};
|
||||
|
||||
const TorrentList = (props) => (
|
||||
<div>
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<AddTorrent addTorrent={props.addTorrent} />
|
||||
<Torrents
|
||||
torrents={props.torrents}
|
||||
removeTorrent={props.removeTorrent}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
TorrentList.propTypes = {
|
||||
fetchTorrents: PropTypes.func.isRequired,
|
||||
@ -31,35 +33,24 @@ export default connect(mapStateToProps, mapDispatchToProps)(TorrentList);
|
||||
const AddTorrent = (props) => {
|
||||
const [url, setUrl] = useState("");
|
||||
|
||||
const handleSubmit = (ev) => {
|
||||
if (ev) { ev.preventDefault(); }
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
if (url === "") { return; }
|
||||
props.addTorrent(url);
|
||||
setUrl("");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-xs-12 col-md-12">
|
||||
<form className="input-group" onSubmit={() => handleSubmit()}>
|
||||
<form onSubmit={(e) => handleSubmit(e)}>
|
||||
<input
|
||||
className="form-control"
|
||||
type="text"
|
||||
className="form-control mb-3 w-100"
|
||||
placeholder="Add torrent URL"
|
||||
onSubmit={handleSubmit}
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
/>
|
||||
<span className="input-group-btn">
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
type="button"
|
||||
onClick={() => handleSubmit()}
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</span>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
AddTorrent.propTypes = {
|
||||
@ -69,21 +60,14 @@ AddTorrent.propTypes = {
|
||||
const Torrents = (props) => {
|
||||
if (props.torrents.size === 0) {
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-xs-12 col-md-12">
|
||||
<h3>Torrents</h3>
|
||||
<div className="panel panel-default">
|
||||
<div className="panel-heading">No torrents</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="jumbotron">
|
||||
<h2>No torrents</h2>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-xs-12 col-md-12">
|
||||
<h3>Torrents</h3>
|
||||
<div className="d-flex flex-wrap">
|
||||
{props.torrents.map((el, index) => (
|
||||
<Torrent
|
||||
key={index}
|
||||
@ -92,7 +76,6 @@ const Torrents = (props) => {
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Torrents.propTypes = {
|
||||
@ -106,10 +89,9 @@ const Torrent = (props) => {
|
||||
}
|
||||
|
||||
const done = props.data.get("is_finished");
|
||||
var progressStyle = "progress-bar progress-bar-info active";
|
||||
if (done) {
|
||||
progressStyle = "progress-bar progress-bar-success";
|
||||
}
|
||||
var progressStyle = done ? "success" : "info progress-bar-striped progress-bar-animated";
|
||||
const progressBarClass = "progress-bar bg-" + progressStyle;
|
||||
|
||||
var percentDone = props.data.get("percent_done");
|
||||
const started = (percentDone !== 0);
|
||||
if (started) {
|
||||
@ -121,28 +103,30 @@ const Torrent = (props) => {
|
||||
const totalSize = prettySize(props.data.get("total_size"));
|
||||
const downloadRate = prettySize(props.data.get("download_rate")) + "/s";
|
||||
return (
|
||||
<div className="panel panel-default">
|
||||
<div className="panel-heading">
|
||||
{props.data.get("name")}
|
||||
<div className="card w-100">
|
||||
<h5 className="card-header">
|
||||
<span className="text text-break">{props.data.get("name")}</span>
|
||||
<span className="fa fa-trash clickable pull-right" onClick={() => handleClick()}></span>
|
||||
</div>
|
||||
<div className="panel-body">
|
||||
</h5>
|
||||
<div className="card-body pb-0">
|
||||
{started &&
|
||||
<div className="progress progress-striped">
|
||||
<React.Fragment>
|
||||
<div className="progress bg-light">
|
||||
<div
|
||||
className={progressStyle}
|
||||
style={{width: percentDone}}>
|
||||
</div>
|
||||
className={progressBarClass}
|
||||
style={{width: percentDone}}
|
||||
role="progressbar"
|
||||
aria-valuenow={percentDone}
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
></div>
|
||||
</div>
|
||||
<p>{downloadedSize} / {totalSize} - {percentDone} - {downloadRate}</p>
|
||||
</React.Fragment>
|
||||
}
|
||||
{!started &&
|
||||
<p>Download not yet started</p>
|
||||
}
|
||||
{started &&
|
||||
<div>
|
||||
<p>{downloadedSize} / {totalSize} - {percentDone} - {downloadRate}</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -31,21 +31,16 @@ const TorrentSearch = (props) => {
|
||||
}, [url]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="col-xs-12">
|
||||
<form className="form-horizontal" onSubmit={(e) => e.preventDefault()}>
|
||||
<div className="form-group">
|
||||
<div className="row">
|
||||
<div className="col-12 d-flex flex-row flex-wrap">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
className="form-control mb-1 w-100 form-control-lg"
|
||||
placeholder="Search torrents"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="mb-3 w-100 d-flex">
|
||||
<SearchButton
|
||||
text="Search movies"
|
||||
type="movies"
|
||||
@ -63,8 +58,8 @@ const TorrentSearch = (props) => {
|
||||
setUrl(getUrl());
|
||||
}}/>
|
||||
</div>
|
||||
<hr />
|
||||
<div className="row">
|
||||
</div>
|
||||
<div className="col-12">
|
||||
<TorrentList
|
||||
searching={props.searching}
|
||||
results={props.results}
|
||||
@ -87,18 +82,15 @@ TorrentSearch.propTypes = {
|
||||
|
||||
|
||||
const SearchButton = (props) => {
|
||||
const color = (props.type === props.typeFromURL) ? "primary" : "default";
|
||||
const variant = (props.type === props.typeFromURL) ? "primary" : "secondary";
|
||||
return (
|
||||
<div className="col-xs-6">
|
||||
<button
|
||||
className={`btn btn-${color} full-width`}
|
||||
type="button"
|
||||
className={`w-50 btn m-1 btn-lg btn-${variant}`}
|
||||
onClick={props.handleClick}
|
||||
>
|
||||
<i className="fa fa-search" aria-hidden="true"></i> {props.text}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
SearchButton.propTypes = {
|
||||
@ -119,16 +111,14 @@ const TorrentList = (props) => {
|
||||
|
||||
if (props.results.size === 0) {
|
||||
return (
|
||||
<div className="col-xs-12">
|
||||
<div className="well well-lg">
|
||||
<div className="jumbotron">
|
||||
<h2>No results</h2>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="col-xs-12">
|
||||
<React.Fragment>
|
||||
{props.results.map(function(el, index) {
|
||||
return (
|
||||
<Torrent
|
||||
@ -137,7 +127,7 @@ const TorrentList = (props) => {
|
||||
addTorrent={props.addTorrent}
|
||||
/>);
|
||||
})}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
TorrentList.propTypes = {
|
||||
@ -148,43 +138,20 @@ TorrentList.propTypes = {
|
||||
};
|
||||
|
||||
const Torrent = (props) => (
|
||||
<div className="row">
|
||||
<div className="col-xs-12">
|
||||
<table className="table responsive table-align-middle torrent-search-result">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td rowSpan="2" className="col-xs-1 torrent-small-width">
|
||||
<h4>
|
||||
<div className="alert d-flex border-bottom border-secondary align-items-center">
|
||||
<TorrentHealth
|
||||
url={props.data.get("url")}
|
||||
seeders={props.data.get("seeders")}
|
||||
leechers={props.data.get("leechers")}
|
||||
/>
|
||||
</h4>
|
||||
</td>
|
||||
<td colSpan="4" className="col-xs-9 title">
|
||||
<span className="torrent-title">{props.data.get("name")}</span>
|
||||
</td>
|
||||
<td rowSpan="2" className="col-xs-1 torrent-small-width">
|
||||
<h4 className="pull-right clickable" onClick={() => props.addTorrent(props.data.get("url"))}>
|
||||
<i className="fa fa-cloud-download" aria-hidden="true"></i>
|
||||
</h4>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="col-xs-1 torrent-label">
|
||||
<span className="label label-warning">{props.data.get("quality")}</span>
|
||||
</td>
|
||||
<td className="col-xs-1 torrent-label">
|
||||
<span className="label label-success">{props.data.get("source")}</span>
|
||||
</td>
|
||||
<td className="col-xs-1 torrent-label">
|
||||
<span className="label label-info">{props.data.get("upload_user")}</span>
|
||||
</td>
|
||||
<td className="col-xs-7 torrent-label"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<span className="mx-3 text text-start text-break flex-fill">{props.data.get("name")}</span>
|
||||
<div>
|
||||
<span className="mx-1 badge badge-pill badge-warning">{props.data.get("quality")}</span>
|
||||
<span className="mx-1 badge badge-pill badge-success">{props.data.get("source")}</span>
|
||||
<span className="mx-1 badge badge-pill badge-info">{props.data.get("upload_user")}</span>
|
||||
</div>
|
||||
<div className="align-self-end ml-3">
|
||||
<i className="fa fa-cloud-download clickable" onClick={() => props.addTorrent(props.data.get("url"))}></i>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -214,7 +181,7 @@ const TorrentHealth = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
const className = `text text-${color}`;
|
||||
const className = `align-self-start text text-center text-${color}`;
|
||||
const tooltip = (
|
||||
<Tooltip id={`tooltip-health-${props.url}`}>
|
||||
<p><span className={className}>Health: {health}</span></p>
|
||||
|
@ -18,16 +18,14 @@ const UserActivation = (props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="content-fluid">
|
||||
<div className="col-md-8 col-md-offset-2 col-xs-12">
|
||||
<div className="row">
|
||||
<div className="col-12 col-md-8 offset-md-2">
|
||||
<h2>Waiting for activation</h2>
|
||||
<hr />
|
||||
<h3>Hang tight! Your user will soon be activated by the administrators of this site.</h3>
|
||||
<Link to="/users/logout">Logout</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
UserActivation.propTypes = {
|
||||
|
@ -28,9 +28,8 @@ const UserLoginForm = (props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="content-fluid">
|
||||
<div className="col-md-6 col-md-offset-3 col-xs-12">
|
||||
<div className="row">
|
||||
<div className="col-10 offset-1 col-md-6 offset-md-3">
|
||||
<h2>Log in</h2>
|
||||
<hr/>
|
||||
{props.error && props.error !== "" &&
|
||||
@ -74,7 +73,6 @@ const UserLoginForm = (props) => {
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
UserLoginForm.propTypes = {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useState } from "react"
|
||||
import React, { useState, useEffect } from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { connect } from "react-redux"
|
||||
import Loader from "../loader/loader"
|
||||
@ -24,12 +24,10 @@ const mapDispatchToProps = {
|
||||
}
|
||||
|
||||
const UserProfile = (props) => {
|
||||
const [fetched, setIsFetched] = useState(false);
|
||||
if (!fetched) {
|
||||
useEffect(() => {
|
||||
props.getUserInfos();
|
||||
props.getUserModules();
|
||||
setIsFetched(true);
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
@ -78,9 +76,8 @@ const UserEdit = (props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="content-fluid">
|
||||
<div className="col-md-6 col-md-offset-3 col-xs-12">
|
||||
<div className="row mb-3">
|
||||
<div className="col-12 col-md-6 offset-md-3">
|
||||
<h2>Edit user</h2>
|
||||
<hr />
|
||||
<form className="form-horizontal" onSubmit={(ev) => handleSubmit(ev)}>
|
||||
@ -132,7 +129,6 @@ const UserEdit = (props) => {
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
UserEdit.propTypes = {
|
||||
|
@ -31,9 +31,8 @@ const UserSignUp = (props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="content-fluid">
|
||||
<div className="col-md-6 col-md-offset-3 col-xs-12">
|
||||
<div className="Row">
|
||||
<div className="col-10 offset-1 col-md-6 offset-md-3">
|
||||
<h2>Sign up</h2>
|
||||
<hr />
|
||||
{props.error && props.error !== "" &&
|
||||
@ -75,7 +74,6 @@ const UserSignUp = (props) => {
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
UserSignUp.propTypes = {
|
||||
|
@ -20,11 +20,8 @@ const UserTokens = (props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="hidden-xs">Tokens</h2>
|
||||
<h3 className="visible-xs">Tokens</h3>
|
||||
|
||||
<div>
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
{props.tokens.map((el, index) => (
|
||||
<Token key={index} data={el} deleteToken={props.deleteUserToken} />
|
||||
))}
|
||||
@ -42,34 +39,25 @@ UserTokens.propTypes = {
|
||||
const Token = (props) => {
|
||||
const ua = UAParser(props.data.get("user_agent"));
|
||||
return (
|
||||
<div className="panel panel-default">
|
||||
<div className="container">
|
||||
<div className="card mt-3">
|
||||
<div className="card-header">
|
||||
<h4>
|
||||
<Logo {...ua} />
|
||||
<div className="col-xs-10">
|
||||
<dl className="dl-horizontal">
|
||||
<dt>Description</dt>
|
||||
<dd>{props.data.get("description")}</dd>
|
||||
|
||||
<dt>Last IP</dt>
|
||||
<dd>{props.data.get("ip")}</dd>
|
||||
|
||||
<dt>Last used</dt>
|
||||
<dd>{moment(props.data.get("last_used")).fromNow()}</dd>
|
||||
|
||||
<dt>Created</dt>
|
||||
<dd>{moment(props.data.get("created_at")).fromNow()}</dd>
|
||||
|
||||
<dt>Device</dt>
|
||||
<dd><Device {...ua.device}/></dd>
|
||||
|
||||
<dt>OS</dt>
|
||||
<dd><OS {...ua.os}/></dd>
|
||||
|
||||
<dt>Browser</dt>
|
||||
<dd><Browser {...ua.browser}/></dd>
|
||||
</dl>
|
||||
</div>
|
||||
<span>{props.data.get("description")}</span>
|
||||
<Actions {...props} />
|
||||
</h4>
|
||||
</div>
|
||||
<div className="card-body row">
|
||||
<div className="col-12 col-md-6">
|
||||
<p>Last IP: {props.data.get("ip")}</p>
|
||||
<p>Last used: {moment(props.data.get("last_used")).fromNow()}</p>
|
||||
<p>Created: {moment(props.data.get("created_at")).fromNow()}</p>
|
||||
</div>
|
||||
<div className="col-12 col-md-6">
|
||||
<p>Device: <Device {...ua.device}/></p>
|
||||
<p>OS: <OS {...ua.os}/></p>
|
||||
<p>Browser: <Browser {...ua.browser}/></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -84,12 +72,10 @@ const Actions = (props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="col-xs-1">
|
||||
<span
|
||||
className="fa fa-trash fa-lg pull-right clickable user-token-action"
|
||||
onClick={() => handleClick()}>
|
||||
className="fa fa-trash fa-lg pull-right clickable"
|
||||
onClick={handleClick}>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Actions.propTypes = {
|
||||
@ -123,16 +109,7 @@ const Logo = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="user-token-icon">
|
||||
<div className="hidden-xs hidden-sm col-md-1">
|
||||
<span className={"fa fa-" + className + " fa-5x"}></span>
|
||||
</div>
|
||||
<div className="hidden-md hidden-lg col-xs-1">
|
||||
<span className={"fa fa-" + className + " fa-lg"}></span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (<span className={`fa fa-${className}`}></span>);
|
||||
}
|
||||
|
||||
const OS = (props) => {
|
||||
|
@ -1,120 +0,0 @@
|
||||
@import "~bootstrap/less/bootstrap.less";
|
||||
@import "~bootswatch/superhero/variables.less";
|
||||
@import "~bootswatch/superhero/bootswatch.less";
|
||||
@import "~font-awesome/less/font-awesome.less";
|
||||
@import "~react-bootstrap-toggle/dist/bootstrap2-toggle.css";
|
||||
|
||||
body {
|
||||
padding-top: @navbar-height + 10px;
|
||||
}
|
||||
|
||||
.thumbnail-selected {
|
||||
transition: border 400ms ease-in-out;
|
||||
animation-name: select-thumbnail;
|
||||
animation-duration: 400ms;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
@keyframes select-thumbnail {
|
||||
0% { background-color: @gray-light; }
|
||||
100% { background-color: @brand-primary; }
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.plot {
|
||||
.text-justify;
|
||||
margin-right: 5%;
|
||||
}
|
||||
|
||||
.list-details-buttons {
|
||||
position: fixed;
|
||||
bottom: 0px;
|
||||
padding-top: 5px;
|
||||
background-color: @body-bg;
|
||||
|
||||
@media (min-width: @screen-xs-max) {
|
||||
right: 0px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
a.btn,
|
||||
div.btn-group {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.poster-list {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.list-filter {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.show-thumbnail {
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.spaced-icons,
|
||||
.episode-buttons {
|
||||
div, span {
|
||||
margin: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar {
|
||||
@media (min-width: @screen-md-min) {
|
||||
opacity: 0.95;
|
||||
}
|
||||
}
|
||||
|
||||
div.sweet-alert > h2 {
|
||||
color: @body-bg;
|
||||
}
|
||||
|
||||
@media (max-width: @screen-xs-max) {
|
||||
form.hidebtn-xs.input-group {
|
||||
display: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.player-modal {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.admin-edit-user-modal {
|
||||
margin-left: 3px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
button.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table.torrent-search-result {
|
||||
font-size: 15px;
|
||||
margin-bottom: 5px;
|
||||
border-bottom: 2px solid @gray-light;
|
||||
td.torrent-small-width {
|
||||
width: 1%;
|
||||
}
|
||||
td.torrent-label {
|
||||
padding-top: 0px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
table.table-align-middle > tbody > tr > td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
div.user-token-icon > div {
|
||||
padding-top: 1%;
|
||||
}
|
||||
|
||||
span.user-token-action {
|
||||
margin: 10px;
|
||||
}
|
91
frontend/scss/app.scss
Normal file
91
frontend/scss/app.scss
Normal file
@ -0,0 +1,91 @@
|
||||
$fa-font-path: "~font-awesome/fonts";
|
||||
|
||||
@import "~font-awesome/scss/font-awesome";
|
||||
@import "~bootswatch/dist/superhero/variables";
|
||||
@import "~bootstrap/scss/bootstrap";
|
||||
@import "~bootswatch/dist/superhero/bootswatch";
|
||||
@import "~react-bootstrap-toggle/dist/bootstrap2-toggle.css";
|
||||
|
||||
// Change all buttons to small on xs
|
||||
@include media-breakpoint-between(xs,sm){
|
||||
.btn {
|
||||
@include button-size($input-btn-padding-y-sm, $input-btn-padding-x-sm, $font-size-sm, $line-height-sm, $btn-border-radius-sm);
|
||||
}
|
||||
}
|
||||
|
||||
$body-padding-top: $navbar-padding-y + 3rem;
|
||||
|
||||
body {
|
||||
padding-top: $body-padding-top;
|
||||
}
|
||||
|
||||
// Remove borders from the navbar
|
||||
.navbar-toggler,
|
||||
div.show.dropdown.nav-item > div {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.thumbnail-selected {
|
||||
transition: border 400ms ease-in-out;
|
||||
animation-name: select-thumbnail;
|
||||
animation-duration: 400ms;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
@keyframes select-thumbnail {
|
||||
0% { background-color: $secondary; }
|
||||
100% { background-color: $primary; }
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.plot {
|
||||
text-align: justify;
|
||||
max-height: 50%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.poster-list {
|
||||
img {
|
||||
max-height: 20rem;
|
||||
object-fit: cover;
|
||||
max-width: 100%;
|
||||
flex-basis: content;
|
||||
}
|
||||
}
|
||||
|
||||
.list-details {
|
||||
position: sticky;
|
||||
top: $body-padding-top;
|
||||
z-index: 1000;
|
||||
height: calc(100vh - #{$body-padding-top});
|
||||
}
|
||||
|
||||
.video-details {
|
||||
font-size: $font-size-sm;
|
||||
p {
|
||||
margin-bottom: .3rem;
|
||||
}
|
||||
|
||||
}
|
||||
@include media-breakpoint-up(sm) {
|
||||
.video-details {
|
||||
font-size: $font-size-base;
|
||||
p {
|
||||
margin-bottom: $paragraph-margin-bottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.show-thumbnail {
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
div.sweet-alert > h2 {
|
||||
color: $body-bg;
|
||||
}
|
||||
|
||||
.player-modal {
|
||||
width: 90%;
|
||||
}
|
13
package.json
13
package.json
@ -6,8 +6,8 @@
|
||||
"lint": "./node_modules/eslint/bin/eslint.js frontend/."
|
||||
},
|
||||
"dependencies": {
|
||||
"bootstrap": "^3.3.6",
|
||||
"bootswatch": "^3.3.7",
|
||||
"bootstrap": "^4.3.1",
|
||||
"bootswatch": "^4.3.1",
|
||||
"font-awesome": "^4.7.0",
|
||||
"fuzzy": "^0.1.3",
|
||||
"history": "^4.7.2",
|
||||
@ -15,13 +15,14 @@
|
||||
"jquery": "^2.2.4",
|
||||
"jwt-decode": "^2.1.0",
|
||||
"moment": "^2.20.1",
|
||||
"popper.js": "^1.14.7",
|
||||
"prop-types": "^15.6.0",
|
||||
"react": "^16.8.6",
|
||||
"react-bootstrap": "^0.32.1",
|
||||
"react-bootstrap": "^1.0.0-beta.8",
|
||||
"react-bootstrap-sweetalert": "^4.2.3",
|
||||
"react-bootstrap-toggle": "^2.2.6",
|
||||
"react-dom": "^16.2.0",
|
||||
"react-infinite-scroller": "^1.2.4",
|
||||
"react-infinite-scroll-component": "^4.5.2",
|
||||
"react-loading": "2.0.3",
|
||||
"react-redux": "6.0.1",
|
||||
"react-router": "5.0.0",
|
||||
@ -37,6 +38,7 @@
|
||||
"@babel/core": "^7.0.0-0",
|
||||
"@babel/preset-env": "^7.4.4",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"autoprefixer": "^9.5.1",
|
||||
"axios": "^0.17.1",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-loader": "^8.0.6",
|
||||
@ -47,6 +49,9 @@
|
||||
"file-loader": "^3.0.1",
|
||||
"less": "^2.3.1",
|
||||
"less-loader": "^4.0.5",
|
||||
"node-sass": "^4.12.0",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"sass-loader": "^7.1.0",
|
||||
"style-loader": "^0.23.1",
|
||||
"url-loader": "^1.1.2",
|
||||
"webpack": "^4.31.0",
|
||||
|
5
postcss.config.js
Normal file
5
postcss.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require("autoprefixer")
|
||||
]
|
||||
}
|
@ -32,14 +32,13 @@ const config = {
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.less$/,
|
||||
use: [{
|
||||
loader: "style-loader" // creates style nodes from JS strings
|
||||
}, {
|
||||
loader: "css-loader" // translates CSS into CommonJS
|
||||
}, {
|
||||
loader: "less-loader" // compiles Less to CSS
|
||||
}]
|
||||
test: /\.scss$/,
|
||||
use: [
|
||||
"style-loader",
|
||||
"css-loader",
|
||||
"sass-loader",
|
||||
"postcss-loader",
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
||||
|
Loading…
x
Reference in New Issue
Block a user