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">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" href="/favicon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="/favicon-16x16.png" sizes="16x16">

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"
// Styles
import "../less/app.less"
import "../scss/app.scss"
// React
import React from "react"
import ReactDOM from "react-dom"
import { Provider } from "react-redux"
import { Router, Route, Switch, Redirect } from "react-router-dom"
import Container from "react-bootstrap/Container"
// Auth
import { ProtectedRoute, AdminRoute } from "./auth"
@ -50,7 +51,7 @@ const App = () => (
<WsHandler />
<NavBar />
<Alert />
<div className="container-fluid">
<Container fluid>
<Switch>
<AdminRoute path="/admin" exact component={AdminPanel} />
<Route path="/users/profile" exact component={UserProfile} />
@ -71,7 +72,7 @@ const App = () => (
<Redirect to="/movies/explore/yts/seeds" />
}/>
</Switch>
</div>
</Container>
</div>
);

View File

@ -2,8 +2,7 @@ import React from "react"
import PropTypes from "prop-types"
export const Stats = props => (
<div>
<h2 className="hidden-xs">Stats</h2>
<div className="row d-flex flex-wrap">
<Stat
name="Movies"
count={props.stats.get("movies_count")}
@ -27,15 +26,15 @@ export const Stats = props => (
Stats.propTypes = { stats: PropTypes.object }
const Stat = props => (
<div className="col-xs-4">
<div className="panel panel-default">
<div className="panel-heading">
<h3 className="panel-title">
<div className="col-12 col-md-4 my-2">
<div className="card">
<div className="card-header">
<h3>
{props.name}
<span className="label label-info pull-right">{props.count}</span>
<span className="badge badge-pill badge-info pull-right">{props.count}</span>
</h3>
</div>
<div className="panel-body">
<div className="card-body">
<TorrentsStat data={props} />
</div>
</div>

View File

@ -6,12 +6,10 @@ import { Button, Modal } from "react-bootstrap"
import Toggle from "react-bootstrap-toggle";
export const UserList = props => (
<div>
<h2 className="hidden-xs">Users</h2>
<h3 className="visible-xs">Users</h3>
<table className="table">
<thead>
<tr className="active">
<div className="table-responsive my-2">
<table className="table table-striped">
<thead className="table-secondary">
<tr>
<th>#</th>
<th>Name</th>
<th>Activated</th>
@ -100,16 +98,18 @@ function UserEdit(props) {
&nbsp; Edit user - {props.data.get("Name")}
</Modal.Title>
</Modal.Header>
<Modal.Body bsClass="modal-body admin-edit-user-modal">
<Modal.Body bsPrefix="modal-body admin-edit-user-modal">
<form className="form-horizontal" onSubmit={(ev) => handleSubmit(ev)}>
<div className="form-group">
<label>Account status</label>
<Toggle className="pull-right" on="Activated" off="Deactivated" active={activated} onClick={() => setActivated(!activated)}
<Toggle className="pull-right" on="Activated" off="Deactivated" active={activated}
offstyle="danger" handlestyle="secondary" onClick={() => setActivated(!activated)}
/>
</div>
<div className="form-group">
<label>Admin status</label>
<Toggle className="pull-right" on="Admin" off="User" active={admin} onClick={() => setAdmin(!admin)} />
<Toggle className="pull-right" on="Admin" off="User" active={admin}
offstyle="info" handlestyle="secondary" onClick={() => setAdmin(!admin)} />
</div>
<div className="form-group">
<label className="control-label">Polochon URL</label>
@ -122,7 +122,7 @@ function UserEdit(props) {
</form>
</Modal.Body>
<Modal.Footer>
<Button bsStyle="success" onClick={handleSubmit}>Apply</Button>
<Button variant="success" onClick={handleSubmit}>Apply</Button>
<Button onClick={() => setModal(false)}>Close</Button>
</Modal.Footer>
</Modal>

View File

@ -1,80 +1,70 @@
import React from "react"
import PropTypes from "prop-types"
import { MenuItem } from "react-bootstrap"
import Dropdown from "react-bootstrap/Dropdown"
import RefreshIndicator from "./refresh"
export class WishlistButton extends React.PureComponent {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick(e) {
export const WishlistButton = (props) => {
const handleClick = (e) => {
e.preventDefault();
if (this.props.wishlisted) {
this.props.deleteFromWishlist(this.props.resourceId);
if (props.wishlisted) {
props.deleteFromWishlist(props.resourceId);
} else {
this.props.addToWishlist(this.props.resourceId);
props.addToWishlist(props.resourceId);
}
}
render() {
if (this.props.wishlisted) {
if (props.wishlisted) {
return (
<MenuItem onClick={this.handleClick}>
<Dropdown.Item onClick={handleClick}>
<span>
<i className="fa fa-bookmark"></i> Delete from wishlist
</span>
</MenuItem>
</Dropdown.Item>
);
} else {
return (
<MenuItem onClick={this.handleClick}>
<Dropdown.Item onClick={handleClick}>
<span>
<i className="fa fa-bookmark-o"></i> Add to wishlist
</span>
</MenuItem>
</Dropdown.Item>
);
}
}
}
export class DeleteButton extends React.PureComponent {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
export const DeleteButton = (props) => {
const handleClick = () => {
props.deleteFunc(props.resourceId, props.lastFetchUrl);
}
handleClick(e) {
e.preventDefault();
this.props.deleteFunc(this.props.resourceId, this.props.lastFetchUrl);
}
render() {
return (
<MenuItem onClick={this.handleClick}>
<Dropdown.Item onClick={handleClick}>
<span>
<i className="fa fa-trash"></i> Delete
</span>
</MenuItem>
</Dropdown.Item>
);
}
}
DeleteButton.propTypes = {
resourceId: PropTypes.string.isRequired,
lastFetchUrl: PropTypes.string,
deleteFunc: PropTypes.func.isRequired,
};
export class RefreshButton extends React.PureComponent {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
export const RefreshButton = (props) => {
const handleClick = () => {
if (props.fetching) { return; }
props.getDetails(props.resourceId);
}
handleClick(e) {
e.preventDefault();
if (this.props.fetching) {
return
}
this.props.getDetails(this.props.resourceId);
}
render() {
return (
<MenuItem onClick={this.handleClick}>
<RefreshIndicator refresh={this.props.fetching} />
</MenuItem>
<Dropdown.Item onClick={handleClick}>
<RefreshIndicator refresh={props.fetching} />
</Dropdown.Item>
);
}
}
RefreshButton.propTypes = {
fetching: PropTypes.bool.isRequired,
resourceId: PropTypes.string.isRequired,
getDetails: PropTypes.func.isRequired,
};

View File

@ -2,53 +2,42 @@ import React, { useState } from "react"
import PropTypes from "prop-types"
import { List } from "immutable"
import { Button, Dropdown, MenuItem, Modal } from "react-bootstrap"
import Modal from "react-bootstrap/Modal"
import Dropdown from "react-bootstrap/Dropdown"
import SplitButton from "react-bootstrap/SplitButton"
const DownloadButton = (props) => {
if (props.url === "") { return null; }
const size = props.xs ? "sm" : "";
const [showModal, setShowModal] = useState(false);
let btnSize = "btn-sm";
if (props.xs) {
btnSize = "btn-xs";
}
return (
<Dropdown id="streaming-buttons" className={props.customClassName} dropup>
<Button bsStyle="danger" className={btnSize} href={props.url}>
<span>
<i className="fa fa-download" aria-hidden="true"></i> Download
</span>
</Button>
<Dropdown.Toggle bsStyle="danger" className={btnSize}/>
<Dropdown.Menu>
<MenuItem eventKey="2" onClick={() => setShowModal(true)}>
<React.Fragment>
<SplitButton
drop="up"
variant="danger"
title="Download"
size={size}
id="download-button-id">
<Dropdown.Item eventKey="1" onClick={() => setShowModal(true)}>
<span>
<i className="fa fa-globe"></i> Stream in browser
</span>
</MenuItem>
</Dropdown.Menu>
</Dropdown.Item>
</SplitButton>
<Modal show={showModal} onHide={() => setShowModal(false)} dialogClassName="player-modal">
<Modal.Header closeButton>
<Modal.Title>
<i className="fa fa-globe"></i>
&nbsp;Browser streaming
</Modal.Title>
</Modal.Header>
<Modal show={showModal} onHide={() => setShowModal(false)} size="lg" centered>
<Modal.Header closeButton>{props.name}</Modal.Header>
<Modal.Body>
<Player
url={props.url}
subtitles={props.subtitles}
/>
<Player url={props.url} subtitles={props.subtitles} />
</Modal.Body>
</Modal>
</Dropdown>
</React.Fragment>
);
}
DownloadButton.propTypes = {
customClassName: PropTypes.string,
name: PropTypes.string,
xs: PropTypes.bool,
url: PropTypes.string.isRequired,
subtitles: PropTypes.instanceOf(List),
@ -60,7 +49,7 @@ const Player = (props) => {
const hasSubtitles = !(subtitles === undefined || subtitles === null || subtitles.size === 0);
return (
<div className="embed-responsive embed-responsive-16by9">
<video controls>
<video className="embed-responsive-item" controls>
<source src={props.url} type="video/mp4"/>
{hasSubtitles && subtitles.toIndexedSeq().map((el, index) => (
<track

View File

@ -1,15 +1,22 @@
import React from "react"
import PropTypes from "prop-types"
export default function ImdbLink(props) {
const link = `http://www.imdb.com/title/${props.imdbId}`;
let className = "btn btn-warning";
if (props.size) {
className += " btn-" + props.size;
const ImdbLink = (props) => {
if (!props.imdbId || props.imdbId === "") {
return null;
}
return(
<a type="button" className={className} href={link}>
<a
className={`btn btn-warning ${props.xs ? "btn-sm" : ""}`}
href={`http://www.imdb.com/title/${props.imdbId}`}>
<i className="fa fa-external-link"></i> IMDB
</a>
);
}
};
ImdbLink.propTypes = {
imdbId: PropTypes.string,
xs: PropTypes.bool,
};
export default ImdbLink;

View File

@ -1,6 +1,7 @@
import React from "react"
import PropTypes from "prop-types"
export default function RefreshIndicator(props) {
const RefreshIndicator = (props) => {
if (!props.refresh) {
return (
<span>
@ -15,4 +16,8 @@ export default function RefreshIndicator(props) {
);
}
}
RefreshIndicator.propTypes = {
refresh: PropTypes.bool.isRequired,
};
export default RefreshIndicator;

View File

@ -1,60 +1,78 @@
import React from "react"
import PropTypes from "prop-types"
import { List } from "immutable"
import { DropdownButton, MenuItem } from "react-bootstrap"
import Dropdown from "react-bootstrap/Dropdown"
import RefreshIndicator from "./refresh"
export default function SubtitlesButton(props) {
const btnSize = props.xs ? "xsmall" : "small";
const SubtitlesButton = (props) => {
const subtitles = props.subtitles;
const hasSubtitles = !(subtitles === undefined || subtitles === null || subtitles.size === 0);
const size = props.xs ? "sm" : "";
return (
<DropdownButton
bsStyle="success"
bsSize={btnSize}
title="Subtitles"
id="download-subtitles-button"
dropup>
<Dropdown drop="up">
<Dropdown.Toggle size={size} variant="success" id="movie-subtitles">
Subtitles
</Dropdown.Toggle>
<Dropdown.Menu>
<RefreshButton
type={props.type}
resourceID={props.resourceID}
season={props.season}
episode={props.episode}
fetching={props.fetching}
fetching={props.fetching ? true : false}
refreshSubtitles={props.refreshSubtitles}
/>
{hasSubtitles &&
<MenuItem divider></MenuItem>
<Dropdown.Divider />
}
{hasSubtitles && subtitles.toIndexedSeq().map(function(subtitle, index) {
return (
<MenuItem key={index} href={subtitle.get("url")}>
<Dropdown.Item key={index} href={subtitle.get("url")}>
<i className="fa fa-download"></i> &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 {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick(e) {
e.preventDefault();
if (this.props.fetching) {
export default SubtitlesButton;
const RefreshButton = (props) => {
const handleClick = () => {
if (props.fetching) {
return
}
this.props.refreshSubtitles(this.props.type, this.props.resourceID,
this.props.season, this.props.episode);
props.refreshSubtitles(props.type, props.resourceID,
props.season, props.episode);
}
render() {
return (
<MenuItem onClick={this.handleClick}>
<RefreshIndicator refresh={this.props.fetching} />
</MenuItem>
<Dropdown.Item onClick={handleClick}>
<RefreshIndicator refresh={props.fetching} />
</Dropdown.Item>
);
}
RefreshButton.propTypes = {
fetching: PropTypes.bool,
refreshSubtitles: PropTypes.func.isRequired,
type: PropTypes.string.isRequired,
resourceID: PropTypes.string.isRequired,
season: PropTypes.number,
episode: PropTypes.number,
}

View File

@ -2,17 +2,22 @@ import React from "react"
import { Map, List } from "immutable"
import PropTypes from "prop-types"
const ListDetails = (props) => (
<div className="col-xs-7 col-md-4">
<div className="affix">
<h1 className="hidden-xs">{props.data.get("title")}</h1>
<h3 className="visible-xs">{props.data.get("title")}</h3>
const ListDetails = (props) => {
if (props.loading) { return null }
if (props.data === undefined) { return null }
return (
<div className="col-8 col-md-4 list-details pl-1 d-flex align-items-start flex-column">
<div className="video-details flex-fill d-flex flex-column">
<h2 className="d-none d-sm-block">{props.data.get("title")}</h2>
<h4 className="d-block d-sm-none">{props.data.get("title")}</h4>
<TrackingLabel
wishlisted={props.data.get("wishlisted")}
trackedSeason={props.data.get("tracked_season")}
trackedEpisode={props.data.get("tracked_episode")}
/>
<h4>{props.data.get("year")}</h4>
<h4 className="d-none d-sm-block">{props.data.get("year")}</h4>
<h5 className="d-block d-sm-none">{props.data.get("year")}</h5>
<Runtime runtime={props.data.get("runtime")} />
<Genres genres={props.data.get("genres")} />
<Ratings
@ -26,13 +31,17 @@ const ListDetails = (props) => (
audioCodec={props.data.get("audio_codec")}
videoCodec={props.data.get("video_codec")}
/>
<p className="plot">{props.data.get("plot")}</p>
<p className="text text-break plot">{props.data.get("plot")}</p>
</div>
<div className="pb-1 align-self-end">
{props.children}
</div>
)
</div>
);
}
ListDetails.propTypes = {
data: PropTypes.instanceOf(Map).isRequired,
data: PropTypes.instanceOf(Map),
loading: PropTypes.bool,
children: PropTypes.object,
};
export default ListDetails;
@ -88,9 +97,11 @@ const TrackingLabel = (props) => {
}
return (
<span className="label label-default">
<p>
<span className="badge badge-secondary">
<i className="fa fa-bookmark"></i> {wishlistStr}
</span>
</p>
);
}
TrackingLabel.propTypes = {
@ -106,11 +117,11 @@ const PolochonMetadata = (props) => {
return (
<p className="spaced-icons">
<span className="label label-default">{props.quality}</span>
<span className="label label-default">{props.container} </span>
<span className="label label-default">{props.videoCodec}</span>
<span className="label label-default">{props.audioCodec}</span>
<span className="label label-default">{props.releaseGroup}</span>
<span className="mx-1 badge badge-pill badge-secondary">{props.quality}</span>
<span className="mx-1 badge badge-pill badge-secondary">{props.container} </span>
<span className="mx-1 badge badge-pill badge-secondary">{props.videoCodec}</span>
<span className="mx-1 badge badge-pill badge-secondary">{props.audioCodec}</span>
<span className="mx-1 badge badge-pill badge-secondary">{props.releaseGroup}</span>
</p>
);
}

View File

@ -1,6 +1,6 @@
import React from "react"
import { withRouter } from "react-router-dom"
import { Form, FormGroup, FormControl, ControlLabel } from "react-bootstrap"
import { Form, FormGroup, FormControl, FormLabel } from "react-bootstrap"
class ExplorerOptions extends React.PureComponent {
constructor(props) {
@ -61,10 +61,10 @@ class ExplorerOptions extends React.PureComponent {
<div className="row">
<div className="col-xs-12 col-md-6">
<FormGroup>
<ControlLabel>Source</ControlLabel>
<FormLabel>Source</FormLabel>
<FormControl
bsClass="form-control input-sm"
componentClass="select"
bsPrefix="form-control input-sm"
as="select"
onChange={this.handleSourceChange}
value={source}
>
@ -77,10 +77,10 @@ class ExplorerOptions extends React.PureComponent {
</div>
<div className="col-xs-12 col-md-6">
<FormGroup>
<ControlLabel>Category</ControlLabel>
<FormLabel>Category</FormLabel>
<FormControl
bsClass="form-control input-sm"
componentClass="select"
bsPrefix="form-control input-sm"
as="select"
onChange={this.handleCategoryChange}
value={category}
>

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 {
constructor(props) {
super(props);
this.state = { filter: "" };
this.handleChange = this.handleChange.bind(this);
}
handleChange(ev) {
if (ev) { ev.preventDefault(); }
const value = this.input.value;
if (this.state.filter === value) { return }
this.setState({ filter: value });
const ListFilter = (props) => {
const [filter, setFilter] = useState("");
useEffect(() => {
// Start filtering at 3 chars
if (value.length >= 3) {
this.props.updateFilter(value);
} else {
this.props.updateFilter("");
if (filter.length >= 3) {
props.updateFilter(filter);
}
}
render() {
}, [filter]);
return (
<div className="row">
<div className="col-xs-12 col-md-12 list-filter">
<form className="input-group" onSubmit={(ev) => this.handleChange(ev)}>
<input
className="form-control input-sm"
placeholder={this.props.placeHolder}
onChange={this.handleChange}
ref={(input) => this.input = input}
value={this.state.filter}
/>
<span className="input-group-btn hidden-xs">
<button className="btn btn-default btn-sm" type="button">Filter</button>
</span>
</form>
<div className="input-group input-group-sm">
<input type="text" className="form-control"
placeholder={props.placeHolder}
onChange={(e) => setFilter(e.target.value)}
value={filter} />
<div className="input-group-append d-none d-md-block">
<span className="input-group-text">Filter</span>
</div>
</div>
);
}
}
ListFilter.propTypes = {
updateFilter: PropTypes.func.isRequired,
placeHolder: PropTypes.string.isRequired,
};
export default ListFilter;

View File

@ -1,19 +1,23 @@
import React from "react"
import PropTypes from "prop-types"
import { Map } from "immutable"
export default function Poster(props) {
const selected = props.selected ? " thumbnail-selected" : "";
const imgClass = "thumbnail" + selected;
const Poster = (props) => {
const className = props.selected ? "border-primary thumbnail-selected" : "border-secondary";
return (
<div>
<div className="col-xs-12 col-sm-6 col-md-3 col-lg-2">
<a className={imgClass}>
<img
src={props.data.get("poster_url")}
onClick={props.onClick}
onDoubleClick={props.onDoubleClick}
className={`my-1 m-md-2 img-thumbnail ${className}`}
/>
</a>
</div>
</div>
);
}
Poster.propTypes = {
data: PropTypes.instanceOf(Map),
selected: PropTypes.bool,
onClick: PropTypes.func,
onDoubleClick: PropTypes.func,
};
export default Poster;

View File

@ -1,8 +1,8 @@
import React from "react"
import { Map } from "immutable"
import React, { useState, useEffect } from "react"
import PropTypes from "prop-types"
import { OrderedMap, Map } from "immutable"
import fuzzy from "fuzzy";
import InfiniteScroll from "react-infinite-scroller";
import InfiniteScroll from "react-infinite-scroll-component";
import ListFilter from "./filter"
import ExplorerOptions from "./explorerOptions"
@ -10,69 +10,24 @@ import Poster from "./poster"
import Loader from "../loader/loader"
const DEFAULT_ADD_EXTRA_ITEMS = 30;
export default class ListPosters extends React.PureComponent {
constructor(props) {
super(props);
this.loadMore = this.loadMore.bind(this);
this.state = this.getNextState(props);
}
loadMore() {
// Nothing to do if the app is loading
if (this.props.loading) {
return;
}
if (this.props.data === undefined) {
return;
}
this.setState(this.getNextState(this.props));
}
getNextState(props) {
let totalListSize = props.data !== undefined ? props.data.size : 0;
let currentListSize = (this.state && this.state.items) ? this.state.items : 0;
let nextListSize = currentListSize + DEFAULT_ADD_EXTRA_ITEMS;
let hasMore = true;
if (nextListSize >= totalListSize) {
nextListSize = totalListSize;
hasMore = false;
}
return {
items: nextListSize,
hasMore: hasMore,
};
}
componentWillReceiveProps(nextProps) {
if (this.props.data === undefined) { return }
if (nextProps.data === undefined) { return }
if (this.props.data.size !== nextProps.data.size) {
this.setState(this.getNextState(nextProps));
}
}
render() {
let elmts = this.props.data;
const ListPosters = (props) => {
let elmts = props.data;
const listSize = elmts !== undefined ? elmts.size : 0;
const colSize = (listSize !== 0) ? "col-xs-5 col-md-8" : "col-xs-12";
// Filter the list of elements
if (this.props.filter !== "") {
elmts = elmts.filter((v) => fuzzy.test(this.props.filter, v.get("title")), this);
if (props.filter !== "") {
elmts = elmts.filter((v) => fuzzy.test(props.filter, v.get("title")));
} else {
elmts = elmts.slice(0, this.state.items);
elmts = elmts.slice(0, listSize.items);
}
// Chose when to display filter / explore options
let displayFilter = true;
if ((this.props.params
&& this.props.params.category
&& this.props.params.category !== ""
&& this.props.params.source
&& this.props.params.source !== "")
if ((props.params
&& props.params.category
&& props.params.category !== ""
&& props.params.source
&& props.params.source !== "")
|| (listSize === 0)) {
displayFilter = false;
}
@ -84,89 +39,53 @@ export default class ListPosters extends React.PureComponent {
}
return (
<div className={colSize}>
<div className="col px-1">
{displayFilter &&
<ListFilter
updateFilter={this.props.updateFilter}
placeHolder={this.props.placeHolder}
updateFilter={props.updateFilter}
placeHolder={props.placeHolder}
/>
}
<ExplorerOptions
type={this.props.type}
type={props.type}
display={displayExplorerOptions}
params={this.props.params}
options={this.props.exploreOptions}
params={props.params}
options={props.exploreOptions}
/>
<Posters
elmts={elmts}
loading={this.props.loading}
hasMore={this.state.hasMore}
loadMore={this.loadMore}
selectedImdbId={this.props.selectedImdbId}
selectPoster={this.props.onClick}
onDoubleClick={this.props.onDoubleClick}
onKeyEnter={this.props.onKeyEnter}
loading={props.loading}
selectedImdbId={props.selectedImdbId}
selectPoster={props.onClick}
onDoubleClick={props.onDoubleClick}
onKeyEnter={props.onKeyEnter}
/>
</div>
);
}
}
ListPosters.propTypes = {
data: PropTypes.instanceOf(OrderedMap),
onClick: PropTypes.func,
onDoubleClick: PropTypes.func,
onKeyEnter: PropTypes.func,
selectedImdbId: PropTypes.string,
loading: PropTypes.bool.isRequired,
params: PropTypes.object.isRequired,
exploreOptions: PropTypes.instanceOf(Map),
type: PropTypes.string.isRequired,
placeHolder: PropTypes.string.isRequired,
updateFilter: PropTypes.func.isRequired,
filter: PropTypes.string,
};
class Posters extends React.PureComponent {
constructor(props) {
super(props);
}
export default ListPosters;
move(event) {
// Detect which direction to go
const keyToDiff = Map({
"ArrowRight": 1,
"l": 1,
"ArrowLeft": -1,
"h": -1,
"ArrowUp": -6,
"k": -6,
"ArrowDown": 6,
"j": 6,
});
if (event.key === "Enter") {
this.props.onKeyEnter(this.props.selectedImdbId);
}
if (! keyToDiff.has(event.key) ) {
return;
}
var diff = keyToDiff.get(event.key);
// Don't scroll when changing poster
event.preventDefault();
// Get the index of the currently selected item
const idx = this.props.elmts.keySeq().findIndex(k => k === this.props.selectedImdbId);
var newIdx = idx + diff;
// Handle edge cases
if (newIdx > this.props.elmts.size -1) {
newIdx = this.props.elmts.size -1;
} else if (newIdx < 0) {
newIdx = 0;
}
// Get the imdbID of the newly selected item
var selectedImdb = Object.keys(this.props.elmts.toJS())[newIdx];
// Select the movie
this.props.selectPoster(selectedImdb);
}
render() {
if (this.props.loading) {
const Posters = (props) => {
if (props.loading) {
return (<Loader />);
}
if (this.props.elmts.size === 0) {
if (props.elmts.size === 0) {
return (
<div className="jumbotron">
<h2>No result</h2>
@ -174,45 +93,53 @@ class Posters extends React.PureComponent {
);
}
const addMoreCount = 20;
const [size, setSize] = useState(0);
useEffect(() => {
loadMore()
}, [props.elmts.size]);
const hasMore = () => (size !== props.elmts.size);
const loadMore = () => {
if (!hasMore()) { return }
const newSize = (((size + addMoreCount) >= props.elmts.size)
? props.elmts.size
: size + addMoreCount);
setSize(newSize);
}
return (
<div tabIndex="0"
onKeyDown={(event) => this.move(event)}
className="poster-list"
>
<InfiniteScroll
hasMore={this.props.hasMore}
loadMore={this.props.loadMore}
className="row"
className="poster-list d-block flex-row flex-wrap justify-content-around"
dataLength={size}
next={loadMore}
hasMore={hasMore()}
loader={<Loader />}
>
{this.props.elmts.toIndexedSeq().map(function(movie, index) {
const imdbId = movie.get("imdb_id");
const selected = (imdbId === this.props.selectedImdbId) ? true : false;
let clearFixes = [];
if ((index % 6) === 0) { clearFixes.push("clearfix visible-lg") };
if ((index % 4) === 0) { clearFixes.push("clearfix visible-md") };
if ((index % 2) === 0) { clearFixes.push("clearfix visible-sm") };
{props.elmts.slice(0, size).toIndexedSeq().map(function(el, index) {
const imdbId = el.get("imdb_id");
const selected = (imdbId === props.selectedImdbId) ? true : false;
return (
<div key={imdbId}>
{clearFixes.length > 0 && clearFixes.map(function(el, i) {
return (
<div key={`clearfix-${imdbId}-${i}`} className={el}></div>
);
})}
<Poster
data={movie}
key={`poster-${imdbId}`}
data={el}
key={`poster-${imdbId}-${index}`}
selected={selected}
onClick={() => this.props.selectPoster(imdbId)}
onDoubleClick={() => this.props.onDoubleClick(imdbId)}
onClick={() => props.selectPoster(imdbId)}
onDoubleClick={() => props.onDoubleClick(imdbId)}
/>
</div>
)
} ,this)}
</InfiniteScroll>
</div>
);
}
}
Posters.propTypes = {
elmts: PropTypes.instanceOf(OrderedMap),
selectedImdbId: PropTypes.string,
loading: PropTypes.bool.isRequired,
onDoubleClick: PropTypes.func,
onKeyEnter: PropTypes.func,
selectPoster: PropTypes.func,
};

View File

@ -3,7 +3,7 @@ import Loading from "react-loading"
const Loader = () => (
<div className="row" id="container">
<div className="col-md-6 col-md-offset-3">
<div className="col-12 col-md-6 offset-md-3">
<Loading
type="bars"
height={"100%"}

View File

@ -3,6 +3,7 @@ import Loader from "../loader/loader"
import PropTypes from "prop-types"
import { Map, List } from "immutable"
// TODO: udpate this
import { OverlayTrigger, Tooltip } from "react-bootstrap"
const Modules = (props) => {
@ -11,8 +12,7 @@ const Modules = (props) => {
}
return (
<div>
<h2>Modules</h2>
<div className="row">
{props.modules && props.modules.keySeq().map((value, key) => (
<ModulesByVideoType
key={key}
@ -33,14 +33,12 @@ const capitalize = (string) =>
string.charAt(0).toUpperCase() + string.slice(1);
const ModulesByVideoType = (props) => (
<div className="col-md-6 col-xs-12">
<div className="panel panel-default">
<div className="panel-heading">
<h3 className="panel-title">
{capitalize(props.videoType)} {/* Movie or Show */}
</h3>
<div className="col-12 col-md-6">
<div className="card mb-3">
<div className="card-header">
<h3>{`${capitalize(props.videoType)} modules`}</h3>
</div>
<div className="panel-body">
<div className="card-body">
{props.data.keySeq().map((value, key) => (
<ModuleByType
key={key}
@ -58,8 +56,8 @@ ModulesByVideoType.propTypes = {
};
const ModuleByType = (props) => (
<div className="col-md-3 col-xs-6">
<h4>{props.type} {/* Detailer / Explorer / ... */}</h4>
<div>
<h4>{capitalize(props.type)}</h4>
<table className="table">
<tbody>
{props.data.map(function(value, key) {
@ -81,35 +79,37 @@ ModuleByType.propTypes = {
};
const Module = (props) => {
let iconClass, prettyStatus, labelClass;
let iconClass, prettyStatus, badgeClass;
const name = props.data.get("name");
switch(props.data.get("status")) {
case "ok":
iconClass = "fa fa-check-circle"
labelClass = "label label-success"
badgeClass = "badge badge-pill badge-success"
prettyStatus = "OK"
break;
case "fail":
iconClass = "fa fa-times-circle"
labelClass = "label label-danger"
badgeClass = "badge badge-pill badge-danger"
prettyStatus = "Fail"
break;
case "not_implemented":
iconClass = "fa fa-question-circle"
labelClass = "label label-default"
badgeClass = "badge badge-pill badge-default"
prettyStatus = "Not implemented"
break;
default:
iconClass = "fa fa-question-circle"
labelClass = "label label-warning"
badgeClass = "badge badge-pill badge-warning"
prettyStatus = "Unknown"
}
const tooltip = (
<Tooltip id={`tooltip-status-${name}`}>
<p><span className={labelClass}>Status: {prettyStatus}</span></p>
<p><span className={badgeClass}>Status: {prettyStatus}</span></p>
{props.data.get("error") !== "" &&
<p>Error: {props.data.get("error")}</p>
}
</Tooltip>
);
@ -118,7 +118,7 @@ const Module = (props) => {
<th>{name}</th>
<td>
<OverlayTrigger placement="right" overlay={tooltip}>
<span className={labelClass}>
<span className={badgeClass}>
<i className={iconClass}></i> {prettyStatus}
</span>
</OverlayTrigger>

View File

@ -1,11 +1,16 @@
import React from "react"
import PropTypes from "prop-types"
import { WishlistButton, DeleteButton, RefreshButton } from "../buttons/actions"
import { DropdownButton } from "react-bootstrap"
import Dropdown from "react-bootstrap/Dropdown"
export default function ActionsButton(props) {
return (
<DropdownButton className="btn btn-default btn-sm" title="Actions" id="actions-button" dropup>
const ActionsButton = (props) => (
<Dropdown drop="up">
<Dropdown.Toggle variant="secondary" id="movie-button-actions">
Actions
</Dropdown.Toggle>
<Dropdown.Menu>
<RefreshButton
fetching={props.fetching}
resourceId={props.movieId}
@ -16,7 +21,6 @@ export default function ActionsButton(props) {
resourceId={props.movieId}
lastFetchUrl={props.lastFetchUrl}
deleteFunc={props.deleteMovie}
isUserAdmin={props.isUserAdmin}
/>
}
<WishlistButton
@ -25,6 +29,19 @@ export default function ActionsButton(props) {
addToWishlist={props.addToWishlist}
deleteFromWishlist={props.deleteFromWishlist}
/>
</DropdownButton>
</Dropdown.Menu>
</Dropdown>
);
ActionsButton.propTypes = {
hasMovie: PropTypes.bool.isRequired,
fetching: PropTypes.bool.isRequired,
wishlisted: PropTypes.bool.isRequired,
movieId: PropTypes.string.isRequired,
getDetails: PropTypes.func.isRequired,
addToWishlist: PropTypes.func.isRequired,
deleteFromWishlist: PropTypes.func.isRequired,
deleteMovie: PropTypes.func.isRequired,
lastFetchUrl: PropTypes.string.isRequired,
}
export default ActionsButton;

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 PropTypes from "prop-types"
import { OrderedMap, Map } from "immutable"
import { connect } from "react-redux"
import { addTorrent } from "../../actions/torrents"
import { refreshSubtitles } from "../../actions/subtitles"
import { addMovieToWishlist, deleteMovie, deleteMovieFromWishlist,
getMovieDetails, selectMovie, updateFilter } from "../../actions/movies"
import DownloadButton from "../buttons/download"
import SubtitlesButton from "../buttons/subtitles"
import ImdbButton from "../buttons/imdb"
import TorrentsButton from "./torrents"
import ActionsButton from "./actions"
import ListPosters from "../list/posters"
import ListDetails from "../list/details"
import MovieButtons from "./buttons"
import Row from "react-bootstrap/Row"
function mapStateToProps(state) {
return {
@ -29,90 +29,60 @@ const mapDispatchToProps = {
refreshSubtitles, updateFilter,
};
function MovieButtons(props) {
const hasMovie = (props.movie.get("polochon_url") !== "");
return (
<div className="list-details-buttons btn-toolbar">
<ActionsButton
fetching={props.movie.get("fetchingDetails")}
movieId={props.movie.get("imdb_id")}
getDetails={props.getMovieDetails}
deleteMovie={props.deleteMovie}
hasMovie={hasMovie}
wishlisted={props.movie.get("wishlisted")}
addToWishlist={props.addToWishlist}
deleteFromWishlist={props.deleteFromWishlist}
lastFetchUrl={props.lastFetchUrl}
/>
<TorrentsButton
movieTitle={props.movie.get("title")}
torrents={props.movie.get("torrents")}
addTorrent={props.addTorrent}
/>
<DownloadButton
url={props.movie.get("polochon_url")}
subtitles={props.movie.get("subtitles")}
/>
{props.movie.get("polochon_url") !== "" &&
<SubtitlesButton
fetching={props.movie.get("fetchingSubtitles")}
subtitles={props.movie.get("subtitles")}
refreshSubtitles={props.refreshSubtitles}
resourceID={props.movie.get("imdb_id")}
type="movie"
/>
}
<ImdbButton imdbId={props.movie.get("imdb_id")} size="sm"/>
</div>
);
}
class MovieList extends React.PureComponent {
constructor(props) {
super(props);
}
render() {
const MovieList = (props) => {
let selectedMovie = undefined;
if (this.props.movies !== undefined && this.props.movies.has(this.props.selectedImdbId)) {
selectedMovie = this.props.movies.get(this.props.selectedImdbId);
if (props.movies !== undefined &&
props.movies.has(props.selectedImdbId)) {
selectedMovie = props.movies.get(props.selectedImdbId);
}
return (
<div className="row" id="container">
<Row>
<ListPosters
data={this.props.movies}
data={props.movies}
type="movies"
placeHolder="Filter movies..."
exploreOptions={this.props.exploreOptions}
selectedImdbId={this.props.selectedImdbId}
updateFilter={this.props.updateFilter}
filter={this.props.filter}
onClick={this.props.selectMovie}
exploreOptions={props.exploreOptions}
selectedImdbId={props.selectedImdbId}
updateFilter={props.updateFilter}
filter={props.filter}
onClick={props.selectMovie}
onDoubleClick={function() { return; }}
onKeyEnter={function() { return; }}
params={this.props.match.params}
loading={this.props.loading}
params={props.match.params}
loading={props.loading}
/>
{selectedMovie !== undefined &&
<ListDetails data={selectedMovie}>
<ListDetails data={selectedMovie} loading={props.loading}>
<MovieButtons
movie={selectedMovie}
getMovieDetails={this.props.getMovieDetails}
addTorrent={this.props.addTorrent}
deleteMovie={this.props.deleteMovie}
addToWishlist={this.props.addMovieToWishlist}
deleteFromWishlist={this.props.deleteMovieFromWishlist}
lastFetchUrl={this.props.lastFetchUrl}
refreshSubtitles={this.props.refreshSubtitles}
getMovieDetails={props.getMovieDetails}
addTorrent={props.addTorrent}
deleteMovie={props.deleteMovie}
addToWishlist={props.addMovieToWishlist}
deleteFromWishlist={props.deleteMovieFromWishlist}
lastFetchUrl={props.lastFetchUrl}
refreshSubtitles={props.refreshSubtitles}
/>
</ListDetails>
}
</div>
</Row>
);
}
}
MovieList.propTypes = {
movies: PropTypes.instanceOf(OrderedMap),
exploreOptions: PropTypes.instanceOf(Map),
selectedImdbId: PropTypes.string,
filter: PropTypes.string,
loading: PropTypes.bool,
lastFetchUrl: PropTypes.string,
updateFilter: PropTypes.func,
selectMovie: PropTypes.func,
addMovieToWishlist: PropTypes.func,
deleteMovieFromWishlist: PropTypes.func,
deleteMovie: PropTypes.func,
addTorrent: PropTypes.func,
refreshSubtitles: PropTypes.func,
getMovieDetails: PropTypes.func,
match: PropTypes.object,
};
export default connect(mapStateToProps, mapDispatchToProps)(MovieList);

View File

@ -1,55 +1,11 @@
import React from "react"
import PropTypes from "prop-types"
import { List } from "immutable"
import { DropdownButton, MenuItem } from "react-bootstrap"
export default class TorrentsButton extends React.PureComponent {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick(e, url) {
e.preventDefault();
this.props.addTorrent(url);
}
render() {
const entries = buildMenuItems(this.props.torrents);
const searchUrl = `#/torrents/search/movies/${encodeURI(this.props.movieTitle)}`;
return (
<DropdownButton className="btn btn-default btn-sm" title="Torrents" id="download-torrents-button" dropup>
<MenuItem className="text-warning" header>Advanced</MenuItem>
<MenuItem href={searchUrl} >
<i className="fa fa-search" aria-hidden="true"></i> Search
</MenuItem>
{entries.length > 0 &&
<MenuItem divider></MenuItem>
}
{entries.map(function(e, index) {
switch (e.type) {
case "header":
return (
<MenuItem key={index} className="text-warning" header>
{e.value}
</MenuItem>
);
case "divider":
return (
<MenuItem key={index} divider></MenuItem>
);
case "entry":
return (
<MenuItem key={index} href={e.url} onClick={(event) => this.handleClick(event, e.url)}>
{e.quality}
</MenuItem>
);
}
}, this)}
</DropdownButton>
);
}
}
import Dropdown from "react-bootstrap/Dropdown"
function buildMenuItems(torrents) {
if (!torrents) {
if (!torrents || torrents.size === 0) {
return [];
}
@ -83,3 +39,54 @@ function buildMenuItems(torrents) {
return entries;
}
const TorrentsButton = (props) => {
const entries = buildMenuItems(props.torrents);
const searchUrl = `#/torrents/search/movies/${encodeURI(props.movieTitle)}`;
return (
<Dropdown drop="up">
<Dropdown.Toggle variant="secondary" id="movie-torrents">
Torrents
</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Header>
<span className="text-warning">Advanced</span>
</Dropdown.Header>
<Dropdown.Item href={searchUrl} >
<i className="fa fa-search" aria-hidden="true"></i> Search
</Dropdown.Item>
{entries.length > 0 &&
<Dropdown.Divider />
}
{entries.map((e, index) => {
switch (e.type) {
case "header":
return (
<Dropdown.Header key={index}>
<span className="text-warning">{e.value}</span>
</Dropdown.Header>
);
case "divider":
return (
<Dropdown.Divider key={index}/>
);
case "entry":
return (
<Dropdown.Item key={index} onClick={() => props.addTorrent(e.url)}>
{e.quality}
</Dropdown.Item>
);
}
})}
</Dropdown.Menu>
</Dropdown>
);
}
TorrentsButton.propTypes = {
torrents: PropTypes.instanceOf(List),
addTorrent: PropTypes.func.isRequired,
movieTitle: PropTypes.string.isRequired,
}
export default TorrentsButton;

View File

@ -1,10 +1,13 @@
import React, { useState } from "react"
import { Route, Link } from "react-router-dom"
import { Route } from "react-router-dom"
import { connect } from "react-redux"
import { Nav, Navbar, NavItem, NavDropdown, MenuItem } from "react-bootstrap"
import { LinkContainer } from "react-router-bootstrap"
import PropTypes from "prop-types"
import Nav from "react-bootstrap/Nav"
import Navbar from "react-bootstrap/Navbar"
import NavDropdown from "react-bootstrap/NavDropdown"
const mapStateToProps = (state) => {
let torrentCount = 0;
if (state.torrentStore.has("torrents") && state.torrentStore.get("torrents") !== undefined) {
@ -21,24 +24,20 @@ const AppNavBar = (props) => {
const [expanded, setExpanded] = useState(false);
return (
<Navbar
fluid fixedTop collapseOnSelect
expanded={expanded} onToggle={() => setExpanded(!expanded)}>
<Navbar.Header>
<Navbar fixed="top" collapseOnSelect bg="dark" variant="dark"
expanded={expanded} expand="sm" onToggle={() => setExpanded(!expanded)}>
<LinkContainer to="/">
<Navbar.Brand><Link to="/">Canapé</Link></Navbar.Brand>
<Navbar.Brand>Canapé</Navbar.Brand>
</LinkContainer>
<Navbar.Toggle />
</Navbar.Header>
<Navbar.Collapse>
<Nav className="mr-auto">
<MoviesDropdown />
<ShowsDropdown />
<WishlistDropdown />
<TorrentsDropdown torrentsCount={props.torrentCount} />
<UserDropdown
username={props.username}
isAdmin={props.isAdmin}
/>
</Nav>
<Nav>
<Route path="/movies" render={(props) =>
<Search
placeholder="Search movies"
@ -53,9 +52,14 @@ const AppNavBar = (props) => {
history={props.history}
/>
}/>
<UserDropdown
username={props.username}
isAdmin={props.isAdmin}
/>
</Nav>
</Navbar.Collapse>
</Navbar>
)
);
}
AppNavBar.propTypes = {
torrentCount: PropTypes.number.isRequired,
@ -94,84 +98,74 @@ Search.propTypes = {
};
const MoviesDropdown = () => (
<Nav>
<NavDropdown title="Movies" id="navbar-movies-dropdown">
<LinkContainer to="/movies/explore/yts/seeds">
<MenuItem>Discover</MenuItem>
<NavDropdown.Item>Discover</NavDropdown.Item>
</LinkContainer>
<LinkContainer to="/movies/polochon">
<MenuItem>My movies</MenuItem>
<NavDropdown.Item>My movies</NavDropdown.Item>
</LinkContainer>
</NavDropdown>
)
const ShowsDropdown = () => (
<NavDropdown title="Shows" id="navbar-shows-dropdown">
<LinkContainer to="/shows/explore/eztv/rating">
<NavDropdown.Item>Discover</NavDropdown.Item>
</LinkContainer>
<LinkContainer to="/shows/polochon">
<NavDropdown.Item>My shows</NavDropdown.Item>
</LinkContainer>
</NavDropdown>
);
const UserDropdown = (props) => (
<Nav>
<NavDropdown title={props.username} alignRight>
{props.isAdmin &&
<LinkContainer to="/admin">
<NavDropdown.Item>Admin Panel</NavDropdown.Item>
</LinkContainer>
}
<LinkContainer to="/users/profile">
<NavDropdown.Item>Profile</NavDropdown.Item>
</LinkContainer>
<LinkContainer to="/users/tokens">
<NavDropdown.Item>Tokens</NavDropdown.Item>
</LinkContainer>
<LinkContainer to="/users/logout">
<NavDropdown.Item>Logout</NavDropdown.Item>
</LinkContainer>
</NavDropdown>
</Nav>
)
const ShowsDropdown = () => (
<Nav>
<NavDropdown title="Shows" id="navbar-shows-dropdown">
<LinkContainer to="/shows/explore/eztv/rating">
<NavItem>Discover</NavItem>
</LinkContainer>
<LinkContainer to="/shows/polochon">
<NavItem>My shows</NavItem>
</LinkContainer>
</NavDropdown>
</Nav>
);
function UserDropdown(props) {
return (
<Nav pullRight>
<NavDropdown title={props.username} id="navbar-dropdown-right">
{props.isAdmin &&
<LinkContainer to="/admin">
<MenuItem>Admin Panel</MenuItem>
</LinkContainer>
}
<LinkContainer to="/users/profile">
<MenuItem>Profile</MenuItem>
</LinkContainer>
<LinkContainer to="/users/tokens">
<MenuItem>Tokens</MenuItem>
</LinkContainer>
<LinkContainer to="/users/logout">
<MenuItem>Logout</MenuItem>
</LinkContainer>
</NavDropdown>
</Nav>
);
}
UserDropdown.propTypes = {
username: PropTypes.string.isRequired,
isAdmin: PropTypes.bool.isRequired,
};
const WishlistDropdown = () => (
<Nav>
<NavDropdown title="Wishlist" id="navbar-wishlit-dropdown">
<LinkContainer to="/movies/wishlist">
<MenuItem>Movies</MenuItem>
<NavDropdown.Item>Movies</NavDropdown.Item>
</LinkContainer>
<LinkContainer to="/shows/wishlist">
<MenuItem>Shows</MenuItem>
<NavDropdown.Item>Shows</NavDropdown.Item>
</LinkContainer>
</NavDropdown>
</Nav>
);
const TorrentsDropdown = (props) => {
const title = (<TorrentsDropdownTitle torrentsCount={props.torrentsCount} />)
return(
<Nav>
<NavDropdown title={title} id="navbar-wishlit-dropdown">
<LinkContainer to="/torrents/list">
<MenuItem>Downloads</MenuItem>
<NavDropdown.Item>Downloads</NavDropdown.Item>
</LinkContainer>
<LinkContainer to="/torrents/search">
<MenuItem>Search</MenuItem>
<NavDropdown.Item>Search</NavDropdown.Item>
</LinkContainer>
</NavDropdown>
</Nav>
);
}
TorrentsDropdown.propTypes = { torrentsCount: PropTypes.number.isRequired };
@ -186,7 +180,7 @@ const TorrentsDropdownTitle = (props) => {
return (
<span> Torrents
<span>
&nbsp; <span className="label label-info">{props.torrentsCount}</span>
&nbsp; <span className="badge badge-info badge-pill">{props.torrentsCount}</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 { withRouter } from "react-router"
import { addTorrent } from "../../actions/torrents"
import { refreshSubtitles } from "../../actions/subtitles"
import { addShowToWishlist, deleteShowFromWishlist, getEpisodeDetails, fetchShowDetails } from "../../actions/shows"
@ -10,12 +13,17 @@ import SubtitlesButton from "../buttons/subtitles"
import ImdbButton from "../buttons/imdb"
import RefreshIndicator from "../buttons/refresh"
import { OverlayTrigger, Tooltip } from "react-bootstrap"
import { Button, Dropdown, MenuItem } from "react-bootstrap"
import Tooltip from "react-bootstrap/Tooltip"
import OverlayTrigger from "react-bootstrap/OverlayTrigger"
import Dropdown from "react-bootstrap/Dropdown"
import SplitButton from "react-bootstrap/SplitButton"
// Helper to change 1 to 01
const pad = (d) => (d < 10) ? "0" + d.toString() : d.toString();
function mapStateToProps(state) {
return {
loading: state.showStore.loading,
loading: state.showStore.get("loading"),
show: state.showStore.get("show"),
};
}
@ -24,188 +32,164 @@ const mapDispatchToProps = {
fetchShowDetails, getEpisodeDetails, refreshSubtitles,
};
class ShowDetails extends React.Component {
render() {
// Loading
if (this.props.loading) {
const ShowDetails = (props) => {
if (props.loading) {
return (<Loader />);
}
return (
<div className="row" id="container">
<div className="row no-gutters">
<Header
data={this.props.show}
addToWishlist={this.props.addShowToWishlist}
deleteFromWishlist={this.props.deleteShowFromWishlist}
data={props.show}
addToWishlist={props.addShowToWishlist}
deleteFromWishlist={props.deleteShowFromWishlist}
/>
<SeasonsList
data={this.props.show}
router={this.props.router}
addTorrent={this.props.addTorrent}
addToWishlist={this.props.addShowToWishlist}
getEpisodeDetails={this.props.getEpisodeDetails}
refreshSubtitles={this.props.refreshSubtitles}
data={props.show}
addTorrent={props.addTorrent}
addToWishlist={props.addShowToWishlist}
getEpisodeDetails={props.getEpisodeDetails}
refreshSubtitles={props.refreshSubtitles}
/>
</div>
);
}
}
ShowDetails.propTypes = {
loading: PropTypes.bool,
show: PropTypes.instanceOf(Map),
deleteShowFromWishlist: PropTypes.func,
addShowToWishlist: PropTypes.func,
addTorrent: PropTypes.func,
refreshSubtitles: PropTypes.func,
getEpisodeDetails: PropTypes.func,
};
export default connect(mapStateToProps, mapDispatchToProps)(ShowDetails);
function Header(props){
return (
<div className="col-xs-12 col-sm-10 col-sm-offset-1 col-md-10 col-md-offset-1">
<div className="panel panel-default">
<div className="panel-body">
<HeaderThumbnail data={props.data} />
<HeaderDetails
data={props.data}
addToWishlist={props.addToWishlist}
deleteFromWishlist={props.deleteFromWishlist}
/>
const Header = (props) => (
<div className="card col-12 col-md-10 offset-md-1 mb-3">
<div className="row no-gutters">
<div className="col-4">
<img className="img-fluid" src={props.data.get("poster_url")} />
</div>
</div>
</div>
);
}
function HeaderThumbnail(props){
return (
<div className="col-xs-12 col-sm-2 text-center">
<img src={props.data.get("poster_url")}
className="show-thumbnail thumbnail-selected img-thumbnail img-responsive"/>
</div>
);
}
function HeaderDetails(props){
return (
<div className="col-xs-12 col-sm-10">
<dl className="dl-horizontal">
<dt>Title</dt>
<dd>{props.data.get("title")}</dd>
<dt>Plot</dt>
<dd className="plot">{props.data.get("plot")}</dd>
<dt>IMDB</dt>
<dd>
<ImdbButton imdbId={props.data.get("imdb_id")} size="xs"/>
</dd>
<dt>Year</dt>
<dd>{props.data.get("year")}</dd>
<dt>Rating</dt>
<dd>{props.data.get("rating")}</dd>
</dl>
<div className="col-8">
<div className="card-body">
<h5 className="card-title">{props.data.get("title")}</h5>
<p className="card-text">{props.data.get("year")}</p>
<p className="card-text">{props.data.get("rating")}</p>
<p className="card-text">{props.data.get("plot")}</p>
<p className="card-text">
<ImdbButton imdbId={props.data.get("imdb_id")} xs/>
</p>
<TrackHeader
data={props.data}
addToWishlist={props.addToWishlist}
deleteFromWishlist={props.deleteFromWishlist}
/>
</div>
</div>
</div>
</div>
);
}
Header.propTypes = {
data: PropTypes.instanceOf(Map),
deleteFromWishlist: PropTypes.func,
addToWishlist: PropTypes.func,
};
function SeasonsList(props){
return (
<div>
const SeasonsList = (props) => (
<div className="col col-12 col-md-10 offset-md-1">
{props.data.get("seasons").entrySeq().map(function([season, data]) {
return (
<div className="col-xs-12 col-sm-10 col-sm-offset-1 col-md-10 col-md-offset-1" key={season.toString()}>
<Season
key={`season-list-key-${season}`}
data={data}
season={season}
showName={props.data.get("title")}
router={props.router}
addTorrent={props.addTorrent}
addToWishlist={props.addToWishlist}
getEpisodeDetails={props.getEpisodeDetails}
refreshSubtitles={props.refreshSubtitles}
/>
</div>
);
})}
</div>
)
}
);
SeasonsList.propTypes = {
data: PropTypes.instanceOf(Map),
addToWishlist: PropTypes.func,
addTorrent: PropTypes.func,
refreshSubtitles: PropTypes.func,
getEpisodeDetails: PropTypes.func,
};
const Season = (props) => {
const [show, setShow] = useState(false);
const visibility = show ? "d-flex flex-column" : "d-none";
const icon = show ? "down" : "left"
class Season extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.state = { colapsed: true };
}
handleClick(e) {
e.preventDefault();
this.setState({ colapsed: !this.state.colapsed });
}
render() {
return (
<div className="panel panel-default">
<div className="panel-heading clickable" onClick={(e) => this.handleClick(e)}>
Season {this.props.season}
<small className="text-primary"> ({this.props.data.toList().size} episodes)</small>
<span className="pull-right">
{this.state.colapsed ||
<i className="fa fa-chevron-down"></i>
}
{this.state.colapsed &&
<i className="fa fa-chevron-left"></i>
}
</span>
<div className="card mb-3">
<div className="card-header clickable" onClick={() => setShow(!show)}>
<h5 className="m-0">
Season {props.season}
<small className="text-primary"> ({props.data.toList().size} episodes)</small>
<i className={`float-right fa fa-chevron-${icon}`}></i>
</h5>
</div>
{this.state.colapsed ||
<table className="table table-striped table-hover">
<tbody>
{this.props.data.toList().map(function(episode) {
<div className={`card-body ${visibility}`}>
{props.data.toList().map(function(episode) {
let key = `${episode.get("season")}-${episode.get("episode")}`;
return (
<Episode
key={key}
data={episode}
showName={this.props.showName}
router={this.props.router}
addTorrent={this.props.addTorrent}
addToWishlist={this.props.addToWishlist}
getEpisodeDetails={this.props.getEpisodeDetails}
refreshSubtitles={this.props.refreshSubtitles}
showName={props.showName}
addTorrent={props.addTorrent}
addToWishlist={props.addToWishlist}
getEpisodeDetails={props.getEpisodeDetails}
refreshSubtitles={props.refreshSubtitles}
/>
)
}, this)}
</tbody>
</table>
}
})}
</div>
</div>
)
}
}
function PolochonMetadata(props) {
if (!props.quality || props.quality === "") {
return null;
}
Season.propTypes = {
data: PropTypes.instanceOf(Map),
season: PropTypes.number,
showName: PropTypes.string,
addToWishlist: PropTypes.func,
addTorrent: PropTypes.func,
refreshSubtitles: PropTypes.func,
getEpisodeDetails: PropTypes.func,
};
const PolochonMetadata = (props) => {
if (!props.quality || props.quality === "") { return null }
return (
<span>
<span className="badge badge-pill badge-secondary">{props.quality}</span>
<span className="badge badge-pill badge-secondary">{props.container} </span>
<span className="badge badge-pill badge-secondary">{props.videoCodec}</span>
<span className="badge badge-pill badge-secondary">{props.audioCodec}</span>
<span className="badge badge-pill badge-secondary">{props.releaseGroup}</span>
<span className="my-2 d-flex d-wrap">
<span className="badge badge-pill badge-light mx-1">{props.quality}</span>
<span className="badge badge-pill badge-light mx-1">{props.container} </span>
<span className="badge badge-pill badge-light mx-1">{props.videoCodec}</span>
<span className="badge badge-pill badge-light mx-1">{props.audioCodec}</span>
<span className="badge badge-pill badge-light mx-1">{props.releaseGroup}</span>
</span>
);
}
PolochonMetadata.propTypes = {
quality: PropTypes.string,
container: PropTypes.string,
videoCodec: PropTypes.string,
audioCodec: PropTypes.string,
releaseGroup: PropTypes.string,
};
function Episode(props) {
return (
<tr>
<th scope="row" className="col-xs-2">
<TrackButton
data={props.data}
addToWishlist={props.addToWishlist}
/>
{props.data.get("episode")}
</th>
<td className="col-xs-12">
{props.data.get("title")}
const Episode = (props) => (
<div className="d-flex flex-wrap flex-md-nowrap align-items-center">
<TrackButton data={props.data} addToWishlist={props.addToWishlist} />
<span className="mx-2 text">{props.data.get("episode")}</span>
<span className="mx-2 text text-truncate flex-fill">{props.data.get("title")}</span>
<PolochonMetadata
quality={props.data.get("quality")}
releaseGroup={props.data.get("release_group")}
@ -213,7 +197,7 @@ function Episode(props) {
audioCodec={props.data.get("audio_codec")}
videoCodec={props.data.get("video_codec")}
/>
<span className="pull-right episode-buttons">
<div className="align-self-end btn-toolbar">
{props.data.get("polochon_url") !== "" &&
<SubtitlesButton
fetching={props.data.get("fetchingSubtitles")}
@ -237,168 +221,147 @@ function Episode(props) {
)
})}
<DownloadButton
name={`${props.showName} - S${pad(props.data.get("season"))}E${pad(props.data.get("episode"))}`}
url={props.data.get("polochon_url")}
subtitles={props.data.get("subtitles")}
xs
/>
<GetDetailsButton
<GetDetailsButtonWithRouter
showName={props.showName}
router={props.router}
data={props.data}
getEpisodeDetails={props.getEpisodeDetails}
/>
</span>
</td>
</tr>
</div>
</div>
)
}
Episode.propTypes = {
data: PropTypes.instanceOf(Map),
season: PropTypes.number,
showName: PropTypes.string,
addToWishlist: PropTypes.func,
addTorrent: PropTypes.func,
refreshSubtitles: PropTypes.func,
getEpisodeDetails: PropTypes.func,
};
class Torrent extends React.PureComponent {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick(e, url) {
e.preventDefault();
this.props.addTorrent(url);
}
render() {
return (
<span>
<a type="button"
className="btn btn-primary btn-xs"
onClick={(e) => this.handleClick(e, this.props.data.get("url"))}
href={this.props.data.url} >
<i className="fa fa-download"></i> {this.props.data.get("quality")}
</a>
</span>
const Torrent = (props) => (
<button type="button"
className="btn btn-primary btn-sm"
onClick={() => props.addTorrent(props.data.get("url"))}
href={props.data.url} >
<i className="fa fa-download"></i> {props.data.get("quality")}
</button>
)
Torrent.propTypes = {
data: PropTypes.instanceOf(Map),
addTorrent: PropTypes.func,
};
const TrackHeader = (props) => {
const trackedSeason = props.data.get("tracked_season");
const trackedEpisode = props.data.get("tracked_episode");
const imdbId = props.data.get("imdb_id");
const wishlisted = (trackedSeason !== null && trackedEpisode !== null);
const handleClick = () => {
if (wishlisted) {
props.deleteFromWishlist(imdbId);
} else {
props.addToWishlist(imdbId);
}
}
class TrackHeader extends React.PureComponent {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick(e) {
e.preventDefault();
const trackedSeason = this.props.data.get("tracked_season");
const trackedEpisode = this.props.data.get("tracked_episode");
const imdbId = this.props.data.get("imdb_id");
const wishlisted = (trackedSeason !== null && trackedEpisode !== null);
if (wishlisted) {
this.props.deleteFromWishlist(imdbId);
} else {
this.props.addToWishlist(imdbId);
}
}
render() {
const trackedSeason = this.props.data.get("tracked_season");
const trackedEpisode = this.props.data.get("tracked_episode");
const wishlisted = (trackedSeason !== null && trackedEpisode !== null);
let msg;
if (wishlisted) {
if (trackedSeason !== 0 && trackedEpisode !== 0) {
msg = (
<dd>Show tracked from <strong>season {trackedSeason} episode {trackedEpisode}</strong></dd>
);
} else {
msg = (
<dd>Whole show tracked</dd>
);
}
const msg = (trackedSeason !== 0 && trackedEpisode !== 0)
? (<p>Show tracked from <strong>season {trackedSeason} episode {trackedEpisode}</strong></p>)
: (<p>Whole show tracked</p>);
return (
<dl className="dl-horizontal">
<dt>Tracking active</dt>
<span className="card-text">
{msg}
<dt></dt>
<dd>
<a className="btn btn-xs btn-danger" onClick={(e) => this.handleClick(e)}>
<a className="btn btn-sm btn-danger" onClick={handleClick}>
<i className="fa fa-bookmark"></i> Untrack the show
</a>
</dd>
</dl>
</span>
);
} else {
return (
<dl className="dl-horizontal">
<dt>Tracking inactive</dt>
<dd>
<a className="btn btn-xs btn-info" onClick={(e) => this.handleClick(e)}>
<i className="fa fa-bookmark-o"></i> Track the whole show
</a>
</dd>
</dl>
);
}
}
}
class TrackButton extends React.PureComponent {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
return (
<span className="card-text">
<p>Tracking inactive</p>
<a className="btn btn-sm btn-info" onClick={(e) => handleClick(e)}>
<i className="fa fa-bookmark-o"></i> Track the whole show
</a>
</span>
);
}
handleClick(e) {
e.preventDefault();
const imdbId = this.props.data.get("show_imdb_id");
const season = this.props.data.get("season");
const episode = this.props.data.get("episode");
this.props.addToWishlist(imdbId, season, episode);
}
render() {
const tooltipId = `tooltip-${this.props.data.season}-${this.props.data.episode}`;
TrackHeader.propTypes = {
data: PropTypes.instanceOf(Map),
addToWishlist: PropTypes.func,
deleteFromWishlist: PropTypes.func,
};
const TrackButton = (props) => {
const imdbId = props.data.get("show_imdb_id");
const season = props.data.get("season");
const episode = props.data.get("episode");
const tooltipId = `tooltip-${props.data.season}-${props.data.episode}`;
const tooltip = (
<Tooltip id={tooltipId}>Track show from here</Tooltip>
);
return (
<OverlayTrigger placement="top" overlay={tooltip}>
<a type="button" className="btn btn-default btn-xs" onClick={(e) => this.handleClick(e)}>
<span className="btn clickable"
onClick={() => props.addToWishlist(imdbId, season, episode)}>
<i className="fa fa-bookmark"></i>
</a>
</span>
</OverlayTrigger>
);
}
TrackButton.propTypes = {
data: PropTypes.instanceOf(Map),
addToWishlist: PropTypes.func,
};
const GetDetailsButton = (props) => {
const imdbId = props.data.get("show_imdb_id");
const season = props.data.get("season");
const episode = props.data.get("episode");
const id = `${imdbId}-${season}-${episode}-refresh-dropdown`;
const handleFetchClick = () => {
if (props.data.get("fetching")) { return }
props.getEpisodeDetails(imdbId, season, episode);
}
class GetDetailsButton extends React.PureComponent {
constructor(props) {
super(props);
this.handleFetchClick = this.handleFetchClick.bind(this);
this.handleAdvanceTorrentSearchClick = this.handleAdvanceTorrentSearchClick.bind(this);
this.state = {
imdbId: this.props.data.get("show_imdb_id"),
season: this.props.data.get("season"),
episode: this.props.data.get("episode"),
};
}
handleFetchClick() {
if (this.props.data.get("fetching")) { return }
this.props.getEpisodeDetails(this.state.imdbId, this.state.season, this.state.episode);
}
handleAdvanceTorrentSearchClick() {
const pad = (d) => (d < 10) ? "0" + d.toString() : d.toString();
const search = `${this.props.showName} S${pad(this.state.season)}E${pad(this.state.episode)}`;
const handleAdvanceTorrentSearchClick = () => {
const search = `${props.showName} S${pad(season)}E${pad(episode)}`;
const url = `/torrents/search/shows/${encodeURI(search)}`;
this.props.router.push(url);
props.history.push(url);
}
render() {
const id = `${this.state.imdbId}-${this.state.season}-${this.state.episode}-refresh-dropdown`;
return (
<Dropdown id={id} dropup>
<Button className="btn-xs" bsStyle="info" onClick={this.handleFetchClick}>
<RefreshIndicator refresh={this.props.data.get("fetching")} />
</Button>
<Dropdown.Toggle className="btn-xs" bsStyle="info"/>
<Dropdown.Menu>
<MenuItem onClick={this.handleAdvanceTorrentSearchClick}>
<SplitButton
drop="up"
variant="info"
title={<RefreshIndicator refresh={props.data.get("fetching")} />}
size="sm"
id={`refresh-${id}`}
onClick={handleFetchClick}>
<Dropdown.Item onClick={handleAdvanceTorrentSearchClick}>
<span>
<i className="fa fa-magnet"></i> Advanced torrent search
</span>
</MenuItem>
</Dropdown.Menu>
</Dropdown>
</Dropdown.Item>
</SplitButton>
);
}
}
GetDetailsButton.propTypes = {
data: PropTypes.instanceOf(Map).isRequired,
history: PropTypes.object.isRequired,
showName: PropTypes.string.isRequired,
getEpisodeDetails: PropTypes.func.isRequired,
};
const GetDetailsButtonWithRouter = withRouter(GetDetailsButton);

View File

@ -1,4 +1,6 @@
import React from "react"
import PropTypes from "prop-types"
import { Map } from "immutable"
import { connect } from "react-redux"
import { selectShow, addShowToWishlist,
deleteShowFromWishlist, getShowDetails, updateFilter } from "../../actions/shows"
@ -13,7 +15,6 @@ function mapStateToProps(state) {
shows : state.showsStore.get("shows"),
filter : state.showsStore.get("filter"),
selectedImdbId : state.showsStore.get("selectedImdbId"),
lastFetchUrl : state.showsStore.get("lastFetchUrl"),
exploreOptions : state.showsStore.get("exploreOptions"),
};
}
@ -22,51 +23,58 @@ const mapDispatchToProps = {
getShowDetails, updateFilter,
};
class ShowList extends React.PureComponent {
constructor(props) {
super(props);
this.showDetails = this.showDetails.bind(this);
const ShowList = (props) => {
const showDetails = (imdbId) => {
props.history.push("/shows/details/" + imdbId);
}
showDetails(imdbId) {
return this.props.history.push("/shows/details/" + imdbId);
}
render() {
let selectedShow;
if (this.props.selectedImdbId !== "") {
selectedShow = this.props.shows.get(this.props.selectedImdbId);
if (props.selectedImdbId !== "") {
selectedShow = props.shows.get(props.selectedImdbId);
}
return (
<div className="row" id="container">
<ListPosters
data={this.props.shows}
data={props.shows}
type="shows"
placeHolder="Filter shows..."
exploreOptions={this.props.exploreOptions}
updateFilter={this.props.updateFilter}
selectedImdbId={this.props.selectedImdbId}
filter={this.props.filter}
onClick={this.props.selectShow}
onDoubleClick={this.showDetails}
onKeyEnter={this.showDetails}
params={this.props.match.params}
loading={this.props.loading}
exploreOptions={props.exploreOptions}
updateFilter={props.updateFilter}
selectedImdbId={props.selectedImdbId}
filter={props.filter}
onClick={props.selectShow}
onDoubleClick={showDetails}
onKeyEnter={showDetails}
params={props.match.params}
loading={props.loading}
/>
{selectedShow &&
<ListDetails data={selectedShow} >
<ListDetails data={selectedShow} loading={props.loading}>
<ShowButtons
show={selectedShow}
deleteFromWishlist={this.props.deleteShowFromWishlist}
addToWishlist={this.props.addShowToWishlist}
getDetails={this.props.getShowDetails}
updateFilter={this.props.updateFilter}
deleteFromWishlist={props.deleteShowFromWishlist}
addToWishlist={props.addShowToWishlist}
getDetails={props.getShowDetails}
updateFilter={props.updateFilter}
/>
</ListDetails>
}
</div>
);
}
}
ShowList.propTypes = {
match: PropTypes.object,
history: PropTypes.object,
shows: PropTypes.instanceOf(Map),
exploreOptions: PropTypes.instanceOf(Map),
selectedImdbId: PropTypes.string,
filter: PropTypes.string,
loading: PropTypes.bool,
deleteShowFromWishlist: PropTypes.func,
addShowToWishlist: PropTypes.func,
selectShow: PropTypes.func,
getShowDetails: PropTypes.func,
updateFilter: PropTypes.func,
};
export default connect(mapStateToProps, mapDispatchToProps)(ShowList);

View File

@ -1,14 +1,16 @@
import React from "react"
import PropTypes from "prop-types"
import { Map } from "immutable"
import { Link } from "react-router-dom"
import { DropdownButton } from "react-bootstrap"
import Dropdown from "react-bootstrap/Dropdown"
import ButtonToolbar from "react-bootstrap/ButtonToolbar"
import { WishlistButton, RefreshButton } from "../buttons/actions"
import ImdbButton from "../buttons/imdb"
export default function ShowButtons(props) {
return (
<div className="list-details-buttons btn-toolbar">
const ShowButtons = (props) => (
<ButtonToolbar>
<ActionsButton
show={props.show}
addToWishlist={props.addToWishlist}
@ -19,14 +21,24 @@ export default function ShowButtons(props) {
<Link type="button" className="btn btn-primary btn-sm" to={"/shows/details/" + props.show.get("imdb_id")}>
<i className="fa fa-external-link"></i> Details
</Link>
</div>
</ButtonToolbar>
);
ShowButtons.propTypes = {
show: PropTypes.instanceOf(Map),
addToWishlist: PropTypes.func.isRequired,
deleteFromWishlist: PropTypes.func.isRequired,
getDetails: PropTypes.func.isRequired,
}
function ActionsButton(props) {
const ActionsButton = (props) => {
let wishlisted = (props.show.get("tracked_season") !== null && props.show.get("tracked_episode") !== null);
return (
<DropdownButton className="btn btn-default btn-sm" title="Actions" id="actions-button" dropup>
<Dropdown drop="up">
<Dropdown.Toggle variant="secondary" id="movie-button-actions">
Actions
</Dropdown.Toggle>
<Dropdown.Menu>
<RefreshButton
fetching={props.show.get("fetchingDetails")}
resourceId={props.show.get("imdb_id")}
@ -38,6 +50,15 @@ function ActionsButton(props) {
addToWishlist={props.addToWishlist}
deleteFromWishlist={props.deleteFromWishlist}
/>
</DropdownButton>
</Dropdown.Menu>
</Dropdown>
);
}
ActionsButton.propTypes = {
show: PropTypes.instanceOf(Map),
addToWishlist: PropTypes.func.isRequired,
deleteFromWishlist: PropTypes.func.isRequired,
getDetails: PropTypes.func.isRequired,
}
export default ShowButtons;

View File

@ -12,13 +12,15 @@ const mapDispatchToProps = {
};
const TorrentList = (props) => (
<div>
<div className="row">
<div className="col-12">
<AddTorrent addTorrent={props.addTorrent} />
<Torrents
torrents={props.torrents}
removeTorrent={props.removeTorrent}
/>
</div>
</div>
);
TorrentList.propTypes = {
fetchTorrents: PropTypes.func.isRequired,
@ -31,35 +33,24 @@ export default connect(mapStateToProps, mapDispatchToProps)(TorrentList);
const AddTorrent = (props) => {
const [url, setUrl] = useState("");
const handleSubmit = (ev) => {
if (ev) { ev.preventDefault(); }
const handleSubmit = (e) => {
e.preventDefault();
if (url === "") { return; }
props.addTorrent(url);
setUrl("");
}
return (
<div className="row">
<div className="col-xs-12 col-md-12">
<form className="input-group" onSubmit={() => handleSubmit()}>
<form onSubmit={(e) => handleSubmit(e)}>
<input
className="form-control"
type="text"
className="form-control mb-3 w-100"
placeholder="Add torrent URL"
onSubmit={handleSubmit}
value={url}
onChange={(e) => setUrl(e.target.value)}
/>
<span className="input-group-btn">
<button
className="btn btn-primary"
type="button"
onClick={() => handleSubmit()}
>
Add
</button>
</span>
</form>
</div>
</div>
);
}
AddTorrent.propTypes = {
@ -69,21 +60,14 @@ AddTorrent.propTypes = {
const Torrents = (props) => {
if (props.torrents.size === 0) {
return (
<div className="row">
<div className="col-xs-12 col-md-12">
<h3>Torrents</h3>
<div className="panel panel-default">
<div className="panel-heading">No torrents</div>
</div>
</div>
<div className="jumbotron">
<h2>No torrents</h2>
</div>
);
}
return (
<div className="row">
<div className="col-xs-12 col-md-12">
<h3>Torrents</h3>
<div className="d-flex flex-wrap">
{props.torrents.map((el, index) => (
<Torrent
key={index}
@ -92,7 +76,6 @@ const Torrents = (props) => {
/>
))}
</div>
</div>
);
}
Torrents.propTypes = {
@ -106,10 +89,9 @@ const Torrent = (props) => {
}
const done = props.data.get("is_finished");
var progressStyle = "progress-bar progress-bar-info active";
if (done) {
progressStyle = "progress-bar progress-bar-success";
}
var progressStyle = done ? "success" : "info progress-bar-striped progress-bar-animated";
const progressBarClass = "progress-bar bg-" + progressStyle;
var percentDone = props.data.get("percent_done");
const started = (percentDone !== 0);
if (started) {
@ -121,28 +103,30 @@ const Torrent = (props) => {
const totalSize = prettySize(props.data.get("total_size"));
const downloadRate = prettySize(props.data.get("download_rate")) + "/s";
return (
<div className="panel panel-default">
<div className="panel-heading">
{props.data.get("name")}
<div className="card w-100">
<h5 className="card-header">
<span className="text text-break">{props.data.get("name")}</span>
<span className="fa fa-trash clickable pull-right" onClick={() => handleClick()}></span>
</div>
<div className="panel-body">
</h5>
<div className="card-body pb-0">
{started &&
<div className="progress progress-striped">
<React.Fragment>
<div className="progress bg-light">
<div
className={progressStyle}
style={{width: percentDone}}>
</div>
className={progressBarClass}
style={{width: percentDone}}
role="progressbar"
aria-valuenow={percentDone}
aria-valuemin="0"
aria-valuemax="100"
></div>
</div>
<p>{downloadedSize} / {totalSize} - {percentDone} - {downloadRate}</p>
</React.Fragment>
}
{!started &&
<p>Download not yet started</p>
}
{started &&
<div>
<p>{downloadedSize} / {totalSize} - {percentDone} - {downloadRate}</p>
</div>
}
</div>
</div>
);

View File

@ -31,21 +31,16 @@ const TorrentSearch = (props) => {
}, [url]);
return (
<div>
<div className="col-xs-12">
<form className="form-horizontal" onSubmit={(e) => e.preventDefault()}>
<div className="form-group">
<div className="row">
<div className="col-12 d-flex flex-row flex-wrap">
<input
type="text"
className="form-control"
className="form-control mb-1 w-100 form-control-lg"
placeholder="Search torrents"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
</form>
</div>
<div className="row">
<div className="mb-3 w-100 d-flex">
<SearchButton
text="Search movies"
type="movies"
@ -63,8 +58,8 @@ const TorrentSearch = (props) => {
setUrl(getUrl());
}}/>
</div>
<hr />
<div className="row">
</div>
<div className="col-12">
<TorrentList
searching={props.searching}
results={props.results}
@ -87,18 +82,15 @@ TorrentSearch.propTypes = {
const SearchButton = (props) => {
const color = (props.type === props.typeFromURL) ? "primary" : "default";
const variant = (props.type === props.typeFromURL) ? "primary" : "secondary";
return (
<div className="col-xs-6">
<button
className={`btn btn-${color} full-width`}
type="button"
className={`w-50 btn m-1 btn-lg btn-${variant}`}
onClick={props.handleClick}
>
<i className="fa fa-search" aria-hidden="true"></i> {props.text}
</button>
</div>
);
}
SearchButton.propTypes = {
@ -119,16 +111,14 @@ const TorrentList = (props) => {
if (props.results.size === 0) {
return (
<div className="col-xs-12">
<div className="well well-lg">
<div className="jumbotron">
<h2>No results</h2>
</div>
</div>
);
}
return (
<div className="col-xs-12">
<React.Fragment>
{props.results.map(function(el, index) {
return (
<Torrent
@ -137,7 +127,7 @@ const TorrentList = (props) => {
addTorrent={props.addTorrent}
/>);
})}
</div>
</React.Fragment>
);
}
TorrentList.propTypes = {
@ -148,43 +138,20 @@ TorrentList.propTypes = {
};
const Torrent = (props) => (
<div className="row">
<div className="col-xs-12">
<table className="table responsive table-align-middle torrent-search-result">
<tbody>
<tr>
<td rowSpan="2" className="col-xs-1 torrent-small-width">
<h4>
<div className="alert d-flex border-bottom border-secondary align-items-center">
<TorrentHealth
url={props.data.get("url")}
seeders={props.data.get("seeders")}
leechers={props.data.get("leechers")}
/>
</h4>
</td>
<td colSpan="4" className="col-xs-9 title">
<span className="torrent-title">{props.data.get("name")}</span>
</td>
<td rowSpan="2" className="col-xs-1 torrent-small-width">
<h4 className="pull-right clickable" onClick={() => props.addTorrent(props.data.get("url"))}>
<i className="fa fa-cloud-download" aria-hidden="true"></i>
</h4>
</td>
</tr>
<tr>
<td className="col-xs-1 torrent-label">
<span className="label label-warning">{props.data.get("quality")}</span>
</td>
<td className="col-xs-1 torrent-label">
<span className="label label-success">{props.data.get("source")}</span>
</td>
<td className="col-xs-1 torrent-label">
<span className="label label-info">{props.data.get("upload_user")}</span>
</td>
<td className="col-xs-7 torrent-label"></td>
</tr>
</tbody>
</table>
<span className="mx-3 text text-start text-break flex-fill">{props.data.get("name")}</span>
<div>
<span className="mx-1 badge badge-pill badge-warning">{props.data.get("quality")}</span>
<span className="mx-1 badge badge-pill badge-success">{props.data.get("source")}</span>
<span className="mx-1 badge badge-pill badge-info">{props.data.get("upload_user")}</span>
</div>
<div className="align-self-end ml-3">
<i className="fa fa-cloud-download clickable" onClick={() => props.addTorrent(props.data.get("url"))}></i>
</div>
</div>
);
@ -214,7 +181,7 @@ const TorrentHealth = (props) => {
}
}
const className = `text text-${color}`;
const className = `align-self-start text text-center text-${color}`;
const tooltip = (
<Tooltip id={`tooltip-health-${props.url}`}>
<p><span className={className}>Health: {health}</span></p>

View File

@ -18,16 +18,14 @@ const UserActivation = (props) => {
}
return (
<div className="container">
<div className="content-fluid">
<div className="col-md-8 col-md-offset-2 col-xs-12">
<div className="row">
<div className="col-12 col-md-8 offset-md-2">
<h2>Waiting for activation</h2>
<hr />
<h3>Hang tight! Your user will soon be activated by the administrators of this site.</h3>
<Link to="/users/logout">Logout</Link>
</div>
</div>
</div>
);
}
UserActivation.propTypes = {

View File

@ -28,9 +28,8 @@ const UserLoginForm = (props) => {
}
return (
<div className="container">
<div className="content-fluid">
<div className="col-md-6 col-md-offset-3 col-xs-12">
<div className="row">
<div className="col-10 offset-1 col-md-6 offset-md-3">
<h2>Log in</h2>
<hr/>
{props.error && props.error !== "" &&
@ -74,7 +73,6 @@ const UserLoginForm = (props) => {
</form>
</div>
</div>
</div>
);
}
UserLoginForm.propTypes = {

View File

@ -1,4 +1,4 @@
import React, { useState } from "react"
import React, { useState, useEffect } from "react"
import PropTypes from "prop-types"
import { connect } from "react-redux"
import Loader from "../loader/loader"
@ -24,12 +24,10 @@ const mapDispatchToProps = {
}
const UserProfile = (props) => {
const [fetched, setIsFetched] = useState(false);
if (!fetched) {
useEffect(() => {
props.getUserInfos();
props.getUserModules();
setIsFetched(true);
}
}, [])
return (
<div>
@ -78,9 +76,8 @@ const UserEdit = (props) => {
}
return (
<div className="container">
<div className="content-fluid">
<div className="col-md-6 col-md-offset-3 col-xs-12">
<div className="row mb-3">
<div className="col-12 col-md-6 offset-md-3">
<h2>Edit user</h2>
<hr />
<form className="form-horizontal" onSubmit={(ev) => handleSubmit(ev)}>
@ -132,7 +129,6 @@ const UserEdit = (props) => {
</form>
</div>
</div>
</div>
);
}
UserEdit.propTypes = {

View File

@ -31,9 +31,8 @@ const UserSignUp = (props) => {
}
return (
<div className="container">
<div className="content-fluid">
<div className="col-md-6 col-md-offset-3 col-xs-12">
<div className="Row">
<div className="col-10 offset-1 col-md-6 offset-md-3">
<h2>Sign up</h2>
<hr />
{props.error && props.error !== "" &&
@ -75,7 +74,6 @@ const UserSignUp = (props) => {
</form>
</div>
</div>
</div>
);
}
UserSignUp.propTypes = {

View File

@ -20,11 +20,8 @@ const UserTokens = (props) => {
}
return (
<div>
<h2 className="hidden-xs">Tokens</h2>
<h3 className="visible-xs">Tokens</h3>
<div>
<div className="row">
<div className="col-12">
{props.tokens.map((el, index) => (
<Token key={index} data={el} deleteToken={props.deleteUserToken} />
))}
@ -42,34 +39,25 @@ UserTokens.propTypes = {
const Token = (props) => {
const ua = UAParser(props.data.get("user_agent"));
return (
<div className="panel panel-default">
<div className="container">
<div className="card mt-3">
<div className="card-header">
<h4>
<Logo {...ua} />
<div className="col-xs-10">
<dl className="dl-horizontal">
<dt>Description</dt>
<dd>{props.data.get("description")}</dd>
<dt>Last IP</dt>
<dd>{props.data.get("ip")}</dd>
<dt>Last used</dt>
<dd>{moment(props.data.get("last_used")).fromNow()}</dd>
<dt>Created</dt>
<dd>{moment(props.data.get("created_at")).fromNow()}</dd>
<dt>Device</dt>
<dd><Device {...ua.device}/></dd>
<dt>OS</dt>
<dd><OS {...ua.os}/></dd>
<dt>Browser</dt>
<dd><Browser {...ua.browser}/></dd>
</dl>
</div>
<span>{props.data.get("description")}</span>
<Actions {...props} />
</h4>
</div>
<div className="card-body row">
<div className="col-12 col-md-6">
<p>Last IP: {props.data.get("ip")}</p>
<p>Last used: {moment(props.data.get("last_used")).fromNow()}</p>
<p>Created: {moment(props.data.get("created_at")).fromNow()}</p>
</div>
<div className="col-12 col-md-6">
<p>Device: <Device {...ua.device}/></p>
<p>OS: <OS {...ua.os}/></p>
<p>Browser: <Browser {...ua.browser}/></p>
</div>
</div>
</div>
);
@ -84,12 +72,10 @@ const Actions = (props) => {
}
return (
<div className="col-xs-1">
<span
className="fa fa-trash fa-lg pull-right clickable user-token-action"
onClick={() => handleClick()}>
className="fa fa-trash fa-lg pull-right clickable"
onClick={handleClick}>
</span>
</div>
);
}
Actions.propTypes = {
@ -123,16 +109,7 @@ const Logo = (props) => {
}
}
return (
<div className="user-token-icon">
<div className="hidden-xs hidden-sm col-md-1">
<span className={"fa fa-" + className + " fa-5x"}></span>
</div>
<div className="hidden-md hidden-lg col-xs-1">
<span className={"fa fa-" + className + " fa-lg"}></span>
</div>
</div>
);
return (<span className={`fa fa-${className}`}></span>);
}
const OS = (props) => {

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/."
},
"dependencies": {
"bootstrap": "^3.3.6",
"bootswatch": "^3.3.7",
"bootstrap": "^4.3.1",
"bootswatch": "^4.3.1",
"font-awesome": "^4.7.0",
"fuzzy": "^0.1.3",
"history": "^4.7.2",
@ -15,13 +15,14 @@
"jquery": "^2.2.4",
"jwt-decode": "^2.1.0",
"moment": "^2.20.1",
"popper.js": "^1.14.7",
"prop-types": "^15.6.0",
"react": "^16.8.6",
"react-bootstrap": "^0.32.1",
"react-bootstrap": "^1.0.0-beta.8",
"react-bootstrap-sweetalert": "^4.2.3",
"react-bootstrap-toggle": "^2.2.6",
"react-dom": "^16.2.0",
"react-infinite-scroller": "^1.2.4",
"react-infinite-scroll-component": "^4.5.2",
"react-loading": "2.0.3",
"react-redux": "6.0.1",
"react-router": "5.0.0",
@ -37,6 +38,7 @@
"@babel/core": "^7.0.0-0",
"@babel/preset-env": "^7.4.4",
"@babel/preset-react": "^7.0.0",
"autoprefixer": "^9.5.1",
"axios": "^0.17.1",
"babel-eslint": "^10.0.1",
"babel-loader": "^8.0.6",
@ -47,6 +49,9 @@
"file-loader": "^3.0.1",
"less": "^2.3.1",
"less-loader": "^4.0.5",
"node-sass": "^4.12.0",
"postcss-loader": "^3.0.0",
"sass-loader": "^7.1.0",
"style-loader": "^0.23.1",
"url-loader": "^1.1.2",
"webpack": "^4.31.0",

5
postcss.config.js Normal file
View File

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

View File

@ -32,14 +32,13 @@ const config = {
}
},
{
test: /\.less$/,
use: [{
loader: "style-loader" // creates style nodes from JS strings
}, {
loader: "css-loader" // translates CSS into CommonJS
}, {
loader: "less-loader" // compiles Less to CSS
}]
test: /\.scss$/,
use: [
"style-loader",
"css-loader",
"sass-loader",
"postcss-loader",
]
},
{
test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,

762
yarn.lock

File diff suppressed because it is too large Load Diff