Update to bootstrap 4

This commit is contained in:
Grégoire Delattre 2019-05-27 15:06:26 +02:00
parent 2bd90e5cb5
commit b23e311238
37 changed files with 2175 additions and 1706 deletions

View File

@ -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">

View File

@ -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>
); );

View File

@ -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>

View File

@ -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) {
&nbsp; Edit user - {props.data.get("Name")} &nbsp; 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>

View File

@ -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,
};

View File

@ -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>
&nbsp;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

View File

@ -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;

View File

@ -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;

View File

@ -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> &nbsp;{subtitle.get("language").split("_")[1]} <i className="fa fa-download"></i> &nbsp;{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,
} }

View File

@ -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>
); );
} }

View File

@ -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}
> >

View File

@ -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;

View File

@ -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;

View File

@ -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>
);
}
}

View File

@ -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%"}

View File

@ -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>

View File

@ -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;

View 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;

View File

@ -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);

View File

@ -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;

View File

@ -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>
&nbsp; <span className="label label-info">{props.torrentsCount}</span> &nbsp; <span className="badge badge-info badge-pill">{props.torrentsCount}</span>
</span> </span>
</span> </span>
); );

View File

@ -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);

View File

@ -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);

View File

@ -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;

View File

@ -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>
); );

View File

@ -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>

View File

@ -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>
); );

View File

@ -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>
); );

View File

@ -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>
); );

View File

@ -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>
); );

View File

@ -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) => {

View File

@ -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
View 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%;
}

View File

@ -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
View File

@ -0,0 +1,5 @@
module.exports = {
plugins: [
require("autoprefixer")
]
}

View File

@ -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])?$/,

762
yarn.lock

File diff suppressed because it is too large Load Diff