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,12 +6,10 @@ 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>
<tr className="active">
<th>#</th> <th>#</th>
<th>Name</th> <th>Name</th>
<th>Activated</th> <th>Activated</th>
@ -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) { if (props.wishlisted) {
return ( return (
<MenuItem onClick={this.handleClick}> <Dropdown.Item onClick={handleClick}>
<span> <span>
<i className="fa fa-bookmark"></i> Delete from wishlist <i className="fa fa-bookmark"></i> Delete from wishlist
</span> </span>
</MenuItem> </Dropdown.Item>
); );
} else { } else {
return ( return (
<MenuItem onClick={this.handleClick}> <Dropdown.Item onClick={handleClick}>
<span> <span>
<i className="fa fa-bookmark-o"></i> Add to wishlist <i className="fa fa-bookmark-o"></i> Add to wishlist
</span> </span>
</MenuItem> </Dropdown.Item>
); );
} }
}
} }
export class DeleteButton 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();
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-trash"></i> Delete
</span> </span>
</MenuItem> </Dropdown.Item>
); );
}
} }
DeleteButton.propTypes = {
resourceId: PropTypes.string.isRequired,
lastFetchUrl: PropTypes.string,
deleteFunc: PropTypes.func.isRequired,
};
export class RefreshButton extends React.PureComponent { export const RefreshButton = (props) => {
constructor(props) { const handleClick = () => {
super(props); if (props.fetching) { return; }
this.handleClick = this.handleClick.bind(this); props.getDetails(props.resourceId);
} }
handleClick(e) {
e.preventDefault();
if (this.props.fetching) {
return
}
this.props.getDetails(this.props.resourceId);
}
render() {
return ( return (
<MenuItem onClick={this.handleClick}> <Dropdown.Item onClick={handleClick}>
<RefreshIndicator refresh={this.props.fetching} /> <RefreshIndicator refresh={props.fetching} />
</MenuItem> </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 ( return (
<MenuItem onClick={this.handleClick}> <Dropdown.Item onClick={handleClick}>
<RefreshIndicator refresh={this.props.fetching} /> <RefreshIndicator refresh={props.fetching} />
</MenuItem> </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,17 +2,22 @@ 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 (
<div className="col-8 col-md-4 list-details pl-1 d-flex align-items-start flex-column">
<div className="video-details flex-fill d-flex flex-column">
<h2 className="d-none d-sm-block">{props.data.get("title")}</h2>
<h4 className="d-block d-sm-none">{props.data.get("title")}</h4>
<TrackingLabel <TrackingLabel
wishlisted={props.data.get("wishlisted")} wishlisted={props.data.get("wishlisted")}
trackedSeason={props.data.get("tracked_season")} trackedSeason={props.data.get("tracked_season")}
trackedEpisode={props.data.get("tracked_episode")} trackedEpisode={props.data.get("tracked_episode")}
/> />
<h4>{props.data.get("year")}</h4> <h4 className="d-none d-sm-block">{props.data.get("year")}</h4>
<h5 className="d-block d-sm-none">{props.data.get("year")}</h5>
<Runtime runtime={props.data.get("runtime")} /> <Runtime runtime={props.data.get("runtime")} />
<Genres genres={props.data.get("genres")} /> <Genres genres={props.data.get("genres")} />
<Ratings <Ratings
@ -26,13 +31,17 @@ const ListDetails = (props) => (
audioCodec={props.data.get("audio_codec")} audioCodec={props.data.get("audio_codec")}
videoCodec={props.data.get("video_codec")} videoCodec={props.data.get("video_codec")}
/> />
<p className="plot">{props.data.get("plot")}</p> <p className="text text-break plot">{props.data.get("plot")}</p>
</div> </div>
<div className="pb-1 align-self-end">
{props.children} {props.children}
</div> </div>
) </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>
<span className="badge badge-secondary">
<i className="fa fa-bookmark"></i> {wishlistStr} <i className="fa fa-bookmark"></i> {wishlistStr}
</span> </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>
<div className="col-xs-12 col-sm-6 col-md-3 col-lg-2">
<a className={imgClass}>
<img <img
src={props.data.get("poster_url")} src={props.data.get("poster_url")}
onClick={props.onClick} onClick={props.onClick}
onDoubleClick={props.onDoubleClick} onDoubleClick={props.onDoubleClick}
className={`my-1 m-md-2 img-thumbnail ${className}`}
/> />
</a>
</div>
</div>
); );
} }
Poster.propTypes = {
data: PropTypes.instanceOf(Map),
selected: PropTypes.bool,
onClick: PropTypes.func,
onDoubleClick: PropTypes.func,
};
export default Poster;

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,69 +10,24 @@ 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;
export default class ListPosters extends React.PureComponent {
constructor(props) {
super(props);
this.loadMore = this.loadMore.bind(this);
this.state = this.getNextState(props);
}
loadMore() {
// Nothing to do if the app is loading
if (this.props.loading) {
return;
}
if (this.props.data === undefined) {
return;
}
this.setState(this.getNextState(this.props));
}
getNextState(props) {
let totalListSize = props.data !== undefined ? props.data.size : 0;
let currentListSize = (this.state && this.state.items) ? this.state.items : 0;
let nextListSize = currentListSize + DEFAULT_ADD_EXTRA_ITEMS;
let hasMore = true;
if (nextListSize >= totalListSize) {
nextListSize = totalListSize;
hasMore = false;
}
return {
items: nextListSize,
hasMore: hasMore,
};
}
componentWillReceiveProps(nextProps) {
if (this.props.data === undefined) { return }
if (nextProps.data === undefined) { return }
if (this.props.data.size !== nextProps.data.size) {
this.setState(this.getNextState(nextProps));
}
}
render() {
let elmts = this.props.data;
const listSize = elmts !== undefined ? elmts.size : 0; 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 // Filter the list of elements
if (this.props.filter !== "") { if (props.filter !== "") {
elmts = elmts.filter((v) => fuzzy.test(this.props.filter, v.get("title")), this); elmts = elmts.filter((v) => fuzzy.test(props.filter, v.get("title")));
} else { } else {
elmts = elmts.slice(0, this.state.items); elmts = elmts.slice(0, listSize.items);
} }
// Chose when to display filter / explore options // Chose when to display filter / explore options
let displayFilter = true; let displayFilter = true;
if ((this.props.params if ((props.params
&& this.props.params.category && props.params.category
&& this.props.params.category !== "" && props.params.category !== ""
&& this.props.params.source && props.params.source
&& this.props.params.source !== "") && props.params.source !== "")
|| (listSize === 0)) { || (listSize === 0)) {
displayFilter = false; displayFilter = false;
} }
@ -84,89 +39,53 @@ export default class ListPosters extends React.PureComponent {
} }
return ( return (
<div className={colSize}> <div className="col px-1">
{displayFilter && {displayFilter &&
<ListFilter <ListFilter
updateFilter={this.props.updateFilter} updateFilter={props.updateFilter}
placeHolder={this.props.placeHolder} placeHolder={props.placeHolder}
/> />
} }
<ExplorerOptions <ExplorerOptions
type={this.props.type} type={props.type}
display={displayExplorerOptions} display={displayExplorerOptions}
params={this.props.params} params={props.params}
options={this.props.exploreOptions} options={props.exploreOptions}
/> />
<Posters <Posters
elmts={elmts} elmts={elmts}
loading={this.props.loading} loading={props.loading}
hasMore={this.state.hasMore} selectedImdbId={props.selectedImdbId}
loadMore={this.loadMore} selectPoster={props.onClick}
selectedImdbId={this.props.selectedImdbId} onDoubleClick={props.onDoubleClick}
selectPoster={this.props.onClick} onKeyEnter={props.onKeyEnter}
onDoubleClick={this.props.onDoubleClick}
onKeyEnter={this.props.onKeyEnter}
/> />
</div> </div>
); );
}
} }
ListPosters.propTypes = {
data: PropTypes.instanceOf(OrderedMap),
onClick: PropTypes.func,
onDoubleClick: PropTypes.func,
onKeyEnter: PropTypes.func,
selectedImdbId: PropTypes.string,
loading: PropTypes.bool.isRequired,
params: PropTypes.object.isRequired,
exploreOptions: PropTypes.instanceOf(Map),
type: PropTypes.string.isRequired,
placeHolder: PropTypes.string.isRequired,
updateFilter: PropTypes.func.isRequired,
filter: PropTypes.string,
};
class Posters extends React.PureComponent { export default ListPosters;
constructor(props) {
super(props);
}
move(event) { const Posters = (props) => {
// Detect which direction to go if (props.loading) {
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 />); return (<Loader />);
} }
if (this.props.elmts.size === 0) { if (props.elmts.size === 0) {
return ( return (
<div className="jumbotron"> <div className="jumbotron">
<h2>No result</h2> <h2>No result</h2>
@ -174,45 +93,53 @@ class Posters extends React.PureComponent {
); );
} }
const addMoreCount = 20;
const [size, setSize] = useState(0);
useEffect(() => {
loadMore()
}, [props.elmts.size]);
const hasMore = () => (size !== props.elmts.size);
const loadMore = () => {
if (!hasMore()) { return }
const newSize = (((size + addMoreCount) >= props.elmts.size)
? props.elmts.size
: size + addMoreCount);
setSize(newSize);
}
return ( return (
<div tabIndex="0"
onKeyDown={(event) => this.move(event)}
className="poster-list"
>
<InfiniteScroll <InfiniteScroll
hasMore={this.props.hasMore} className="poster-list d-block flex-row flex-wrap justify-content-around"
loadMore={this.props.loadMore} dataLength={size}
className="row" next={loadMore}
hasMore={hasMore()}
loader={<Loader />}
> >
{this.props.elmts.toIndexedSeq().map(function(movie, index) { {props.elmts.slice(0, size).toIndexedSeq().map(function(el, index) {
const imdbId = movie.get("imdb_id"); const imdbId = el.get("imdb_id");
const selected = (imdbId === this.props.selectedImdbId) ? true : false; const selected = (imdbId === 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 ( return (
<div key={imdbId}>
{clearFixes.length > 0 && clearFixes.map(function(el, i) {
return (
<div key={`clearfix-${imdbId}-${i}`} className={el}></div>
);
})}
<Poster <Poster
data={movie} data={el}
key={`poster-${imdbId}`} key={`poster-${imdbId}-${index}`}
selected={selected} selected={selected}
onClick={() => this.props.selectPoster(imdbId)} onClick={() => props.selectPoster(imdbId)}
onDoubleClick={() => this.props.onDoubleClick(imdbId)} onDoubleClick={() => props.onDoubleClick(imdbId)}
/> />
</div>
) )
} ,this)} } ,this)}
</InfiniteScroll> </InfiniteScroll>
</div>
); );
}
} }
Posters.propTypes = {
elmts: PropTypes.instanceOf(OrderedMap),
selectedImdbId: PropTypes.string,
loading: PropTypes.bool.isRequired,
onDoubleClick: PropTypes.func,
onKeyEnter: PropTypes.func,
selectPoster: PropTypes.func,
};

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>
{props.data.get("error") !== "" &&
<p>Error: {props.data.get("error")}</p> <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,11 +1,16 @@
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}
@ -16,7 +21,6 @@ export default function ActionsButton(props) {
resourceId={props.movieId} resourceId={props.movieId}
lastFetchUrl={props.lastFetchUrl} lastFetchUrl={props.lastFetchUrl}
deleteFunc={props.deleteMovie} deleteFunc={props.deleteMovie}
isUserAdmin={props.isUserAdmin}
/> />
} }
<WishlistButton <WishlistButton
@ -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}
/> />
{selectedMovie !== undefined && <ListDetails data={selectedMovie} loading={props.loading}>
<ListDetails data={selectedMovie}>
<MovieButtons <MovieButtons
movie={selectedMovie} movie={selectedMovie}
getMovieDetails={this.props.getMovieDetails} getMovieDetails={props.getMovieDetails}
addTorrent={this.props.addTorrent} addTorrent={props.addTorrent}
deleteMovie={this.props.deleteMovie} deleteMovie={props.deleteMovie}
addToWishlist={this.props.addMovieToWishlist} addToWishlist={props.addMovieToWishlist}
deleteFromWishlist={this.props.deleteMovieFromWishlist} deleteFromWishlist={props.deleteMovieFromWishlist}
lastFetchUrl={this.props.lastFetchUrl} lastFetchUrl={props.lastFetchUrl}
refreshSubtitles={this.props.refreshSubtitles} refreshSubtitles={props.refreshSubtitles}
/> />
</ListDetails> </ListDetails>
} </Row>
</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,24 +24,20 @@ 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)}>
<Navbar.Header>
<LinkContainer to="/"> <LinkContainer to="/">
<Navbar.Brand><Link to="/">Canapé</Link></Navbar.Brand> <Navbar.Brand>Canapé</Navbar.Brand>
</LinkContainer> </LinkContainer>
<Navbar.Toggle /> <Navbar.Toggle />
</Navbar.Header>
<Navbar.Collapse> <Navbar.Collapse>
<Nav className="mr-auto">
<MoviesDropdown /> <MoviesDropdown />
<ShowsDropdown /> <ShowsDropdown />
<WishlistDropdown /> <WishlistDropdown />
<TorrentsDropdown torrentsCount={props.torrentCount} /> <TorrentsDropdown torrentsCount={props.torrentCount} />
<UserDropdown </Nav>
username={props.username} <Nav>
isAdmin={props.isAdmin}
/>
<Route path="/movies" render={(props) => <Route path="/movies" render={(props) =>
<Search <Search
placeholder="Search movies" placeholder="Search movies"
@ -53,9 +52,14 @@ const AppNavBar = (props) => {
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">
<MenuItem>Discover</MenuItem> <NavDropdown.Item>Discover</NavDropdown.Item>
</LinkContainer> </LinkContainer>
<LinkContainer to="/movies/polochon"> <LinkContainer to="/movies/polochon">
<MenuItem>My movies</MenuItem> <NavDropdown.Item>My movies</NavDropdown.Item>
</LinkContainer>
</NavDropdown>
)
const ShowsDropdown = () => (
<NavDropdown title="Shows" id="navbar-shows-dropdown">
<LinkContainer to="/shows/explore/eztv/rating">
<NavDropdown.Item>Discover</NavDropdown.Item>
</LinkContainer>
<LinkContainer to="/shows/polochon">
<NavDropdown.Item>My shows</NavDropdown.Item>
</LinkContainer>
</NavDropdown>
);
const UserDropdown = (props) => (
<Nav>
<NavDropdown title={props.username} alignRight>
{props.isAdmin &&
<LinkContainer to="/admin">
<NavDropdown.Item>Admin Panel</NavDropdown.Item>
</LinkContainer>
}
<LinkContainer to="/users/profile">
<NavDropdown.Item>Profile</NavDropdown.Item>
</LinkContainer>
<LinkContainer to="/users/tokens">
<NavDropdown.Item>Tokens</NavDropdown.Item>
</LinkContainer>
<LinkContainer to="/users/logout">
<NavDropdown.Item>Logout</NavDropdown.Item>
</LinkContainer> </LinkContainer>
</NavDropdown> </NavDropdown>
</Nav> </Nav>
) )
const ShowsDropdown = () => (
<Nav>
<NavDropdown title="Shows" id="navbar-shows-dropdown">
<LinkContainer to="/shows/explore/eztv/rating">
<NavItem>Discover</NavItem>
</LinkContainer>
<LinkContainer to="/shows/polochon">
<NavItem>My shows</NavItem>
</LinkContainer>
</NavDropdown>
</Nav>
);
function UserDropdown(props) {
return (
<Nav pullRight>
<NavDropdown title={props.username} id="navbar-dropdown-right">
{props.isAdmin &&
<LinkContainer to="/admin">
<MenuItem>Admin Panel</MenuItem>
</LinkContainer>
}
<LinkContainer to="/users/profile">
<MenuItem>Profile</MenuItem>
</LinkContainer>
<LinkContainer to="/users/tokens">
<MenuItem>Tokens</MenuItem>
</LinkContainer>
<LinkContainer to="/users/logout">
<MenuItem>Logout</MenuItem>
</LinkContainer>
</NavDropdown>
</Nav>
);
}
UserDropdown.propTypes = { 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">
<MenuItem>Movies</MenuItem> <NavDropdown.Item>Movies</NavDropdown.Item>
</LinkContainer> </LinkContainer>
<LinkContainer to="/shows/wishlist"> <LinkContainer to="/shows/wishlist">
<MenuItem>Shows</MenuItem> <NavDropdown.Item>Shows</NavDropdown.Item>
</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">
<MenuItem>Downloads</MenuItem> <NavDropdown.Item>Downloads</NavDropdown.Item>
</LinkContainer> </LinkContainer>
<LinkContainer to="/torrents/search"> <LinkContainer to="/torrents/search">
<MenuItem>Search</MenuItem> <NavDropdown.Item>Search</NavDropdown.Item>
</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,188 +32,164 @@ const mapDispatchToProps = {
fetchShowDetails, getEpisodeDetails, refreshSubtitles, fetchShowDetails, getEpisodeDetails, refreshSubtitles,
}; };
class ShowDetails extends React.Component { const ShowDetails = (props) => {
render() { if (props.loading) {
// Loading
if (this.props.loading) {
return (<Loader />); return (<Loader />);
} }
return ( return (
<div className="row" id="container"> <div className="row no-gutters">
<Header <Header
data={this.props.show} data={props.show}
addToWishlist={this.props.addShowToWishlist} addToWishlist={props.addShowToWishlist}
deleteFromWishlist={this.props.deleteShowFromWishlist} deleteFromWishlist={props.deleteShowFromWishlist}
/> />
<SeasonsList <SeasonsList
data={this.props.show} data={props.show}
router={this.props.router} addTorrent={props.addTorrent}
addTorrent={this.props.addTorrent} addToWishlist={props.addShowToWishlist}
addToWishlist={this.props.addShowToWishlist} getEpisodeDetails={props.getEpisodeDetails}
getEpisodeDetails={this.props.getEpisodeDetails} refreshSubtitles={props.refreshSubtitles}
refreshSubtitles={this.props.refreshSubtitles}
/> />
</div> </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} />
<HeaderDetails
data={props.data}
addToWishlist={props.addToWishlist}
deleteFromWishlist={props.deleteFromWishlist}
/>
</div> </div>
</div> <div className="col-8">
</div> <div className="card-body">
); <h5 className="card-title">{props.data.get("title")}</h5>
} <p className="card-text">{props.data.get("year")}</p>
<p className="card-text">{props.data.get("rating")}</p>
function HeaderThumbnail(props){ <p className="card-text">{props.data.get("plot")}</p>
return ( <p className="card-text">
<div className="col-xs-12 col-sm-2 text-center"> <ImdbButton imdbId={props.data.get("imdb_id")} xs/>
<img src={props.data.get("poster_url")} </p>
className="show-thumbnail thumbnail-selected img-thumbnail img-responsive"/>
</div>
);
}
function HeaderDetails(props){
return (
<div className="col-xs-12 col-sm-10">
<dl className="dl-horizontal">
<dt>Title</dt>
<dd>{props.data.get("title")}</dd>
<dt>Plot</dt>
<dd className="plot">{props.data.get("plot")}</dd>
<dt>IMDB</dt>
<dd>
<ImdbButton imdbId={props.data.get("imdb_id")} size="xs"/>
</dd>
<dt>Year</dt>
<dd>{props.data.get("year")}</dd>
<dt>Rating</dt>
<dd>{props.data.get("rating")}</dd>
</dl>
<TrackHeader <TrackHeader
data={props.data} data={props.data}
addToWishlist={props.addToWishlist} addToWishlist={props.addToWishlist}
deleteFromWishlist={props.deleteFromWishlist} deleteFromWishlist={props.deleteFromWishlist}
/> />
</div> </div>
); </div>
} </div>
</div>
);
Header.propTypes = {
data: PropTypes.instanceOf(Map),
deleteFromWishlist: PropTypes.func,
addToWishlist: PropTypes.func,
};
function SeasonsList(props){ const SeasonsList = (props) => (
return ( <div className="col col-12 col-md-10 offset-md-1">
<div>
{props.data.get("seasons").entrySeq().map(function([season, data]) { {props.data.get("seasons").entrySeq().map(function([season, data]) {
return ( return (
<div className="col-xs-12 col-sm-10 col-sm-offset-1 col-md-10 col-md-offset-1" key={season.toString()}>
<Season <Season
key={`season-list-key-${season}`}
data={data} data={data}
season={season} season={season}
showName={props.data.get("title")} 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>
) );
} SeasonsList.propTypes = {
data: PropTypes.instanceOf(Map),
addToWishlist: PropTypes.func,
addTorrent: PropTypes.func,
refreshSubtitles: PropTypes.func,
getEpisodeDetails: PropTypes.func,
};
const Season = (props) => {
const [show, setShow] = useState(false);
const visibility = show ? "d-flex flex-column" : "d-none";
const icon = show ? "down" : "left"
class Season extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.state = { colapsed: true };
}
handleClick(e) {
e.preventDefault();
this.setState({ colapsed: !this.state.colapsed });
}
render() {
return ( return (
<div className="panel panel-default"> <div className="card mb-3">
<div className="panel-heading clickable" onClick={(e) => this.handleClick(e)}> <div className="card-header clickable" onClick={() => setShow(!show)}>
Season {this.props.season} <h5 className="m-0">
<small className="text-primary"> ({this.props.data.toList().size} episodes)</small> Season {props.season}
<span className="pull-right"> <small className="text-primary"> ({props.data.toList().size} episodes)</small>
{this.state.colapsed || <i className={`float-right fa fa-chevron-${icon}`}></i>
<i className="fa fa-chevron-down"></i> </h5>
}
{this.state.colapsed &&
<i className="fa fa-chevron-left"></i>
}
</span>
</div> </div>
{this.state.colapsed || <div className={`card-body ${visibility}`}>
<table className="table table-striped table-hover"> {props.data.toList().map(function(episode) {
<tbody>
{this.props.data.toList().map(function(episode) {
let key = `${episode.get("season")}-${episode.get("episode")}`; let key = `${episode.get("season")}-${episode.get("episode")}`;
return ( return (
<Episode <Episode
key={key} key={key}
data={episode} data={episode}
showName={this.props.showName} showName={props.showName}
router={this.props.router} addTorrent={props.addTorrent}
addTorrent={this.props.addTorrent} addToWishlist={props.addToWishlist}
addToWishlist={this.props.addToWishlist} getEpisodeDetails={props.getEpisodeDetails}
getEpisodeDetails={this.props.getEpisodeDetails} refreshSubtitles={props.refreshSubtitles}
refreshSubtitles={this.props.refreshSubtitles}
/> />
) )
}, this)} })}
</tbody> </div>
</table>
}
</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,
};
function PolochonMetadata(props) { const PolochonMetadata = (props) => {
if (!props.quality || props.quality === "") { if (!props.quality || props.quality === "") { return null }
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}
addToWishlist={props.addToWishlist}
/>
{props.data.get("episode")}
</th>
<td className="col-xs-12">
{props.data.get("title")}
<PolochonMetadata <PolochonMetadata
quality={props.data.get("quality")} quality={props.data.get("quality")}
releaseGroup={props.data.get("release_group")} releaseGroup={props.data.get("release_group")}
@ -213,7 +197,7 @@ function Episode(props) {
audioCodec={props.data.get("audio_codec")} audioCodec={props.data.get("audio_codec")}
videoCodec={props.data.get("video_codec")} videoCodec={props.data.get("video_codec")}
/> />
<span className="pull-right episode-buttons"> <div className="align-self-end btn-toolbar">
{props.data.get("polochon_url") !== "" && {props.data.get("polochon_url") !== "" &&
<SubtitlesButton <SubtitlesButton
fetching={props.data.get("fetchingSubtitles")} fetching={props.data.get("fetchingSubtitles")}
@ -237,168 +221,147 @@ function Episode(props) {
) )
})} })}
<DownloadButton <DownloadButton
name={`${props.showName} - S${pad(props.data.get("season"))}E${pad(props.data.get("episode"))}`}
url={props.data.get("polochon_url")} url={props.data.get("polochon_url")}
subtitles={props.data.get("subtitles")} subtitles={props.data.get("subtitles")}
xs xs
/> />
<GetDetailsButton <GetDetailsButtonWithRouter
showName={props.showName} showName={props.showName}
router={props.router}
data={props.data} data={props.data}
getEpisodeDetails={props.getEpisodeDetails} getEpisodeDetails={props.getEpisodeDetails}
/> />
</span> </div>
</td> </div>
</tr> )
) Episode.propTypes = {
data: PropTypes.instanceOf(Map),
season: PropTypes.number,
showName: PropTypes.string,
addToWishlist: PropTypes.func,
addTorrent: PropTypes.func,
refreshSubtitles: PropTypes.func,
getEpisodeDetails: PropTypes.func,
};
const Torrent = (props) => (
<button type="button"
className="btn btn-primary btn-sm"
onClick={() => props.addTorrent(props.data.get("url"))}
href={props.data.url} >
<i className="fa fa-download"></i> {props.data.get("quality")}
</button>
)
Torrent.propTypes = {
data: PropTypes.instanceOf(Map),
addTorrent: PropTypes.func,
};
const TrackHeader = (props) => {
const trackedSeason = props.data.get("tracked_season");
const trackedEpisode = props.data.get("tracked_episode");
const imdbId = props.data.get("imdb_id");
const wishlisted = (trackedSeason !== null && trackedEpisode !== null);
const handleClick = () => {
if (wishlisted) {
props.deleteFromWishlist(imdbId);
} else {
props.addToWishlist(imdbId);
}
} }
class Torrent extends React.PureComponent { if (wishlisted) {
constructor(props) { const msg = (trackedSeason !== 0 && trackedEpisode !== 0)
super(props); ? (<p>Show tracked from <strong>season {trackedSeason} episode {trackedEpisode}</strong></p>)
this.handleClick = this.handleClick.bind(this); : (<p>Whole show tracked</p>);
}
handleClick(e, url) {
e.preventDefault();
this.props.addTorrent(url);
}
render() {
return (
<span>
<a type="button"
className="btn btn-primary btn-xs"
onClick={(e) => this.handleClick(e, this.props.data.get("url"))}
href={this.props.data.url} >
<i className="fa fa-download"></i> {this.props.data.get("quality")}
</a>
</span>
)
}
}
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 ( return (
<dl className="dl-horizontal"> <span className="card-text">
<dt>Tracking active</dt>
{msg} {msg}
<dt></dt> <a className="btn btn-sm btn-danger" onClick={handleClick}>
<dd>
<a className="btn btn-xs btn-danger" onClick={(e) => this.handleClick(e)}>
<i className="fa fa-bookmark"></i> Untrack the show <i className="fa fa-bookmark"></i> Untrack the show
</a> </a>
</dd> </span>
</dl>
); );
} else { }
return ( return (
<dl className="dl-horizontal"> <span className="card-text">
<dt>Tracking inactive</dt> <p>Tracking inactive</p>
<dd> <a className="btn btn-sm btn-info" onClick={(e) => handleClick(e)}>
<a className="btn btn-xs btn-info" onClick={(e) => this.handleClick(e)}>
<i className="fa fa-bookmark-o"></i> Track the whole show <i className="fa fa-bookmark-o"></i> Track the whole show
</a> </a>
</dd> </span>
</dl>
); );
}
}
} }
TrackHeader.propTypes = {
data: PropTypes.instanceOf(Map),
addToWishlist: PropTypes.func,
deleteFromWishlist: PropTypes.func,
};
class TrackButton extends React.PureComponent { const TrackButton = (props) => {
constructor(props) { const imdbId = props.data.get("show_imdb_id");
super(props); const season = props.data.get("season");
this.handleClick = this.handleClick.bind(this); const episode = props.data.get("episode");
}
handleClick(e) { const tooltipId = `tooltip-${props.data.season}-${props.data.episode}`;
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 = ( const tooltip = (
<Tooltip id={tooltipId}>Track show from here</Tooltip> <Tooltip id={tooltipId}>Track show from here</Tooltip>
); );
return ( return (
<OverlayTrigger placement="top" overlay={tooltip}> <OverlayTrigger placement="top" overlay={tooltip}>
<a type="button" className="btn btn-default btn-xs" onClick={(e) => this.handleClick(e)}> <span className="btn clickable"
onClick={() => props.addToWishlist(imdbId, season, episode)}>
<i className="fa fa-bookmark"></i> <i className="fa fa-bookmark"></i>
</a> </span>
</OverlayTrigger> </OverlayTrigger>
); );
}
} }
TrackButton.propTypes = {
data: PropTypes.instanceOf(Map),
addToWishlist: PropTypes.func,
};
class GetDetailsButton extends React.PureComponent { const GetDetailsButton = (props) => {
constructor(props) { const imdbId = props.data.get("show_imdb_id");
super(props); const season = props.data.get("season");
this.handleFetchClick = this.handleFetchClick.bind(this); const episode = props.data.get("episode");
this.handleAdvanceTorrentSearchClick = this.handleAdvanceTorrentSearchClick.bind(this); const id = `${imdbId}-${season}-${episode}-refresh-dropdown`;
this.state = {
imdbId: this.props.data.get("show_imdb_id"), const handleFetchClick = () => {
season: this.props.data.get("season"), if (props.data.get("fetching")) { return }
episode: this.props.data.get("episode"), 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 ( return (
<Dropdown id={id} dropup> <SplitButton
<Button className="btn-xs" bsStyle="info" onClick={this.handleFetchClick}> drop="up"
<RefreshIndicator refresh={this.props.data.get("fetching")} /> variant="info"
</Button> title={<RefreshIndicator refresh={props.data.get("fetching")} />}
<Dropdown.Toggle className="btn-xs" bsStyle="info"/> size="sm"
<Dropdown.Menu> id={`refresh-${id}`}
<MenuItem onClick={this.handleAdvanceTorrentSearchClick}> onClick={handleFetchClick}>
<Dropdown.Item onClick={handleAdvanceTorrentSearchClick}>
<span> <span>
<i className="fa fa-magnet"></i> Advanced torrent search <i className="fa fa-magnet"></i> Advanced torrent search
</span> </span>
</MenuItem> </Dropdown.Item>
</Dropdown.Menu> </SplitButton>
</Dropdown>
); );
}
} }
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) {
return this.props.history.push("/shows/details/" + imdbId);
}
render() {
let selectedShow; let selectedShow;
if (this.props.selectedImdbId !== "") { if (props.selectedImdbId !== "") {
selectedShow = this.props.shows.get(this.props.selectedImdbId); selectedShow = props.shows.get(props.selectedImdbId);
} }
return ( return (
<div className="row" id="container"> <div className="row" id="container">
<ListPosters <ListPosters
data={this.props.shows} data={props.shows}
type="shows" type="shows"
placeHolder="Filter shows..." placeHolder="Filter shows..."
exploreOptions={this.props.exploreOptions} exploreOptions={props.exploreOptions}
updateFilter={this.props.updateFilter} updateFilter={props.updateFilter}
selectedImdbId={this.props.selectedImdbId} selectedImdbId={props.selectedImdbId}
filter={this.props.filter} filter={props.filter}
onClick={this.props.selectShow} onClick={props.selectShow}
onDoubleClick={this.showDetails} onDoubleClick={showDetails}
onKeyEnter={this.showDetails} onKeyEnter={showDetails}
params={this.props.match.params} params={props.match.params}
loading={this.props.loading} loading={props.loading}
/> />
{selectedShow && {selectedShow &&
<ListDetails data={selectedShow} > <ListDetails data={selectedShow} loading={props.loading}>
<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,14 +1,16 @@
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}
@ -19,14 +21,24 @@ export default function ShowButtons(props) {
<Link type="button" className="btn btn-primary btn-sm" to={"/shows/details/" + props.show.get("imdb_id")}> <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>
</div> </ButtonToolbar>
); );
ShowButtons.propTypes = {
show: PropTypes.instanceOf(Map),
addToWishlist: PropTypes.func.isRequired,
deleteFromWishlist: PropTypes.func.isRequired,
getDetails: PropTypes.func.isRequired,
} }
function ActionsButton(props) { const ActionsButton = (props) => {
let wishlisted = (props.show.get("tracked_season") !== null && props.show.get("tracked_episode") !== null); 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">
<Dropdown.Toggle variant="secondary" id="movie-button-actions">
Actions
</Dropdown.Toggle>
<Dropdown.Menu>
<RefreshButton <RefreshButton
fetching={props.show.get("fetchingDetails")} fetching={props.show.get("fetchingDetails")}
resourceId={props.show.get("imdb_id")} resourceId={props.show.get("imdb_id")}
@ -38,6 +50,15 @@ function ActionsButton(props) {
addToWishlist={props.addToWishlist} addToWishlist={props.addToWishlist}
deleteFromWishlist={props.deleteFromWishlist} deleteFromWishlist={props.deleteFromWishlist}
/> />
</DropdownButton> </Dropdown.Menu>
</Dropdown>
); );
} }
ActionsButton.propTypes = {
show: PropTypes.instanceOf(Map),
addToWishlist: PropTypes.func.isRequired,
deleteFromWishlist: PropTypes.func.isRequired,
getDetails: PropTypes.func.isRequired,
}
export default ShowButtons;

View File

@ -12,13 +12,15 @@ const mapDispatchToProps = {
}; };
const TorrentList = (props) => ( const TorrentList = (props) => (
<div> <div className="row">
<div className="col-12">
<AddTorrent addTorrent={props.addTorrent} /> <AddTorrent addTorrent={props.addTorrent} />
<Torrents <Torrents
torrents={props.torrents} torrents={props.torrents}
removeTorrent={props.removeTorrent} removeTorrent={props.removeTorrent}
/> />
</div> </div>
</div>
); );
TorrentList.propTypes = { TorrentList.propTypes = {
fetchTorrents: PropTypes.func.isRequired, fetchTorrents: PropTypes.func.isRequired,
@ -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">
<form className="input-group" onSubmit={() => handleSubmit()}>
<input <input
className="form-control" type="text"
className="form-control mb-3 w-100"
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">
<button
className="btn btn-primary"
type="button"
onClick={() => handleSubmit()}
>
Add
</button>
</span>
</form> </form>
</div>
</div>
); );
} }
AddTorrent.propTypes = { AddTorrent.propTypes = {
@ -69,21 +60,14 @@ 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">
<h3>Torrents</h3>
{props.torrents.map((el, index) => ( {props.torrents.map((el, index) => (
<Torrent <Torrent
key={index} key={index}
@ -92,7 +76,6 @@ const Torrents = (props) => {
/> />
))} ))}
</div> </div>
</div>
); );
} }
Torrents.propTypes = { Torrents.propTypes = {
@ -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 className="progress bg-light">
<div <div
className={progressStyle} className={progressBarClass}
style={{width: percentDone}}> style={{width: percentDone}}
</div> role="progressbar"
aria-valuenow={percentDone}
aria-valuemin="0"
aria-valuemax="100"
></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,21 +31,16 @@ 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()}>
<div className="form-group">
<input <input
type="text" type="text"
className="form-control" className="form-control mb-1 w-100 form-control-lg"
placeholder="Search torrents" placeholder="Search torrents"
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
/> />
</div> <div className="mb-3 w-100 d-flex">
</form>
</div>
<div className="row">
<SearchButton <SearchButton
text="Search movies" text="Search movies"
type="movies" type="movies"
@ -63,8 +58,8 @@ const TorrentSearch = (props) => {
setUrl(getUrl()); setUrl(getUrl());
}}/> }}/>
</div> </div>
<hr /> </div>
<div className="row"> <div className="col-12">
<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
className={`btn btn-${color} full-width`}
type="button" type="button"
className={`w-50 btn m-1 btn-lg btn-${variant}`}
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">
<table className="table responsive table-align-middle torrent-search-result">
<tbody>
<tr>
<td rowSpan="2" className="col-xs-1 torrent-small-width">
<h4>
<TorrentHealth <TorrentHealth
url={props.data.get("url")} url={props.data.get("url")}
seeders={props.data.get("seeders")} seeders={props.data.get("seeders")}
leechers={props.data.get("leechers")} leechers={props.data.get("leechers")}
/> />
</h4> <span className="mx-3 text text-start text-break flex-fill">{props.data.get("name")}</span>
</td> <div>
<td colSpan="4" className="col-xs-9 title"> <span className="mx-1 badge badge-pill badge-warning">{props.data.get("quality")}</span>
<span className="torrent-title">{props.data.get("name")}</span> <span className="mx-1 badge badge-pill badge-success">{props.data.get("source")}</span>
</td> <span className="mx-1 badge badge-pill badge-info">{props.data.get("upload_user")}</span>
<td rowSpan="2" className="col-xs-1 torrent-small-width"> </div>
<h4 className="pull-right clickable" onClick={() => props.addTorrent(props.data.get("url"))}> <div className="align-self-end ml-3">
<i className="fa fa-cloud-download" aria-hidden="true"></i> <i className="fa fa-cloud-download clickable" onClick={() => props.addTorrent(props.data.get("url"))}></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,16 +18,14 @@ 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>
); );
} }
UserActivation.propTypes = { UserActivation.propTypes = {

View File

@ -28,9 +28,8 @@ 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 !== "" &&
@ -74,7 +73,6 @@ const UserLoginForm = (props) => {
</form> </form>
</div> </div>
</div> </div>
</div>
); );
} }
UserLoginForm.propTypes = { UserLoginForm.propTypes = {

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,9 +76,8 @@ 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 /> <hr />
<form className="form-horizontal" onSubmit={(ev) => handleSubmit(ev)}> <form className="form-horizontal" onSubmit={(ev) => handleSubmit(ev)}>
@ -132,7 +129,6 @@ const UserEdit = (props) => {
</form> </form>
</div> </div>
</div> </div>
</div>
); );
} }
UserEdit.propTypes = { UserEdit.propTypes = {

View File

@ -31,9 +31,8 @@ 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 !== "" &&
@ -75,7 +74,6 @@ const UserSignUp = (props) => {
</form> </form>
</div> </div>
</div> </div>
</div>
); );
} }
UserSignUp.propTypes = { UserSignUp.propTypes = {

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">
<h4>
<Logo {...ua} /> <Logo {...ua} />
<div className="col-xs-10"> <span>{props.data.get("description")}</span>
<dl className="dl-horizontal">
<dt>Description</dt>
<dd>{props.data.get("description")}</dd>
<dt>Last IP</dt>
<dd>{props.data.get("ip")}</dd>
<dt>Last used</dt>
<dd>{moment(props.data.get("last_used")).fromNow()}</dd>
<dt>Created</dt>
<dd>{moment(props.data.get("created_at")).fromNow()}</dd>
<dt>Device</dt>
<dd><Device {...ua.device}/></dd>
<dt>OS</dt>
<dd><OS {...ua.os}/></dd>
<dt>Browser</dt>
<dd><Browser {...ua.browser}/></dd>
</dl>
</div>
<Actions {...props} /> <Actions {...props} />
</h4>
</div>
<div className="card-body row">
<div className="col-12 col-md-6">
<p>Last IP: {props.data.get("ip")}</p>
<p>Last used: {moment(props.data.get("last_used")).fromNow()}</p>
<p>Created: {moment(props.data.get("created_at")).fromNow()}</p>
</div>
<div className="col-12 col-md-6">
<p>Device: <Device {...ua.device}/></p>
<p>OS: <OS {...ua.os}/></p>
<p>Browser: <Browser {...ua.browser}/></p>
</div>
</div> </div>
</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 user-token-action" className="fa fa-trash fa-lg pull-right clickable"
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