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