Merge branch 'perf' into 'master'

Refactoring

See merge request !77
This commit is contained in:
Lucas 2017-06-04 12:56:17 +00:00
commit 44d953e566
43 changed files with 1630 additions and 1104 deletions

23
.eslintrc Normal file
View File

@ -0,0 +1,23 @@
{
"parser": "babel-eslint",
"env": {
"browser": true,
"node": true
},
"settings": {
"ecmascript": 6,
"jsx": true
},
"plugins": [
"react"
],
"rules": {
"strict": 1,
"quotes": 1,
"no-unused-vars": 1,
"camelcase": 1,
"no-underscore-dangle": 1,
"react/jsx-uses-react": 1,
"react/jsx-uses-vars": 1
}
}

View File

@ -21,7 +21,6 @@
"react-infinite-scroller": "^1.0.4",
"react-loading": "^0.0.9",
"react-redux": "^4.4.6",
"react-redux-form": "^1.2.4",
"react-router": "^3.0.0",
"react-router-bootstrap": "^0.23.1",
"react-router-redux": "^4.0.7",
@ -34,12 +33,15 @@
"axios": "^0.15.2",
"babel": "^6.5.2",
"babel-core": "^6.18.2",
"babel-eslint": "^7.2.3",
"babel-loader": "^6.2.7",
"babel-preset-es2015": "^6.18.0",
"babel-preset-latest": "^6.16.0",
"babel-preset-react": "^6.16.0",
"css-loader": "^0.26.0",
"del": "^2.2.2",
"eslint": "^3.19.0",
"eslint-plugin-react": "^7.0.1",
"file-loader": "^0.9.0",
"gulp": "^3.9.1",
"gulp-babel": "^6.1.2",

View File

@ -1,6 +1,6 @@
export function addAlertError(message) {
return {
type: 'ADD_ALERT_ERROR',
type: "ADD_ALERT_ERROR",
payload: {
message,
}
@ -9,7 +9,7 @@ export function addAlertError(message) {
export function addAlertOk(message) {
return {
type: 'ADD_ALERT_OK',
type: "ADD_ALERT_OK",
payload: {
message,
}
@ -18,6 +18,6 @@ export function addAlertOk(message) {
export function dismissAlert() {
return {
type: 'DISMISS_ALERT',
type: "DISMISS_ALERT",
}
}

View File

@ -1,10 +1,10 @@
import { configureAxios, request } from '../requests'
import { configureAxios, request } from "../requests"
import { addAlertOk } from './alerts'
import { addAlertOk } from "./alerts"
export function updateLastMovieFetchUrl(url) {
return {
type: 'UPDATE_LAST_MOVIE_FETCH_URL',
type: "UPDATE_LAST_MOVIE_FETCH_URL",
payload: {
url: url,
},
@ -13,28 +13,43 @@ export function updateLastMovieFetchUrl(url) {
export function selectMovie(imdbId) {
return {
type: 'SELECT_MOVIE',
imdbId
type: "SELECT_MOVIE",
payload: {
imdbId,
},
}
}
export function updateFilter(filter) {
return {
type: "MOVIE_UPDATE_FILTER",
payload: {
filter,
},
}
}
export function getMovieExploreOptions() {
return request(
'MOVIE_GET_EXPLORE_OPTIONS',
configureAxios().get('/movies/explore/options')
"MOVIE_GET_EXPLORE_OPTIONS",
configureAxios().get("/movies/explore/options")
)
}
export function getMovieDetails(imdbId) {
return request(
'MOVIE_GET_DETAILS',
configureAxios().post(`/movies/${imdbId}/refresh`)
"MOVIE_GET_DETAILS",
configureAxios().post(`/movies/${imdbId}/refresh`),
null,
{
imdbId,
}
)
}
export function deleteMovie(imdbId, lastFetchUrl) {
return request(
'MOVIE_DELETE',
"MOVIE_DELETE",
configureAxios().delete(`/movies/${imdbId}`),
[
fetchMovies(lastFetchUrl),
@ -45,10 +60,9 @@ export function deleteMovie(imdbId, lastFetchUrl) {
export function addMovieToWishlist(imdbId) {
return request(
'MOVIE_ADD_TO_WISHLIST',
"MOVIE_ADD_TO_WISHLIST",
configureAxios().post(`/wishlist/movies/${imdbId}`),
[
addAlertOk("Movie added to the wishlist"),
updateMovieWishlistStore(imdbId, true),
],
)
@ -56,10 +70,9 @@ export function addMovieToWishlist(imdbId) {
export function deleteMovieFromWishlist(imdbId) {
return request(
'MOVIE_DELETE_FROM_WISHLIST',
"MOVIE_DELETE_FROM_WISHLIST",
configureAxios().delete(`/wishlist/movies/${imdbId}`),
[
addAlertOk("Movie deleted from the wishlist"),
updateMovieWishlistStore(imdbId, false),
],
)
@ -67,7 +80,7 @@ export function deleteMovieFromWishlist(imdbId) {
export function updateMovieWishlistStore(imdbId, wishlisted) {
return {
type: 'MOVIE_UPDATE_STORE_WISHLIST',
type: "MOVIE_UPDATE_STORE_WISHLIST",
payload: {
imdbId,
wishlisted,
@ -77,7 +90,7 @@ export function updateMovieWishlistStore(imdbId, wishlisted) {
export function fetchMovies(url) {
return request(
'MOVIE_LIST_FETCH',
"MOVIE_LIST_FETCH",
configureAxios().get(url),
[
updateLastMovieFetchUrl(url),

View File

@ -1,10 +1,8 @@
import { configureAxios, request } from '../requests'
import { addAlertOk } from './alerts'
import { configureAxios, request } from "../requests"
export function fetchShows(url) {
return request(
'SHOW_LIST_FETCH',
"SHOW_LIST_FETCH",
configureAxios().get(url),
[
updateLastShowsFetchUrl(url),
@ -14,15 +12,17 @@ export function fetchShows(url) {
export function getShowDetails(imdbId) {
return request(
'SHOW_GET_DETAILS',
configureAxios().post(`/shows/${imdbId}/refresh`)
"SHOW_GET_DETAILS",
configureAxios().post(`/shows/${imdbId}/refresh`),
null,
{ imdbId }
)
}
export function getEpisodeDetails(imdbId, season, episode) {
return request(
'EPISODE_GET_DETAILS',
"EPISODE_GET_DETAILS",
configureAxios().post(`/shows/${imdbId}/seasons/${season}/episodes/${episode}`),
null,
{
@ -35,20 +35,21 @@ export function getEpisodeDetails(imdbId, season, episode) {
export function fetchShowDetails(imdbId) {
return request(
'SHOW_FETCH_DETAILS',
configureAxios().get(`/shows/${imdbId}`)
"SHOW_FETCH_DETAILS",
configureAxios().get(`/shows/${imdbId}`),
null,
{ imdbId }
)
}
export function addShowToWishlist(imdbId, season = null, episode = null) {
return request(
'SHOW_ADD_TO_WISHLIST',
"SHOW_ADD_TO_WISHLIST",
configureAxios().post(`/wishlist/shows/${imdbId}`, {
season: season,
episode: episode,
}),
[
addAlertOk("Show added to the wishlist"),
updateShowWishlistStore(imdbId, true, season, episode),
],
)
@ -56,10 +57,9 @@ export function addShowToWishlist(imdbId, season = null, episode = null) {
export function deleteShowFromWishlist(imdbId) {
return request(
'SHOW_DELETE_FROM_WISHLIST',
"SHOW_DELETE_FROM_WISHLIST",
configureAxios().delete(`/wishlist/shows/${imdbId}`),
[
addAlertOk("Show deleted from the wishlist"),
updateShowWishlistStore(imdbId, false),
],
)
@ -67,7 +67,7 @@ export function deleteShowFromWishlist(imdbId) {
export function updateShowWishlistStore(imdbId, wishlisted, season = null, episode = null) {
return {
type: 'SHOW_UPDATE_STORE_WISHLIST',
type: "SHOW_UPDATE_STORE_WISHLIST",
payload: {
wishlisted: wishlisted,
imdbId,
@ -79,21 +79,33 @@ export function updateShowWishlistStore(imdbId, wishlisted, season = null, episo
export function getShowExploreOptions() {
return request(
'SHOW_GET_EXPLORE_OPTIONS',
configureAxios().get('/shows/explore/options')
"SHOW_GET_EXPLORE_OPTIONS",
configureAxios().get("/shows/explore/options")
)
}
export function selectShow(imdbId) {
return {
type: 'SELECT_SHOW',
imdbId
type: "SELECT_SHOW",
payload: {
imdbId,
}
}
}
export function updateFilter(filter) {
return {
type: "SHOWS_UPDATE_FILTER",
payload: {
filter,
},
}
}
export function updateLastShowsFetchUrl(url) {
return {
type: 'UPDATE_LAST_SHOWS_FETCH_URL',
type: "UPDATE_LAST_SHOWS_FETCH_URL",
payload: {
url: url,
},

View File

@ -1,30 +1,28 @@
import { configureAxios, request } from '../requests'
import { addAlertOk } from './alerts'
import { configureAxios, request } from "../requests"
export function refreshSubtitles(type, id, season, episode) {
switch (type) {
case 'movie':
case "movie":
var resourceURL = `/movies/${id}`
return request(
'MOVIE_SUBTITLES_UPDATE',
"MOVIE_SUBTITLES_UPDATE",
configureAxios().post(`${resourceURL}/subtitles/refresh`),
null,
{ imdb_id: id },
{ imdbId: id },
)
case 'episode':
case "episode":
var resourceURL = `/shows/${id}/seasons/${season}/episodes/${episode}`
return request(
'EPISODE_SUBTITLES_UPDATE',
"EPISODE_SUBTITLES_UPDATE",
configureAxios().post(`${resourceURL}/subtitles/refresh`),
null,
{
imdb_id: id,
imdbId: id,
season: season,
episode: episode,
},
)
default:
console.log("refreshSubtitles - Unknown type " + type)
console.warn("refreshSubtitles - Unknown type " + type)
}
}

View File

@ -1,11 +1,11 @@
import { configureAxios, request } from '../requests'
import { configureAxios, request } from "../requests"
import { addAlertOk } from './alerts'
import { addAlertOk } from "./alerts"
export function addTorrent(url) {
return request(
'ADD_TORRENT',
configureAxios().post('/torrents', {
"ADD_TORRENT",
configureAxios().post("/torrents", {
url: url,
}),
[
@ -16,7 +16,7 @@ export function addTorrent(url) {
export function fetchTorrents() {
return request(
'TORRENTS_FETCH',
configureAxios().get('/torrents')
"TORRENTS_FETCH",
configureAxios().get("/torrents")
)
}

View File

@ -1,18 +1,18 @@
import { configureAxios, request } from '../requests'
import { configureAxios, request } from "../requests"
import { addAlertOk } from './alerts'
import { addAlertOk } from "./alerts"
export function userLogout() {
return {
type: 'USER_LOGOUT',
type: "USER_LOGOUT",
}
}
export function loginUser(username, password) {
return request(
'USER_LOGIN',
"USER_LOGIN",
configureAxios().post(
'/users/login',
"/users/login",
{
username: username,
password: password,
@ -23,8 +23,8 @@ export function loginUser(username, password) {
export function updateUser(config) {
return request(
'USER_UPDATE',
configureAxios().post('/users/edit', config),
"USER_UPDATE",
configureAxios().post("/users/edit", config),
[
addAlertOk("User updated"),
],
@ -33,14 +33,14 @@ export function updateUser(config) {
export function userSignUp(config) {
return request(
'USER_SIGNUP',
configureAxios().post('/users/signup', config)
"USER_SIGNUP",
configureAxios().post("/users/signup", config)
)
}
export function getUserInfos() {
return request(
'GET_USER',
configureAxios().get('/users/details')
"GET_USER",
configureAxios().get("/users/details")
)
}

View File

@ -1,52 +1,51 @@
// Html page
import 'file-loader?name=[name].[ext]!../index.html'
import "file-loader?name=[name].[ext]!../index.html"
// Import default image
import 'file-loader?name=img/[name].png!../img/noimage.png'
import "file-loader?name=img/[name].png!../img/noimage.png"
// Import favicon settings
import 'file-loader?name=[name].png!../img/android-chrome-192x192.png'
import 'file-loader?name=[name].png!../img/android-chrome-512x512.png'
import 'file-loader?name=[name].png!../img/apple-touch-icon.png'
import 'file-loader?name=[name].png!../img/favicon-16x16.png'
import 'file-loader?name=[name].png!../img/favicon-32x32.png'
import 'file-loader?name=[name].png!../img/favicon.ico'
import 'file-loader?name=[name].png!../img/safari-pinned-tab.svg'
import "file-loader?name=[name].png!../img/android-chrome-192x192.png"
import "file-loader?name=[name].png!../img/android-chrome-512x512.png"
import "file-loader?name=[name].png!../img/apple-touch-icon.png"
import "file-loader?name=[name].png!../img/favicon-16x16.png"
import "file-loader?name=[name].png!../img/favicon-32x32.png"
import "file-loader?name=[name].png!../img/favicon.ico"
import "file-loader?name=[name].png!../img/safari-pinned-tab.svg"
// Import manifest
import 'file-loader?name=[name].json!../manifest.json'
import "file-loader?name=[name].json!../manifest.json"
// Styles
import '../less/app.less'
import "../less/app.less"
// React
import React from 'react'
import ReactDOM from 'react-dom'
import { bindActionCreators } from 'redux'
import { Provider, connect } from 'react-redux'
import { Router } from 'react-router'
import { routerActions } from 'react-router-redux'
import React from "react"
import ReactDOM from "react-dom"
import { bindActionCreators } from "redux"
import { Provider, connect } from "react-redux"
import { Router } from "react-router"
// Action creators
import { dismissAlert } from './actions/alerts'
import { dismissAlert } from "./actions/alerts"
// Store
import store, { history } from './store'
import store, { history } from "./store"
// Components
import NavBar from './components/navbar'
import Alert from './components/alerts/alert'
import NavBar from "./components/navbar"
import Alert from "./components/alerts/alert"
// Routes
import getRoutes from './routes'
import getRoutes from "./routes"
function mapStateToProps(state) {
let torrentCount = 0;
if (state.torrentStore.has('torrents')) {
torrentCount = state.torrentStore.get('torrents').size;
if (state.torrentStore.has("torrents") && state.torrentStore.get("torrents") !== undefined) {
torrentCount = state.torrentStore.get("torrents").size;
}
return {
username: state.userStore.username,
username: state.userStore.get("username"),
torrentCount: torrentCount,
alerts: state.alerts,
}
@ -80,4 +79,4 @@ ReactDOM.render((
<Provider store={store}>
<Router history={history} routes={getRoutes(App)} />
</Provider>
),document.getElementById('app'));
),document.getElementById("app"));

View File

@ -1,17 +1,16 @@
import React from 'react'
import SweetAlert from 'react-bootstrap-sweetalert';
import React from "react"
import SweetAlert from "react-bootstrap-sweetalert";
export default function Alert(props) {
if (!props.alerts.show) {
if (!props.alerts.get("show")) {
return null
}
return (
<SweetAlert
type={props.alerts.type}
type={props.alerts.get("type")}
title={props.alerts.get("message")}
onConfirm={props.dismissAlert}
title={props.alerts.message}
/>
)
}

View File

@ -1,8 +1,10 @@
import React from 'react'
import React from "react"
import { MenuItem } from 'react-bootstrap'
import { MenuItem } from "react-bootstrap"
export class WishlistButton extends React.Component {
import RefreshIndicator from "./refresh"
export class WishlistButton extends React.PureComponent {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
@ -36,7 +38,7 @@ export class WishlistButton extends React.Component {
}
}
export class DeleteButton extends React.Component {
export class DeleteButton extends React.PureComponent {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
@ -56,7 +58,7 @@ export class DeleteButton extends React.Component {
}
}
export class RefreshButton extends React.Component {
export class RefreshButton extends React.PureComponent {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
@ -71,16 +73,7 @@ export class RefreshButton extends React.Component {
render() {
return (
<MenuItem onClick={this.handleClick}>
{this.props.fetching ||
<span>
<i className="fa fa-refresh"></i> Refresh
</span>
}
{this.props.fetching &&
<span>
<i className="fa fa-spin fa-refresh"></i> Refreshing
</span>
}
<RefreshIndicator refresh={this.props.fetching} />
</MenuItem>
);
}

View File

@ -1,8 +1,8 @@
import React from 'react'
import React from "react"
import { Button, Dropdown, MenuItem, Modal } from 'react-bootstrap'
import { Button, Dropdown, MenuItem, Modal } from "react-bootstrap"
export default class DownloadButton extends React.Component {
export default class DownloadButton extends React.PureComponent {
constructor(props) {
super(props);
this.showModal = this.showModal.bind(this);
@ -66,30 +66,25 @@ export default class DownloadButton extends React.Component {
}
}
class Player extends React.Component {
class Player extends React.PureComponent {
constructor(props) {
super(props);
var subtitles = [];
if (props.subtitles && props.subtitles.length) {
subtitles = props.subtitles;
}
this.state = {
subtitles: subtitles,
};
}
render() {
const subtitles = this.props.subtitles;
const hasSubtitles = !(subtitles === undefined || subtitles === null || subtitles.size === 0);
return (
<div className="embed-responsive embed-responsive-16by9">
<video controls>
<source src={this.props.url} type="video/mp4"/>
{this.props.subtitles.map(function(el, index) {
{hasSubtitles && subtitles.toIndexedSeq().map(function(el, index) {
return (
<track
key={index}
kind="subtitles"
label={el.language}
src={el.vvt_file}
srcLang={el.language}
label={el.get("language")}
src={el.get("vvt_file")}
srcLang={el.get("language")}
/>
);
})}

View File

@ -0,0 +1,15 @@
import React from "react"
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;
}
return (
<a type="button" className={className} href={link}>
<i className="fa fa-external-link"></i> IMDB
</a>
);
}

View File

@ -0,0 +1,18 @@
import React from "react"
export default function RefreshIndicator(props) {
if (!props.refresh) {
return (
<span>
<i className="fa fa-refresh"></i> Refresh
</span>
);
} else {
return (
<span>
<i className="fa fa-spin fa-refresh"></i> Refreshing
</span>
);
}
}

View File

@ -1,97 +1,60 @@
import React from 'react'
import React from "react"
import { DropdownButton, MenuItem } from 'react-bootstrap'
import { DropdownButton, MenuItem } from "react-bootstrap"
export default class SubtitlesButton extends React.Component {
import RefreshIndicator from "./refresh"
export default function SubtitlesButton(props) {
const btnSize = props.xs ? "xsmall" : "small";
const subtitles = props.subtitles;
const hasSubtitles = !(subtitles === undefined || subtitles === null || subtitles.size === 0);
return (
<DropdownButton
bsStyle="success"
bsSize={btnSize}
title="Subtitles"
id="download-subtitles-button"
dropup>
<RefreshButton
type={props.type}
resourceID={props.resourceID}
season={props.season}
episode={props.episode}
fetching={props.fetching}
refreshSubtitles={props.refreshSubtitles}
/>
{hasSubtitles &&
<MenuItem divider></MenuItem>
}
{hasSubtitles && subtitles.toIndexedSeq().map(function(subtitle, index) {
return (
<MenuItem key={index} href={subtitle.get("url")}>
<i className="fa fa-download"></i> &nbsp;{subtitle.get("language").split("_")[1]}
</MenuItem>
);
})}
</DropdownButton>
);
}
class RefreshButton extends React.PureComponent {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick(e, url) {
handleClick(e) {
e.preventDefault();
if (this.props.fetching) {
return
}
// Refresh the subtitles
this.props.refreshSubtitles(this.props.type, this.props.resourceID, this.props.season, this.props.episode);
this.props.refreshSubtitles(this.props.type, this.props.resourceID,
this.props.season, this.props.episode);
}
render() {
// If there is no URL, the resource is not in polochon, we won't be able to download subtitles
if (this.props.url === "") {
return null;
}
// Build the button
const entries = buildMenuItems(this.props.subtitles);
let btnSize = "small";
if (this.props.xs) {
btnSize = "xsmall";
}
return (
<DropdownButton bsStyle="success" bsSize={btnSize} title="Subtitles" id="download-subtitles-button" dropup>
{entries.map(function(e, index) {
switch (e.type) {
case 'action':
return (
<MenuItem key={index} onClick={(event) => this.handleClick(event, e.url)}>
{this.props.fetchingSubtitles ||
<span>
<i className="fa fa-refresh">
</i> Refresh
</span>
}
{this.props.fetchingSubtitles &&
<span>
<i className="fa fa-spin fa-refresh">
</i> Refreshing
</span>
}
</MenuItem>
);
case 'divider':
return (
<MenuItem key={index} divider></MenuItem>
);
case 'entry':
return (
<MenuItem key={index} href={e.url}>
<i className="fa fa-download"></i> {e.lang}
</MenuItem>
);
}
}, this)}
</DropdownButton>
<MenuItem onClick={this.handleClick}>
<RefreshIndicator refresh={this.props.fetching} />
</MenuItem>
);
}
}
function buildMenuItems(subtitles) {
// Build the array of entries
let entries = [];
// Push the refresh button
entries.push({
type: "action",
value: "Refresh",
});
// If there is no subtitles, stop here
if (!subtitles) {
return entries;
}
// Push the divider
entries.push({ type: "divider" });
// Push the subtitles
for (let sub of subtitles) {
entries.push({
type: "entry",
// Take only the last part of fr_FR
lang: sub.language.split("_")[1],
url: sub.url,
});
}
return entries;
}

View File

@ -1,61 +1,96 @@
import React from 'react'
import React from "react"
export default function ListDetails(props) {
let genres;
if (props.data.genres) {
// Uppercase first genres
genres = props.data.genres.map(
(word) => word[0].toUpperCase() + word.substr(1)
).join(', ');
}
let wishlistStr = "";
if (props.data.wishlisted === true) {
wishlistStr = "Wishlisted";
}
if (props.data.tracked_episode !== null && props.data.tracked_season != null) {
let season = props.data.tracked_season;
let episode = props.data.tracked_episode;
if ((season === 0) && (episode === 0)) {
wishlistStr = "Whole show tracked";
} else {
wishlistStr = `Tracked from season ${season} episode ${episode}`;
}
}
return (
<div className="col-xs-7 col-md-4">
<div className="affix">
<h1 className="hidden-xs">{props.data.title}</h1>
<h3 className="visible-xs">{props.data.title}</h3>
{wishlistStr !== "" &&
<span className="label label-default">
<i className="fa fa-bookmark"></i> {wishlistStr}
</span>
}
<h4>{props.data.year}</h4>
{props.data.runtime &&
<p>
<i className="fa fa-clock-o"></i>
&nbsp;{props.data.runtime} min
</p>
}
{props.data.genres &&
<p>
<i className="fa fa-tags"></i>
&nbsp;{genres}
</p>
}
<p>
<i className="fa fa-star-o"></i>
&nbsp;{Number(props.data.rating).toFixed(1)}&nbsp;
{props.data.votes &&
<small>({props.data.votes} counts)</small>
}
</p>
<p className="plot">{props.data.plot}</p>
<h1 className="hidden-xs">{props.data.get("title")}</h1>
<h3 className="visible-xs">{props.data.get("title")}</h3>
<TrackingLabel
wishlisted={props.data.get("wishlisted")}
trackedSeason={props.data.get("tracked_season")}
trackedEpisode={props.data.get("tracked_episode")}
/>
<h4>{props.data.get("year")}</h4>
<Runtime runtime={props.data.get("runtime")} />
<Genres genres={props.data.get("genres")} />
<Ratings
rating={props.data.get("rating")}
votes={props.data.get("votes")}
/>
<p className="plot">{props.data.get("plot")}</p>
</div>
{props.children}
</div>
);
}
function Runtime(props) {
if (props.runtime === undefined) {
return null;
}
return (
<p>
<i className="fa fa-clock-o"></i>
&nbsp;{props.runtime} min
</p>
);
}
function Ratings(props) {
if (props.rating === undefined) {
return null;
}
return (
<p>
<i className="fa fa-star-o"></i>
&nbsp;{Number(props.rating).toFixed(1)}&nbsp;
{props.votes !== undefined &&
<small>({props.votes} counts)</small>
}
</p>
);
}
function TrackingLabel(props) {
let wishlistStr = props.wishlisted ? "Wishlisted" : "";
if (props.trackedEpisode !== null && props.trackedSeason !== null
&& props.trackedEpisode !== undefined && props.trackedSeason !== undefined) {
if ((props.trackedSeason === 0) && (props.trackedEpisode === 0)) {
wishlistStr = "Whole show tracked";
} else {
wishlistStr = `Tracked from season ${props.trackedSeason} episode ${props.trackedEpisode}`;
}
}
if (wishlistStr === "") {
return null;
}
return (
<span className="label label-default">
<i className="fa fa-bookmark"></i> {wishlistStr}
</span>
);
}
function Genres(props) {
if (props.genres === undefined) {
return null;
}
// Uppercase first genres
const prettyGenres = props.genres.toJS().map(
(word) => word[0].toUpperCase() + word.substr(1)
).join(", ");
return (
<p>
<i className="fa fa-tags"></i>
&nbsp;{prettyGenres}
</p>
);
}

View File

@ -1,7 +1,7 @@
import React from 'react'
import { Form, FormGroup, FormControl, ControlLabel } from 'react-bootstrap'
import React from "react"
import { Form, FormGroup, FormControl, ControlLabel } from "react-bootstrap"
export default class ExplorerOptions extends React.Component {
export default class ExplorerOptions extends React.PureComponent {
constructor(props) {
super(props);
this.handleSourceChange = this.handleSourceChange.bind(this);
@ -9,7 +9,7 @@ export default class ExplorerOptions extends React.Component {
}
handleSourceChange(event) {
let source = event.target.value;
let category = this.props.options[event.target.value][0];
let category = this.props.options.get(event.target.value).first();
this.props.router.push(`/${this.props.type}/explore/${source}/${category}`);
}
handleCategoryChange(event) {
@ -40,7 +40,7 @@ export default class ExplorerOptions extends React.Component {
}
// Options are not yet fetched
if (Object.keys(this.props.options).length === 0) {
if (this.props.options.size === 0) {
return null;
}
@ -51,7 +51,7 @@ export default class ExplorerOptions extends React.Component {
let source = this.props.params.source;
let category = this.props.params.category;
let categories = this.props.options[this.props.params.source];
let categories = this.props.options.get(this.props.params.source);
return (
<div className="row">
@ -67,8 +67,9 @@ export default class ExplorerOptions extends React.Component {
onChange={this.handleSourceChange}
value={source}
>
{Object.keys(this.props.options).map(function(source) {
return (<option key={source} value={source}>{this.prettyName(source)}</option>)
{this.props.options.keySeq().map(function(source) {
return (
<option key={source} value={source}>{this.prettyName(source)}</option>)
}, this)}
</FormControl>
</FormGroup>

View File

@ -1,30 +1,42 @@
import React from 'react'
import { Control, Form } from 'react-redux-form';
import React from "react"
export default function ListFilter(props) {
if (!props.display) {
return null;
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 });
if (props.listSize === 0) {
return null;
// Start filtering at 3 chars
if (value.length >= 3) {
this.props.updateFilter(value);
} else {
this.props.updateFilter("");
}
}
return (
<div className="row">
<div className="col-xs-12 col-md-12 list-filter">
<Form model={props.formModel} className="input-group hidebtn-xs" >
<Control.text
model={props.controlModel}
className="form-control input-sm"
placeholder={props.controlPlaceHolder}
updateOn="change"
/>
<span className="input-group-btn hidden-xs">
<button className="btn btn-default btn-sm" type="button">Filter</button>
</span>
</Form>
render() {
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>
</div>
</div>
);
);
}
}

View File

@ -1,30 +1,41 @@
import React from 'react'
import React from "react"
export default function ListPoster(props) {
const selected = props.selected ? ' thumbnail-selected' : '';
const imgClass = 'thumbnail' + selected;
const displayClearFixLg = (props.index % 6) === 0;
const displayClearFixMd = (props.index % 4) === 0;
const displayClearFixSm = (props.index % 2) === 0;
return (
<div>
{displayClearFixLg &&
<div className="clearfix visible-lg"></div>
}
{displayClearFixMd &&
<div className="clearfix visible-md"></div>
}
{displayClearFixSm &&
<div className="clearfix visible-sm"></div>
}
<div className="col-xs-12 col-sm-6 col-md-3 col-lg-2">
<a className={imgClass}>
<img
src={props.data.poster_url}
onClick={props.onClick}
/>
</a>
export default class Poster extends React.Component {
constructor(props) {
super(props);
}
shouldComponentUpdate(nextProps) {
if (nextProps.index !== this.props.index) { return true }
if (nextProps.selected !== this.props.selected) { return true }
if (nextProps.data.get("poster_url") !== this.props.data.get("poster_url")) { return true }
return false;
}
render() {
const selected = this.props.selected ? " thumbnail-selected" : "";
const imgClass = "thumbnail" + selected;
const displayClearFixLg = (this.props.index % 6) === 0;
const displayClearFixMd = (this.props.index % 4) === 0;
const displayClearFixSm = (this.props.index % 2) === 0;
return (
<div>
{displayClearFixLg &&
<div className="clearfix visible-lg"></div>
}
{displayClearFixMd &&
<div className="clearfix visible-md"></div>
}
{displayClearFixSm &&
<div className="clearfix visible-sm"></div>
}
<div className="col-xs-12 col-sm-6 col-md-3 col-lg-2">
<a className={imgClass}>
<img
src={this.props.data.get("poster_url")}
onClick={this.props.onClick}
/>
</a>
</div>
</div>
</div>
);
);
}
}

View File

@ -1,18 +1,17 @@
import React from 'react'
import React from "react"
import fuzzy from 'fuzzy';
import InfiniteScroll from 'react-infinite-scroller';
import fuzzy from "fuzzy";
import InfiniteScroll from "react-infinite-scroller";
import ListFilter from './filter'
import ExplorerOptions from './explorerOptions'
import ListPoster from './poster'
import ListFilter from "./filter"
import ExplorerOptions from "./explorerOptions"
import Poster from "./poster"
import Loader from '../loader/loader'
import Loader from "../loader/loader"
const DEFAULT_LIST_SIZE = 30;
const DEFAULT_ADD_EXTRA_ITEMS = 30;
export default class ListPosters extends React.Component {
export default class ListPosters extends React.PureComponent {
constructor(props) {
super(props);
this.loadMore = this.loadMore.bind(this);
@ -24,14 +23,14 @@ export default class ListPosters extends React.Component {
return;
}
if (!this.props.data.length) {
if (this.props.data === undefined) {
return;
}
this.setState(this.getNextState(this.props));
}
getNextState(props) {
let totalListSize = props.data.length;
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;
@ -47,44 +46,44 @@ export default class ListPosters extends React.Component {
};
}
componentWillReceiveProps(nextProps) {
if (this.props.data.length !== nextProps.data.length) {
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.slice();
const listSize = elmts.length;
let elmts = this.props.data;
const listSize = elmts !== undefined ? elmts.size : 0;
const colSize = (listSize !== 0) ? "col-xs-5 col-md-8" : "col-xs-12";
// Filter the list of elements
if (this.props.filter !== "") {
const filtered = fuzzy.filter(this.props.filter, elmts, {
extract: (el) => el.title
});
elmts = filtered.map((el) => el.original);
elmts = elmts.filter((v) => fuzzy.test(this.props.filter, v.get("title")), this);
} else {
elmts = elmts.slice(0, this.state.items);
}
// Chose when to display filter / explore options
let displayFilter = true;
if (this.props.params
if ((this.props.params
&& this.props.params.category
&& this.props.params.category !== ""
&& this.props.params.source
&& this.props.params.source !== "") {
&& this.props.params.source !== "")
|| (listSize === 0)) {
displayFilter = false;
}
return (
<div className={colSize}>
<ListFilter
listSize={listSize}
display={displayFilter}
formModel={this.props.formModel}
controlModel={this.props.filterControlModel}
controlPlaceHolder={this.props.filterControlPlaceHolder}
/>
{displayFilter &&
<ListFilter
updateFilter={this.props.updateFilter}
placeHolder={this.props.placeHolder}
/>
}
<ExplorerOptions
type={this.props.type}
display={!displayFilter}
@ -105,39 +104,45 @@ export default class ListPosters extends React.Component {
}
}
function Posters(props) {
if (props.loading) {
return (<Loader />);
class Posters extends React.PureComponent {
constructor(props) {
super(props);
}
render() {
if (this.props.loading) {
return (<Loader />);
}
if (this.props.elmts.size === 0) {
return (
<div className="jumbotron">
<h2>No result</h2>
</div>
);
}
if (props.elmts.length === 0) {
return (
<div className="jumbotron">
<h2>No result</h2>
<div>
<InfiniteScroll
hasMore={this.props.hasMore}
loadMore={this.props.loadMore}
className="row"
>
{this.props.elmts.toIndexedSeq().map(function(movie, index) {
const imdbId = movie.get("imdb_id");
const selected = (imdbId === this.props.selectedImdbId) ? true : false;
return (
<Poster
index={index}
data={movie}
key={imdbId}
selected={selected}
onClick={() => this.props.onClick(imdbId)}
/>
)
} ,this)}
</InfiniteScroll>
</div>
);
}
return (
<div>
<InfiniteScroll
hasMore={props.hasMore}
loadMore={props.loadMore}
className="row"
>
{props.elmts.map(function(el, index) {
const selected = (el.imdb_id === props.selectedImdbId) ? true : false;
return (
<ListPoster
index={index}
data={el}
key={el.imdb_id}
selected={selected}
onClick={() => props.onClick(el.imdb_id)}
/>
)}
)}
</InfiniteScroll>
</div>
);
}

View File

@ -1,5 +1,5 @@
import React from 'react'
import Loading from 'react-loading'
import React from "react"
import Loading from "react-loading"
export default function Loader() {
return (

View File

@ -1,7 +1,7 @@
import React from 'react'
import React from "react"
import { WishlistButton, DeleteButton, RefreshButton } from '../buttons/actions'
import { DropdownButton } from 'react-bootstrap'
import { WishlistButton, DeleteButton, RefreshButton } from "../buttons/actions"
import { DropdownButton } from "react-bootstrap"
export default function ActionsButton(props) {
return (

View File

@ -1,112 +1,112 @@
import React from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { addTorrent } from '../../actions/torrents'
import { refreshSubtitles } from '../../actions/subtitles'
import { addMovieToWishlist, deleteMovieFromWishlist,
getMovieDetails, selectMovie } from '../../actions/movies'
import React from "react"
import { connect } from "react-redux"
import { bindActionCreators } from "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 TorrentsButton from './torrents'
import ActionsButton from './actions'
import ListPosters from '../list/posters'
import ListDetails from '../list/details'
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"
function mapStateToProps(state) {
return { movieStore: state.movieStore };
return {
loading : state.movieStore.get("loading"),
movies : state.movieStore.get("movies"),
filter : state.movieStore.get("filter"),
selectedImdbId : state.movieStore.get("selectedImdbId"),
lastFetchUrl : state.movieStore.get("lastFetchUrl"),
exploreOptions : state.movieStore.get("exploreOptions"),
};
}
const mapDispatchToProps = (dipatch) =>
bindActionCreators({ selectMovie, getMovieDetails, addTorrent,
addMovieToWishlist, deleteMovieFromWishlist, refreshSubtitles }, dipatch)
addMovieToWishlist, deleteMovie, deleteMovieFromWishlist,
refreshSubtitles, updateFilter }, dipatch)
function MovieButtons(props) {
const imdb_link = `http://www.imdb.com/title/${props.movie.imdb_id}`;
const hasMovie = (props.movie.polochon_url !== "");
const hasMovie = (props.movie.get("polochon_url") !== "");
return (
<div className="list-details-buttons btn-toolbar">
<ActionsButton
fetching={props.fetching}
movieId={props.movie.imdb_id}
fetching={props.movie.get("fetchingDetails")}
movieId={props.movie.get("imdb_id")}
getDetails={props.getMovieDetails}
deleteMovie={props.deleteMovie}
hasMovie={hasMovie}
wishlisted={props.movie.wishlisted}
wishlisted={props.movie.get("wishlisted")}
addToWishlist={props.addToWishlist}
deleteFromWishlist={props.deleteFromWishlist}
lastFetchUrl={props.lastFetchUrl}
fetchMovies={props.fetchMovies}
/>
{props.movie.torrents &&
<TorrentsButton
torrents={props.movie.torrents}
addTorrent={props.addTorrent}
/>
}
{props.movie.get("torrents") !== null &&
<TorrentsButton
torrents={props.movie.get("torrents")}
addTorrent={props.addTorrent}
/>
}
<DownloadButton
url={props.movie.polochon_url}
subtitles={props.movie.subtitles}
/>
<DownloadButton
url={props.movie.get("polochon_url")}
subtitles={props.movie.get("subtitles")}
/>
<SubtitlesButton
url={props.movie.polochon_url}
subtitles={props.movie.subtitles}
refreshSubtitles={props.refreshSubtitles}
resourceID={props.movie.imdb_id}
data={props.movie}
type="movie"
/>
{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"
/>
}
<a type="button" className="btn btn-warning btn-sm" href={imdb_link}>
<i className="fa fa-external-link"></i> IMDB
</a>
<ImdbButton imdbId={props.movie.get("imdb_id")} size="sm"/>
</div>
);
}
class MovieList extends React.Component {
class MovieList extends React.PureComponent {
constructor(props) {
super(props);
}
render() {
const movies = this.props.movieStore.movies;
const selectedMovieId = this.props.movieStore.selectedImdbId;
let index = movies.map((el) => el.imdb_id).indexOf(selectedMovieId);
if (index === -1) {
index = 0;
let selectedMovie = undefined;
if (this.props.movies !== undefined && this.props.movies.has(this.props.selectedImdbId)) {
selectedMovie = this.props.movies.get(this.props.selectedImdbId);
}
const selectedMovie = movies[index];
return (
<div className="row" id="container">
<ListPosters
data={movies}
data={this.props.movies}
type="movies"
formModel="movieStore"
filterControlModel="movieStore.filter"
filterControlPlaceHolder="Filter movies..."
exploreOptions={this.props.movieStore.exploreOptions}
selectedImdbId={selectedMovieId}
filter={this.props.movieStore.filter}
perPage={this.props.movieStore.perPage}
placeHolder="Filter movies..."
exploreOptions={this.props.exploreOptions}
selectedImdbId={this.props.selectedImdbId}
updateFilter={this.props.updateFilter}
filter={this.props.filter}
onClick={this.props.selectMovie}
params={this.props.params}
router={this.props.router}
loading={this.props.movieStore.loading}
loading={this.props.loading}
/>
{selectedMovie &&
{selectedMovie !== undefined &&
<ListDetails data={selectedMovie}>
<MovieButtons
movie={selectedMovie}
fetching={this.props.movieStore.fetchingDetails}
getMovieDetails={this.props.getMovieDetails}
addTorrent={this.props.addTorrent}
deleteMovie={this.props.deleteMovie}
addToWishlist={this.props.addMovieToWishlist}
deleteFromWishlist={this.props.deleteMovieFromWishlist}
lastFetchUrl={this.props.movieStore.lastFetchUrl}
lastFetchUrl={this.props.lastFetchUrl}
refreshSubtitles={this.props.refreshSubtitles}
/>
</ListDetails>

View File

@ -1,8 +1,8 @@
import React from 'react'
import React from "react"
import { DropdownButton, MenuItem } from 'react-bootstrap'
import { DropdownButton, MenuItem } from "react-bootstrap"
export default class TorrentsButton extends React.Component {
export default class TorrentsButton extends React.PureComponent {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
@ -17,17 +17,17 @@ export default class TorrentsButton extends React.Component {
<DropdownButton className="btn btn-default btn-sm" title="Torrents" id="download-torrents-button" dropup>
{entries.map(function(e, index) {
switch (e.type) {
case 'header':
case "header":
return (
<MenuItem key={index} className="text-warning" header>
{e.value}
</MenuItem>
);
case 'divider':
case "divider":
return (
<MenuItem key={index} divider></MenuItem>
);
case 'entry':
case "entry":
return (
<MenuItem key={index} href={e.url} onClick={(event) => this.handleClick(event, e.url)}>
{e.quality}
@ -41,20 +41,12 @@ export default class TorrentsButton extends React.Component {
}
function buildMenuItems(torrents) {
// Organise by source
let sources = {}
for (let torrent of torrents) {
if (!sources[torrent.source]) {
sources[torrent.source] = [];
}
sources[torrent.source].push(torrent);
}
const t = torrents.groupBy((el) => el.get("source"));
// Build the array of entries
let entries = [];
let sourceNames = Object.keys(sources);
let dividerCount = sourceNames.length - 1;
for (let source of sourceNames) {
let dividerCount = t.size - 1;
for (let [source, torrentList] of t.entrySeq()) {
// Push the title
entries.push({
type: "header",
@ -62,11 +54,11 @@ function buildMenuItems(torrents) {
});
// Push the torrents
for (let torrent of sources[source]) {
for (let torrent of torrentList) {
entries.push({
type: "entry",
quality: torrent.quality,
url: torrent.url,
quality: torrent.get("quality"),
url: torrent.get("url"),
});
}

View File

@ -1,6 +1,6 @@
import React from 'react'
import { Nav, Navbar, NavItem, NavDropdown, MenuItem } from 'react-bootstrap'
import { LinkContainer } from 'react-router-bootstrap'
import React from "react"
import { Nav, Navbar, NavItem, NavDropdown, MenuItem } from "react-bootstrap"
import { LinkContainer } from "react-router-bootstrap"
export default class NavBar extends React.PureComponent {
constructor(props) {
@ -25,10 +25,10 @@ export default class NavBar extends React.PureComponent {
}
}
shouldDisplayMoviesSearch(router) {
return this.matchPath(router, 'movies');
return this.matchPath(router, "movies");
}
shouldDisplayShowsSearch(router) {
return this.matchPath(router, 'shows');
return this.matchPath(router, "shows");
}
matchPath(router, keyword) {
const location = router.getCurrentLocation().pathname;
@ -103,7 +103,7 @@ class Search extends React.Component {
}
}
function MoviesDropdown(props) {
function MoviesDropdown() {
return(
<Nav>
<NavDropdown title="Movies" id="navbar-movies-dropdown">
@ -118,7 +118,7 @@ function MoviesDropdown(props) {
);
}
function ShowsDropdown(props) {
function ShowsDropdown() {
return(
<Nav>
<NavDropdown title="Shows" id="navbar-shows-dropdown">
@ -161,7 +161,7 @@ function UserDropdown(props) {
}
}
function WishlistDropdown(props) {
function WishlistDropdown() {
return(
<Nav>
<NavDropdown title="Wishlist" id="navbar-wishlit-dropdown">

View File

@ -1,22 +1,22 @@
import React from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { toJS } from 'immutable'
import { addTorrent } from '../../actions/torrents'
import { refreshSubtitles } from '../../actions/subtitles'
import { addShowToWishlist, deleteShowFromWishlist, getEpisodeDetails, updateShowDetails } from '../../actions/shows'
import React from "react"
import { connect } from "react-redux"
import { bindActionCreators } from "redux"
import { addTorrent } from "../../actions/torrents"
import { refreshSubtitles } from "../../actions/subtitles"
import { addShowToWishlist, deleteShowFromWishlist, getEpisodeDetails, updateShowDetails } from "../../actions/shows"
import Loader from '../loader/loader'
import DownloadButton from '../buttons/download'
import SubtitlesButton from '../buttons/subtitles'
import Loader from "../loader/loader"
import DownloadButton from "../buttons/download"
import SubtitlesButton from "../buttons/subtitles"
import ImdbButton from "../buttons/imdb"
import RefreshIndicator from "../buttons/refresh"
import { OverlayTrigger, Tooltip } from 'react-bootstrap'
import { OverlayTrigger, Tooltip } from "react-bootstrap"
function mapStateToProps(state) {
return {
loading: state.showStore.loading,
show: state.showStore.get('show'),
show: state.showStore.get("show"),
};
}
const mapDispatchToProps = (dispatch) =>
@ -70,31 +70,28 @@ function Header(props){
function HeaderThumbnail(props){
return (
<div className="col-xs-12 col-sm-2 text-center">
<img src={props.data.get('poster_url')}
<img src={props.data.get("poster_url")}
className="show-thumbnail thumbnail-selected img-thumbnail img-responsive"/>
</div>
);
}
function HeaderDetails(props){
const imdbLink = `http://www.imdb.com/title/${props.data.get('imdb_id')}`;
return (
<div className="col-xs-12 col-sm-10">
<dl className="dl-horizontal">
<dt>Title</dt>
<dd>{props.data.get('title')}</dd>
<dd>{props.data.get("title")}</dd>
<dt>Plot</dt>
<dd className="plot">{props.data.get('plot')}</dd>
<dd className="plot">{props.data.get("plot")}</dd>
<dt>IMDB</dt>
<dd>
<a type="button" className="btn btn-warning btn-xs" href={imdbLink}>
<i className="fa fa-external-link"></i> Open in IMDB
</a>
<ImdbButton imdbId={props.data.get("imdb_id")} size="xs"/>
</dd>
<dt>Year</dt>
<dd>{props.data.get('year')}</dd>
<dd>{props.data.get("year")}</dd>
<dt>Rating</dt>
<dd>{props.data.get('rating')}</dd>
<dd>{props.data.get("rating")}</dd>
</dl>
<TrackHeader
data={props.data}
@ -108,7 +105,7 @@ function HeaderDetails(props){
function SeasonsList(props){
return (
<div>
{props.data.get('seasons').entrySeq().map(function([season, data]) {
{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()}>
@ -156,7 +153,7 @@ class Season extends React.Component {
<table className="table table-striped table-hover">
<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 (
<Episode
key={key}
@ -177,12 +174,6 @@ class Season extends React.Component {
}
function Episode(props) {
// TODO: remove this when everything uses immutable
let subtitles;
if (props.data.has('subtitles') && props.data.get('subtitles')) {
subtitles = props.data.get('subtitles').toJS();
}
return (
<tr>
<th scope="row" className="col-xs-2">
@ -190,24 +181,26 @@ function Episode(props) {
data={props.data}
addToWishlist={props.addToWishlist}
/>
{props.data.get('episode')}
{props.data.get("episode")}
</th>
<td className="col-xs-12">
{props.data.get('title')}
{props.data.get("title")}
<span className="pull-right episode-buttons">
<SubtitlesButton
url={props.data.get('polochon_url')}
subtitles={subtitles}
refreshSubtitles={props.refreshSubtitles}
resourceID={props.data.get('show_imdb_id')}
season={props.data.get('season')}
episode={props.data.get('episode')}
type="episode"
xs
/>
{props.data.get('torrents') && props.data.get('torrents').toList().map(function(torrent) {
let key = `${props.data.get('season')}-${props.data.get('episode')}-${torrent.get('source')}-${torrent.get('quality')}`;
{props.data.get("polochon_url") !== "" &&
<SubtitlesButton
fetching={props.data.get("fetchingSubtitles")}
subtitles={props.data.get("subtitles")}
refreshSubtitles={props.refreshSubtitles}
resourceID={props.data.get("show_imdb_id")}
season={props.data.get("season")}
episode={props.data.get("episode")}
type="episode"
xs
/>
}
{props.data.get("torrents") && props.data.get("torrents").toList().map(function(torrent) {
let key = `${props.data.get("season")}-${props.data.get("episode")}-${torrent.get("source")}-${torrent.get("quality")}`;
return (
<Torrent
data={torrent}
@ -217,8 +210,8 @@ function Episode(props) {
)
})}
<DownloadButton
url={props.data.get('polochon_url')}
subtitles={subtitles}
url={props.data.get("polochon_url")}
subtitles={props.data.get("subtitles")}
xs
/>
<GetDetailsButton
@ -229,9 +222,9 @@ function Episode(props) {
</td>
</tr>
)
}
}
class Torrent extends React.Component {
class Torrent extends React.PureComponent {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
@ -245,25 +238,25 @@ class Torrent extends React.Component {
<span>
<a type="button"
className="btn btn-primary btn-xs"
onClick={(e) => this.handleClick(e, this.props.data.get('url'))}
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')}
<i className="fa fa-download"></i> {this.props.data.get("quality")}
</a>
</span>
)
}
}
class TrackHeader extends React.Component {
class TrackHeader extends React.PureComponent {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick(e, url) {
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 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);
@ -272,9 +265,8 @@ class TrackHeader extends React.Component {
}
}
render() {
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 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) {
@ -314,16 +306,16 @@ class TrackHeader extends React.Component {
}
}
class TrackButton extends React.Component {
class TrackButton extends React.PureComponent {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick(e, url) {
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');
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() {
@ -341,34 +333,25 @@ class TrackButton extends React.Component {
}
}
class GetDetailsButton extends React.Component {
class GetDetailsButton extends React.PureComponent {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick(e, url) {
handleClick(e) {
e.preventDefault();
if (this.props.data.get('fetching')) {
if (this.props.data.get("fetching")) {
return
}
const imdbId = this.props.data.get('show_imdb_id');
const season = this.props.data.get('season');
const episode = this.props.data.get('episode');
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.getEpisodeDetails(imdbId, season, episode);
}
render() {
return (
<a type="button" className="btn btn-xs btn-info" onClick={(e) => this.handleClick(e)}>
{this.props.data.get('fetching') ||
<span>
<i className="fa fa-refresh"></i> Refresh
</span>
}
{this.props.data.get('fetching') &&
<span>
<i className="fa fa-spin fa-refresh"></i> Refreshing
</span>
}
<RefreshIndicator refresh={this.props.data.get("fetching")} />
</a>
);
}

View File

@ -1,46 +1,48 @@
import React from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import React from "react"
import { connect } from "react-redux"
import { bindActionCreators } from "redux"
import { selectShow, addShowToWishlist,
deleteShowFromWishlist, getShowDetails } from '../../actions/shows'
deleteShowFromWishlist, getShowDetails, updateFilter } from "../../actions/shows"
import ListDetails from '../list/details'
import ListPosters from '../list/posters'
import ShowButtons from './listButtons'
import ListDetails from "../list/details"
import ListPosters from "../list/posters"
import ShowButtons from "./listButtons"
function mapStateToProps(state) {
return { showsStore: state.showsStore };
return {
loading : state.showsStore.get("loading"),
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"),
};
}
const mapDispatchToProps = (dispatch) =>
bindActionCreators({ selectShow, addShowToWishlist,
deleteShowFromWishlist, getShowDetails }, dispatch)
deleteShowFromWishlist, getShowDetails, updateFilter }, dispatch)
class ShowList extends React.Component {
class ShowList extends React.PureComponent {
render() {
const shows = this.props.showsStore.shows;
const selectedShowId = this.props.showsStore.selectedImdbId;
let index = shows.map((el) => el.imdb_id).indexOf(selectedShowId);
if (index === -1) {
index = 0;
let selectedShow;
if (this.props.selectedImdbId !== "") {
selectedShow = this.props.shows.get(this.props.selectedImdbId);
}
const selectedShow = shows[index];
return (
<div className="row" id="container">
<ListPosters
data={shows}
data={this.props.shows}
type="shows"
formModel="showsStore"
filterControlModel="showsStore.filter"
filterControlPlaceHolder="Filter shows..."
exploreOptions={this.props.showsStore.exploreOptions}
selectedImdbId={selectedShowId}
filter={this.props.showsStore.filter}
perPage={this.props.showsStore.perPage}
placeHolder="Filter shows..."
exploreOptions={this.props.exploreOptions}
updateFilter={this.props.updateFilter}
selectedImdbId={this.props.selectedImdbId}
filter={this.props.filter}
onClick={this.props.selectShow}
router={this.props.router}
params={this.props.params}
loading={this.props.showsStore.loading}
loading={this.props.loading}
/>
{selectedShow &&
<ListDetails data={selectedShow} >
@ -49,7 +51,7 @@ class ShowList extends React.Component {
deleteFromWishlist={this.props.deleteShowFromWishlist}
addToWishlist={this.props.addShowToWishlist}
getDetails={this.props.getShowDetails}
fetching={this.props.showsStore.getDetails}
updateFilter={this.props.updateFilter}
/>
</ListDetails>
}

View File

@ -1,12 +1,12 @@
import React from 'react'
import React from "react"
import { Link } from 'react-router'
import { DropdownButton } from 'react-bootstrap'
import { Link } from "react-router"
import { DropdownButton } from "react-bootstrap"
import { WishlistButton, RefreshButton } from '../buttons/actions'
import { WishlistButton, RefreshButton } from "../buttons/actions"
import ImdbButton from "../buttons/imdb"
export default function ShowButtons(props) {
const imdbLink = `http://www.imdb.com/title/${props.show.imdb_id}`;
return (
<div className="list-details-buttons btn-toolbar">
<ActionsButton
@ -14,12 +14,9 @@ export default function ShowButtons(props) {
addToWishlist={props.addToWishlist}
deleteFromWishlist={props.deleteFromWishlist}
getDetails={props.getDetails}
fetching={props.fetching}
/>
<a type="button" className="btn btn-warning btn-sm" href={imdbLink}>
<i className="fa fa-external-link"></i> IMDB
</a>
<Link type="button" className="btn btn-primary btn-sm" to={"/shows/details/" + props.show.imdb_id}>
<ImdbButton imdbId={props.show.get("imdb_id")} size="sm"/>
<Link type="button" className="btn btn-primary btn-sm" to={"/shows/details/" + props.show.get("imdb_id")}>
<i className="fa fa-external-link"></i> Details
</Link>
</div>
@ -27,16 +24,16 @@ export default function ShowButtons(props) {
}
function ActionsButton(props) {
let wishlisted = (props.show.tracked_season !== null && props.show.tracked_episode !== null);
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>
<RefreshButton
fetching={props.fetching}
resourceId={props.show.imdb_id}
fetching={props.show.get("fetchingDetails")}
resourceId={props.show.get("imdb_id")}
getDetails={props.getDetails}
/>
<WishlistButton
resourceId={props.show.imdb_id}
resourceId={props.show.get("imdb_id")}
wishlisted={wishlisted}
addToWishlist={props.addToWishlist}
deleteFromWishlist={props.deleteFromWishlist}

View File

@ -1,10 +1,10 @@
import React from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { addTorrent } from '../../actions/torrents'
import React from "react"
import { connect } from "react-redux"
import { bindActionCreators } from "redux"
import { addTorrent } from "../../actions/torrents"
function mapStateToProps(state) {
return { torrents: state.torrentStore.get('torrents') };
return { torrents: state.torrentStore.get("torrents") };
}
const mapDispatchToProps = (dispatch) =>
bindActionCreators({ addTorrent }, dispatch)
@ -24,7 +24,7 @@ export default connect(mapStateToProps, mapDispatchToProps)(TorrentList);
class AddTorrent extends React.PureComponent {
constructor(props) {
super(props);
this.state = { url: '' };
this.state = { url: "" };
this.handleSubmit = this.handleSubmit.bind(this);
this.handleChange = this.handleChange.bind(this);
}
@ -35,7 +35,7 @@ class AddTorrent extends React.PureComponent {
if (this.state.url === "") {
return;
}
this.setState({ url: '' });
this.setState({ url: "" });
this.props.func(this.state.url);
}
render() {
@ -96,23 +96,23 @@ class List extends React.PureComponent {
class Torrent extends React.PureComponent {
render() {
const done = this.props.data.get('is_finished');
var progressStyle = 'progress-bar progress-bar-warning';
const done = this.props.data.get("is_finished");
var progressStyle = "progress-bar progress-bar-warning";
if (done) {
progressStyle = 'progress-bar progress-bar-success';
progressStyle = "progress-bar progress-bar-success";
}
var percentDone = this.props.data.get('percent_done');
var percentDone = this.props.data.get("percent_done");
const started = (percentDone !== 0);
if (started) {
percentDone = Number(percentDone).toFixed(1) + '%';
percentDone = Number(percentDone).toFixed(1) + "%";
}
var downloadedSize = prettySize(this.props.data.get('downloaded_size'));
var totalSize = prettySize(this.props.data.get('total_size'));
var downloadRate = prettySize(this.props.data.get('download_rate')) + "/s";
var downloadedSize = prettySize(this.props.data.get("downloaded_size"));
var totalSize = prettySize(this.props.data.get("total_size"));
var downloadRate = prettySize(this.props.data.get("download_rate")) + "/s";
return (
<div className="panel panel-default">
<div className="panel-heading">{this.props.data.get('name')}</div>
<div className="panel-heading">{this.props.data.get("name")}</div>
<div className="panel-body">
{started &&
<div className="progress progress-striped">
@ -138,7 +138,7 @@ class Torrent extends React.PureComponent {
function prettySize(fileSizeInBytes) {
var i = -1;
var byteUnits = [' kB', ' MB', ' GB', ' TB', 'PB', 'EB', 'ZB', 'YB'];
var byteUnits = [" kB", " MB", " GB", " TB", "PB", "EB", "ZB", "YB"];
do {
fileSizeInBytes = fileSizeInBytes / 1024;
i++;

View File

@ -1,29 +1,55 @@
import React from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import React from "react"
import { connect } from "react-redux"
import { bindActionCreators } from "redux"
import { Control, Form } from 'react-redux-form';
import { updateUser } from '../../actions/users'
import { updateUser } from "../../actions/users"
function mapStateToProps(state) {
return { user: state.userStore };
return {
polochonToken: state.userStore.get("polochonToken"),
polochonUrl: state.userStore.get("polochonUrl"),
};
}
const mapDispatchToProps = (dispatch) =>
bindActionCreators({ updateUser }, dispatch)
class UserEdit extends React.Component {
class UserEdit extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
polochonToken: props.polochonToken,
polochonUrl: props.polochonUrl,
};
this.handleSubmit = this.handleSubmit.bind(this);
this.handleUrlInput = this.handleUrlInput.bind(this);
this.handleTokenInput = this.handleTokenInput.bind(this);
}
handleSubmit() {
handleSubmit(ev) {
ev.preventDefault();
this.props.updateUser({
'polochon_url': this.props.user.polochonUrl,
'polochon_token': this.props.user.polochonToken,
'password': this.refs.newPassword.value,
'password_confirm': this.refs.newPasswordConfirm.value,
"polochon_url": this.refs.polochonUrl.value,
"polochon_token": this.refs.polochonToken.value,
"password": this.refs.newPassword.value,
"password_confirm": this.refs.newPasswordConfirm.value,
});
}
handleTokenInput() {
this.setState({ polochonToken: this.refs.polochonToken.value });
}
handleUrlInput() {
this.setState({ polochonUrl: this.refs.polochonUrl.value });
}
componentWillReceiveProps(nextProps) {
if ((nextProps.polochonUrl !== "")
&& (this.state.polochonUrl === "")
&& (nextProps.polochonToken !== "")
&& (this.state.polochonToken === "")) {
this.setState({
polochonToken: nextProps.polochonToken,
polochonUrl: nextProps.polochonUrl,
});
}
}
render() {
return (
<div className="container">
@ -31,35 +57,44 @@ class UserEdit extends React.Component {
<div className="col-md-6 col-md-offset-3 col-xs-12">
<h2>Edit user</h2>
<hr />
<Form model="userStore" className="form-horizontal" onSubmit={(val) => this.handleSubmit(val)}>
<form className="form-horizontal" onSubmit={(ev) => this.handleSubmit(ev)}>
<div className="form-group">
<label className="control-label">Polochon URL</label>
<Control.text model="userStore.polochonUrl" className="form-control" />
<input
className="form-control"
value={this.state.polochonUrl}
onChange={this.handleUrlInput}
ref="polochonUrl"
/>
</div>
<div className="form-group">
<label className="control-label">Polochon token</label>
<Control.text model="userStore.polochonToken" className="form-control"/>
<input
className="form-control"
value={this.state.polochonToken}
onChange={this.handleTokenInput}
ref="polochonToken"
/>
</div>
<hr />
<div className="form-group">
<label className="control-label">Password</label>
<input autoComplete="off" className="form-control" ref="newPassword" type="password"/>
</div>
<div className="form-group">
<label className="control-label">Password</label>
<input type="password" autoComplete="off" ref="newPassword" className="form-control"/>
</div>
<div className="form-group">
<label className="control-label">Confirm Password</label>
<input autoComplete="off" className="form-control" ref="newPasswordConfirm" type="password"/>
</div>
<div className="form-group">
<label className="control-label">Confirm Password</label>
<input type="password" autoComplete="off" ref="newPasswordConfirm" className="form-control"/>
</div>
<div>
<input className="btn btn-primary pull-right" type="submit" value="Update"/>
</div>
</Form>
</div>
<div>
<input type="submit" className="btn btn-primary pull-right" value="Update"/>
</div>
</form>
</div>
</div>
</div>
);

View File

@ -1,27 +1,30 @@
import React from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import React from "react"
import { connect } from "react-redux"
import { bindActionCreators } from "redux"
import { loginUser } from '../../actions/users'
import { loginUser } from "../../actions/users"
function mapStateToProps(state) {
return { user: state.userStore };
return {
isLogged: state.userStore.get("isLogged"),
loading: state.userStore.get("loading"),
};
}
const mapDispatchToProps = (dispatch) =>
bindActionCreators({ loginUser }, dispatch)
class UserLoginForm extends React.Component {
class UserLoginForm extends React.PureComponent {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
}
componentWillReceiveProps(nextProps) {
if (!nextProps.user.isLogged) {
if (!nextProps.isLogged) {
return
}
if (!nextProps.location.query.redirect) {
// Redirect home
nextProps.router.push('/');
nextProps.router.push("/");
} else {
// Redirect to the previous page
nextProps.router.push(nextProps.location.query.redirect);
@ -29,7 +32,7 @@ class UserLoginForm extends React.Component {
}
handleSubmit(e) {
e.preventDefault();
if (this.props.user.userLoading) {
if (this.props.loading) {
return;
}
const username = this.refs.username.value;
@ -57,12 +60,12 @@ class UserLoginForm extends React.Component {
<p></p>
</div>
<div>
{this.props.user.userLoading &&
{this.props.loading &&
<button className="btn btn-primary pull-right">
<i className="fa fa-spinner fa-spin"></i>
</button>
}
{this.props.user.userLoading ||
{this.props.loading ||
<input className="btn btn-primary pull-right" type="submit" value="Log in"/>
}
<br/>

View File

@ -1,13 +1,13 @@
import React from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import React from "react"
import { connect } from "react-redux"
import { bindActionCreators } from "redux"
import { userSignUp } from '../../actions/users'
import { userSignUp } from "../../actions/users"
const mapDispatchToProps = (dispatch) =>
bindActionCreators({ userSignUp }, dispatch)
class UserSignUp extends React.Component {
class UserSignUp extends React.PureComponent {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
@ -15,9 +15,9 @@ class UserSignUp extends React.Component {
handleSubmit(e) {
e.preventDefault();
this.props.userSignUp({
'username': this.refs.username.value,
'password': this.refs.password.value,
'password_confirm': this.refs.passwordConfirm.value,
"username": this.refs.username.value,
"password": this.refs.password.value,
"password_confirm": this.refs.passwordConfirm.value,
});
}
render() {

View File

@ -1,30 +1,29 @@
const defaultState = {
import { Map } from "immutable"
const defaultState = Map({
show: false,
message: "",
type: "",
};
});
export default function Alert(state = defaultState, action) {
switch (action.type) {
case 'ADD_ALERT_ERROR':
return Object.assign({}, state, {
message: action.payload.message,
show: true,
type: "error",
})
case 'ADD_ALERT_OK':
return Object.assign({}, state, {
const handlers = {
"ADD_ALERT_ERROR": (state, action) => state.merge(Map({
message: action.payload.message,
show: true,
type: "error",
})),
"ADD_ALERT_OK": (state, action) => state.merge(Map({
message: action.payload.message,
show: true,
type: "success",
})
case 'DISMISS_ALERT':
return Object.assign({}, state, {
})),
"DISMISS_ALERT": state => state.merge(Map({
message: "",
show: false,
type: "",
})
default:
return state;
}
})),
}
export default (state = defaultState, action) =>
handlers[action.type] ? handlers[action.type](state, action) : state;

View File

@ -1,17 +1,14 @@
import { combineForms } from 'react-redux-form'
import { routerReducer } from 'react-router-redux'
import { combineReducers } from "redux";
import { routerReducer } from "react-router-redux"
import movieStore from './movies'
import showsStore from './shows'
import showStore from './show'
import userStore from './users'
import alerts from './alerts'
import torrentStore from './torrents'
import movieStore from "./movies"
import showsStore from "./shows"
import showStore from "./show"
import userStore from "./users"
import alerts from "./alerts"
import torrentStore from "./torrents"
// Use combine form form react-redux-form, it's a thin wrapper arround the
// default combinedReducers provided with React. It allows the forms to be
// linked directly to the store.
const rootReducer = combineForms({
const rootReducer = combineReducers({
routing: routerReducer,
movieStore,
showsStore,

View File

@ -1,96 +1,52 @@
const defaultState = {
import { OrderedMap, Map, fromJS } from "immutable"
const defaultState = Map({
loading: false,
movies: [],
movies: OrderedMap(),
filter: "",
perPage: 30,
selectedImdbId: "",
fetchingDetails: false,
lastFetchUrl: "",
exploreOptions: {},
};
exploreOptions: Map(),
});
export default function movieStore(state = defaultState, action) {
switch (action.type) {
case 'MOVIE_LIST_FETCH_PENDING':
return Object.assign({}, state, {
loading: true,
})
case 'MOVIE_LIST_FETCH_FULFILLED':
let selectedImdbId = "";
// Select the first movie
if (action.payload.response.data.length > 0) {
// Sort by year
action.payload.response.data.sort((a,b) => b.year - a.year);
selectedImdbId = action.payload.response.data[0].imdb_id;
}
return Object.assign({}, state, {
movies: action.payload.response.data,
selectedImdbId: selectedImdbId,
filter: defaultState.filter,
perPage: defaultState.perPage,
loading: false,
})
case 'MOVIE_GET_DETAILS_PENDING':
return Object.assign({}, state, {
fetchingDetails: true,
})
case 'MOVIE_GET_DETAILS_FULFILLED':
return Object.assign({}, state, {
movies: updateMovieDetails(state.movies.slice(), action.payload.response.data.imdb_id, action.payload.response.data),
fetchingDetails: false,
})
case 'MOVIE_UPDATE_STORE_WISHLIST':
return Object.assign({}, state, {
movies: updateStoreWishlist(state.movies.slice(), action.payload.imdbId, action.payload.wishlisted),
})
case 'MOVIE_GET_EXPLORE_OPTIONS_FULFILLED':
return Object.assign({}, state, {
exploreOptions: action.payload.response.data,
})
case 'UPDATE_LAST_MOVIE_FETCH_URL':
return Object.assign({}, state, {
lastFetchUrl: action.payload.url,
})
case 'MOVIE_SUBTITLES_UPDATE_PENDING':
return Object.assign({}, state, {
movies: updateMovieSubtitles(state.movies.slice(), state.selectedImdbId, true),
})
case 'MOVIE_SUBTITLES_UPDATE_FULFILLED':
console.log("payload :", action.payload);
return Object.assign({}, state, {
movies: updateMovieSubtitles(state.movies.slice(), state.selectedImdbId, false, action.payload.response.data),
})
case 'SELECT_MOVIE':
// Don't select the movie if we're fetching another movie's details
if (state.fetchingDetails) {
return state
}
const handlers = {
"MOVIE_LIST_FETCH_PENDING": state => state.set("loading", true),
"MOVIE_LIST_FETCH_FULFILLED": (state, action) => {
let movies = Map();
action.payload.response.data.map(function (movie) {
movie.fetchingDetails = false;
movie.fetchingSubtitles = false;
movies = movies.set(movie.imdb_id, fromJS(movie));
})
return Object.assign({}, state, {
selectedImdbId: action.imdbId,
})
default:
return state
}
// Select the first movie if the list is not empty
let selectedImdbId = "";
if (movies.size > 0) {
// Sort by year
movies = movies.sort((a,b) => b.get("year") - a.get("year"));
selectedImdbId = movies.first().get("imdb_id");
}
return state.delete("movies").merge(Map({
movies: movies,
filter: "",
loading: false,
selectedImdbId: selectedImdbId,
}))
},
"MOVIE_GET_DETAILS_PENDING" : (state, action) => state.setIn(["movies", action.payload.main.imdbId, "fetchingDetails"], true),
"MOVIE_GET_DETAILS_FULFILLED" : (state, action) => state.setIn(["movies", action.payload.response.data.imdb_id], fromJS(action.payload.response.data))
.setIn(["movies", action.payload.response.data.imdb_id, "fetchingDetails"], false)
.setIn(["movies", action.payload.response.data.imdb_id, "fetchingSubtitles"], false),
"MOVIE_UPDATE_STORE_WISHLIST" : (state, action) => state.setIn(["movies", action.payload.imdbId, "wishlisted"], action.payload.wishlisted),
"MOVIE_GET_EXPLORE_OPTIONS_FULFILLED" : (state, action) => state.set("exploreOptions", fromJS(action.payload.response.data)),
"UPDATE_LAST_MOVIE_FETCH_URL" : (state, action) => state.set("lastFetchUrl", action.payload.url),
"MOVIE_SUBTITLES_UPDATE_PENDING" : (state, action) => state.setIn(["movies", action.payload.main.imdbId, "fetchingSubtitles"], true),
"MOVIE_SUBTITLES_UPDATE_FULFILLED" : (state, action) => state.setIn(["movies", action.payload.main.imdbId, "fetchingSubtitles"], false)
.setIn(["movies", action.payload.main.imdbId, "subtitles"], fromJS(action.payload.response.data)),
"SELECT_MOVIE" : (state, action) => state.set("selectedImdbId", action.payload.imdbId),
"MOVIE_UPDATE_FILTER" : (state, action) => state.set("filter", action.payload.filter),
}
function updateMovieDetails(movies, imdbId, data) {
let index = movies.map((el) => el.imdb_id).indexOf(imdbId);
movies[index] = data;
return movies
}
function updateStoreWishlist(movies, imdbId, wishlisted) {
let index = movies.map((el) => el.imdb_id).indexOf(imdbId);
movies[index].wishlisted = wishlisted;
return movies
}
function updateMovieSubtitles(movies, imdbId, fetching, data = null) {
let index = movies.map((el) => el.imdb_id).indexOf(imdbId);
if (data) {
movies[index].subtitles = data;
}
movies[index].fetchingSubtitles = fetching;
return movies
}
export default (state = defaultState, action) =>
handlers[action.type] ? handlers[action.type](state, action) : state;

View File

@ -1,4 +1,4 @@
import { OrderedMap, Map, List, fromJS } from 'immutable'
import { OrderedMap, Map, fromJS } from "immutable"
const defaultState = Map({
loading: false,
@ -7,88 +7,70 @@ const defaultState = Map({
}),
});
export default function showStore(state = defaultState, action) {
switch (action.type) {
case 'SHOW_FETCH_DETAILS_PENDING':
return state.set('loading', true)
case 'SHOW_FETCH_DETAILS_FULFILLED':
return sortEpisodes(state, action.payload.response.data);
case 'SHOW_UPDATE_STORE_WISHLIST':
let season = action.payload.season;
let episode = action.payload.episode;
if (action.payload.wishlisted && season === null) {
season = 0;
episode = 0;
const handlers = {
"SHOW_FETCH_DETAILS_PENDING": state => state.set("loading", true),
"SHOW_FETCH_DETAILS_FULFILLED": (state, action) => sortEpisodes(state, action.payload.response.data),
"SHOW_UPDATE_STORE_WISHLIST": (state, action) => {
let season = action.payload.season;
let episode = action.payload.episode;
if (action.payload.wishlisted && season === null) {
season = 0;
episode = 0;
}
return state.mergeDeep(fromJS({
"show": {
"tracked_season": season,
"tracked_episode": episode,
}
return state.mergeDeep(fromJS({
'show': {
'tracked_season': season,
'tracked_episode': episode,
}
}));
case 'EPISODE_GET_DETAILS_PENDING':
return state.setIn(['show', 'seasons', action.payload.main.season, action.payload.main.episode, 'fetching'], true);
case 'EPISODE_GET_DETAILS_FULFILLED':
let data = action.payload.response.data;
if (!data) { return state }
data.fetching = false;
return state.setIn(['show', 'seasons', data.season, data.episode], fromJS(data));
case 'EPISODE_SUBTITLES_UPDATE_PENDING':
return state.setIn(['show', 'seasons', action.payload.main.season, action.payload.main.episode, 'fetchingSubtitles'], true);
case 'EPISODE_SUBTITLES_UPDATE_FULFILLED':
let epId = ['show', 'seasons', action.payload.main.season, action.payload.main.episode];
let ep = state.getIn(epId);
ep = ep.set('subtitles', fromJS(action.payload.response.data)).set('fetchingSubtitles', false);
return state.setIn(epId, ep);
default:
return state
}
}))},
"EPISODE_GET_DETAILS_PENDING": (state, action) => state.setIn(["show", "seasons", action.payload.main.season, action.payload.main.episode, "fetching"], true),
"EPISODE_GET_DETAILS_FULFILLED": (state, action) => {
let data = action.payload.response.data;
if (!data) { return state }
data.fetching = false;
return state.setIn(["show", "seasons", data.season, data.episode], fromJS(data));
},
"EPISODE_SUBTITLES_UPDATE_PENDING" : (state, action) =>
state.setIn(["show", "seasons", action.payload.main.season, action.payload.main.episode, "fetchingSubtitles"], true),
"EPISODE_SUBTITLES_UPDATE_FULFILLED": (state, action) =>
state.setIn(["show", "seasons", action.payload.main.season, action.payload.main.episode, "subtitles"], fromJS(action.payload.response.data))
.setIn(["show", "seasons", action.payload.main.season, action.payload.main.episode, "fetchingSubtitles"], false),
}
function updateEpisode(state, fetching, data = null) {
if (data === null) {
return state;
}
if (!state.hasIn(['show', 'season', data.season, data.episode])) {
return show;
}
data.fetching = fetching
return show.updateIn(['show', 'seasons', data.season, data.episode], fromJS(data));
}
function sortEpisodes(state, show) {
const sortEpisodes = (state, show) => {
let episodes = show.episodes;
delete show["episodes"];
let ret = state.set('loading', false);
let ret = state.set("loading", false);
if (episodes.length == 0) {
return ret;
}
// Set the show data
ret = ret.set('show', fromJS(show));
ret = ret.set("show", fromJS(show));
// Set the show episodes
for (let ep of episodes) {
ep.fetching = false;
ret = ret.setIn(['show', 'seasons', ep.season, ep.episode], fromJS(ep));
ret = ret.setIn(["show", "seasons", ep.season, ep.episode], fromJS(ep));
}
// Sort the episodes
ret = ret.updateIn(['show', 'seasons'], function(seasons) {
ret = ret.updateIn(["show", "seasons"], function(seasons) {
return seasons.map(function(episodes) {
return episodes.sort((a,b) => a.get('episode') - b.get('episode'));
return episodes.sort((a,b) => a.get("episode") - b.get("episode"));
});
});
// Sort the seasons
ret = ret.updateIn(['show', 'seasons'], function(seasons) {
ret = ret.updateIn(["show", "seasons"], function(seasons) {
return seasons.sort(function(a,b) {
return a.first().get('season') - b.first().get('season');
return a.first().get("season") - b.first().get("season");
});
});
return ret
}
export default (state = defaultState, action) =>
handlers[action.type] ? handlers[action.type](state, action) : state;

View File

@ -1,101 +1,67 @@
const defaultState = {
import { OrderedMap, Map, fromJS } from "immutable"
const defaultState = Map({
loading: false,
shows: [],
shows: OrderedMap(),
filter: "",
perPage: 30,
selectedImdbId: "",
getDetails: false,
lastShowsFetchUrl: "",
exploreOptions: {},
};
lastFetchUrl: "",
exploreOptions: Map(),
});
export default function showsStore(state = defaultState, action) {
switch (action.type) {
case 'SHOW_LIST_FETCH_PENDING':
return Object.assign({}, state, {
loading: true,
})
case 'SHOW_LIST_FETCH_FULFILLED':
let selectedImdbId = "";
// Select the first show
console.log("Hey", action.payload);
if (action.payload.response.data.length > 0) {
selectedImdbId = action.payload.response.data[0].imdb_id;
}
return Object.assign({}, state, {
shows: action.payload.response.data,
selectedImdbId: selectedImdbId,
filter: defaultState.filter,
loading: false,
})
case 'SHOW_GET_DETAILS_PENDING':
return Object.assign({}, state, {
getDetails: true,
})
case 'SHOW_GET_DETAILS_FULFILLED':
return Object.assign({}, state, {
shows: updateShowDetails(state.shows.slice(), action.payload.response.data),
getDetails: false,
})
case 'EXPLORE_SHOWS_PENDING':
return Object.assign({}, state, {
loading: true,
})
case 'EXPLORE_SHOWS_FULFILLED':
return Object.assign({}, state, {
shows: action.payload.response.data,
loading: false,
})
case 'SHOW_GET_EXPLORE_OPTIONS_FULFILLED':
return Object.assign({}, state, {
exploreOptions: action.payload.response.data,
})
case 'SHOW_UPDATE_STORE_WISHLIST':
return Object.assign({}, state, {
shows: updateShowsStoreWishlist(state.shows.slice(), action.payload),
})
case 'UPDATE_LAST_SHOWS_FETCH_URL':
return Object.assign({}, state, {
lastShowsFetchUrl: action.payload.url,
})
case 'SELECT_SHOW':
// Don't select the show if we're fetching another show's details
if (state.fetchingDetails) {
return state
}
const handlers = {
"SHOW_LIST_FETCH_PENDING": state => state.set("loading", true),
"SHOW_LIST_FETCH_FULFILLED": (state, action) => {
let shows = Map();
action.payload.response.data.map(function (show) {
show.fetchingDetails = false;
show.fetchingSubtitles = false;
shows = shows.set(show.imdb_id, fromJS(show));
});
return Object.assign({}, state, {
selectedImdbId: action.imdbId,
})
default:
return state
}
}
// Update the store containing all the shows
function updateShowsStoreWishlist(shows, payload) {
if (shows.length === 0) {
return shows;
}
let index = shows.map((el) => el.imdb_id).indexOf(payload.imdbId);
let season = payload.season;
let episode = payload.episode;
if (payload.wishlisted) {
if (season === null) {
season = 0;
let selectedImdbId = "";
if (shows.size > 0) {
// Sort by year
shows = shows.sort((a,b) => b.get("year") - a.get("year"));
selectedImdbId = shows.first().get("imdb_id");
}
if (episode === null) {
episode = 0;
return state.delete("shows").merge(Map({
shows: shows,
filter: "",
loading: false,
selectedImdbId: selectedImdbId,
}));
},
"SHOW_GET_DETAILS_PENDING": (state, action) => state.setIn(["shows", action.payload.main.imdbId, "fetchingDetails"], true),
"SHOW_GET_DETAILS_FULFILLED": (state, action) => {
let show = action.payload.response.data;
show.fetchingDetails = false;
show.fetchingSubtitles = false;
return state.setIn(["shows", show.imdb_id], fromJS(show));
},
"SHOW_GET_EXPLORE_OPTIONS_FULFILLED": (state, action) => state.set("exploreOptions", fromJS(action.payload.response.data)),
"SHOW_UPDATE_STORE_WISHLIST": (state, action) => {
let season = action.payload.season;
let episode = action.payload.episode;
if (action.payload.wishlisted) {
if (season === null) {
season = 0;
}
if (episode === null) {
episode = 0;
}
}
}
shows[index].tracked_season = season;
shows[index].tracked_episode = episode;
return shows
return state.mergeIn(["shows", action.payload.imdbId], Map({
"tracked_season": season,
"tracked_episode": episode,
}));
},
"UPDATE_LAST_SHOWS_FETCH_URL": (state, action) => state.set("lastFetchUrl", action.payload.url),
"SELECT_SHOW": (state, action) => state.set("selectedImdbId", action.payload.imdbId),
"SHOWS_UPDATE_FILTER": (state, action) => state.set("filter", action.payload.filter),
}
function updateShowDetails(shows, data) {
let index = shows.map((el) => el.imdb_id).indexOf(data.imdb_id);
shows[index] = data;
return shows
}
export default (state = defaultState, action) =>
handlers[action.type] ? handlers[action.type](state, action) : state;

View File

@ -1,20 +1,17 @@
import { Map, List, fromJS } from 'immutable'
import { Map, List, fromJS } from "immutable"
const defaultState = Map({
'fetching': false,
'torrents': List(),
"fetching": false,
"torrents": List(),
});
export default function torrentStore(state = defaultState, action) {
switch (action.type) {
case 'TORRENTS_FETCH_PENDING':
return state.set('fetching', false);
case 'TORRENTS_FETCH_FULFILLED':
return state.merge(fromJS({
fetching: false,
torrents: action.payload.response.data,
}));
default:
return state
}
const handlers = {
"TORRENTS_FETCH_PENDING": state => state.set("fetching", false),
"TORRENTS_FETCH_FULFILLED": (state, action) => state.merge(fromJS({
fetching: false,
torrents: action.payload.response.data,
})),
}
export default (state = defaultState, action) =>
handlers[action.type] ? handlers[action.type](state, action) : state;

View File

@ -1,59 +1,54 @@
import jwtDecode from 'jwt-decode'
import Cookies from 'universal-cookie'
import { Map } from "immutable"
const defaultState = {
userLoading: false,
import jwtDecode from "jwt-decode"
import Cookies from "universal-cookie"
const defaultState = Map({
loading: false,
username: "",
isAdmin: false,
isLogged: false,
polochonToken: "",
polochonUrl: "",
};
});
export default function userStore(state = defaultState, action) {
switch (action.type) {
case 'USER_LOGIN_PENDING':
return Object.assign({}, state, {
userLoading: true,
})
case 'USER_LOGIN_FULFILLED':
if (action.payload.response.status === "error") {
return logoutUser(state)
}
return updateFromToken(state, action.payload.response.data.token)
case 'USER_SET_TOKEN':
return updateFromToken(state, action.payload.token)
case 'USER_LOGOUT':
return logoutUser(state)
case 'GET_USER_FULFILLED':
return Object.assign({}, state, {
polochonToken: action.payload.response.data.token,
polochonUrl: action.payload.response.data.url,
})
default:
return state;
}
const handlers = {
"USER_LOGIN_PENDING": state => state.set("loading", true),
"USER_LOGIN_FULFILLED": (state, action) => {
if (action.payload.response.status === "error") {
return logoutUser()
}
return updateFromToken(state, action.payload.response.data.token)
},
"USER_SET_TOKEN": (state, action) => updateFromToken(state, action.payload.token),
"USER_LOGOUT": () => logoutUser(),
"GET_USER_FULFILLED": (state, action) => state.merge(Map({
polochonToken: action.payload.response.data.token,
polochonUrl: action.payload.response.data.url,
})),
}
function logoutUser(state) {
localStorage.removeItem('token');
function logoutUser() {
localStorage.removeItem("token");
const cookies = new Cookies();
cookies.remove('token');
return Object.assign({}, state, defaultState)
cookies.remove("token");
return defaultState
}
function updateFromToken(state, token) {
const decodedToken = jwtDecode(token);
localStorage.setItem('token', token);
localStorage.setItem("token", token);
const cookies = new Cookies();
cookies.set('token', token);
cookies.set("token", token);
return Object.assign({}, state, {
return state.merge(Map({
userLoading: false,
isLogged: true,
isAdmin: decodedToken.isAdmin,
username: decodedToken.username,
})
}))
}
export default (state = defaultState, action) =>
handlers[action.type] ? handlers[action.type](state, action) : state;

View File

@ -1,12 +1,12 @@
import axios from 'axios'
import axios from "axios"
// This functions returns an axios instance, the token is added to the
// configuration if found in the localStorage
export function configureAxios(headers = {}) {
// Get the token from the localStorate
const token = localStorage.getItem('token');
const token = localStorage.getItem("token");
if (token) {
headers = { 'Authorization': `Bearer ${token}` };
headers = { "Authorization": `Bearer ${token}` };
}
return axios.create({
@ -30,15 +30,16 @@ export function request(eventPrefix, promise, callbackEvents = null, mainPayload
})
promise
.then(response => {
if (response.data.status === 'error')
if (response.data.status === "error")
{
dispatch({
type: 'ADD_ALERT_ERROR',
type: "ADD_ALERT_ERROR",
payload: {
message: response.data.message,
main: mainPayload,
}
})
});
return;
}
dispatch({
type: fulfilled,
@ -57,11 +58,11 @@ export function request(eventPrefix, promise, callbackEvents = null, mainPayload
// Unauthorized
if (error.response && error.response.status == 401) {
dispatch({
type: 'USER_LOGOUT',
type: "USER_LOGOUT",
})
}
dispatch({
type: 'ADD_ALERT_ERROR',
type: "ADD_ALERT_ERROR",
payload: {
message: error.response.data,
main: mainPayload,

View File

@ -1,31 +1,29 @@
import React from 'react'
import MovieList from "./components/movies/list"
import ShowList from "./components/shows/list"
import ShowDetails from "./components/shows/details"
import UserLoginForm from "./components/users/login"
import UserEdit from "./components/users/edit"
import UserSignUp from "./components/users/signup"
import TorrentList from "./components/torrents/list"
import MovieList from './components/movies/list'
import ShowList from './components/shows/list'
import ShowDetails from './components/shows/details'
import UserLoginForm from './components/users/login'
import UserEdit from './components/users/edit'
import UserSignUp from './components/users/signup'
import TorrentList from './components/torrents/list'
import { fetchTorrents } from "./actions/torrents"
import { userLogout, getUserInfos } from "./actions/users"
import { fetchMovies, getMovieExploreOptions } from "./actions/movies"
import { fetchShows, fetchShowDetails, getShowExploreOptions } from "./actions/shows"
import { fetchTorrents } from './actions/torrents'
import { userLogout, getUserInfos } from './actions/users'
import { fetchMovies, getMovieExploreOptions } from './actions/movies'
import { fetchShows, fetchShowDetails, getShowExploreOptions } from './actions/shows'
import store from './store'
import store from "./store"
// This function returns true if the user is logged in, false otherwise
function isLoggedIn() {
const state = store.getState();
const isLogged = state.userStore.isLogged;
let token = localStorage.getItem('token');
let token = localStorage.getItem("token");
// Let's check if the user has a token, if he does let's assume he's logged
// in. If that's not the case he will be logged out on the fisrt query
if (token && token !== "") {
store.dispatch({
type: 'USER_SET_TOKEN',
type: "USER_SET_TOKEN",
payload: {
token: token,
},
@ -43,7 +41,7 @@ var pollingTorrentsId;
const loginCheck = function(nextState, replace, next, f = null) {
const loggedIn = isLoggedIn();
if (!loggedIn) {
replace('/users/login');
replace("/users/login");
} else {
if (f) {
f();
@ -61,19 +59,19 @@ const loginCheck = function(nextState, replace, next, f = null) {
next();
}
const defaultRoute = '/movies/explore/yts/seeds';
const defaultRoute = "/movies/explore/yts/seeds";
export default function getRoutes(App) {
return {
path: '/',
path: "/",
component: App,
indexRoute: {onEnter: ({params}, replace) => replace(defaultRoute)},
childRoutes: [
{
path: '/users/signup',
path: "/users/signup",
component: UserSignUp
},
{
path: '/users/login',
path: "/users/login",
component: UserLoginForm,
onEnter: function(nextState, replace, next) {
if (isLoggedIn()) {
@ -84,7 +82,7 @@ export default function getRoutes(App) {
},
},
{
path: '/users/edit',
path: "/users/edit",
component: UserEdit,
onEnter: function(nextState, replace, next) {
loginCheck(nextState, replace, next, function() {
@ -93,7 +91,7 @@ export default function getRoutes(App) {
},
},
{
path: '/users/logout',
path: "/users/logout",
onEnter: function(nextState, replace, next) {
// Stop polling
if (pollingTorrentsId !== null) {
@ -101,12 +99,12 @@ export default function getRoutes(App) {
pollingTorrentsId = null;
}
store.dispatch(userLogout());
replace('/users/login');
replace("/users/login");
next();
},
},
{
path: '/movies/search/:search',
path: "/movies/search/:search",
component: MovieList,
onEnter: function(nextState, replace, next) {
loginCheck(nextState, replace, next, function() {
@ -115,22 +113,22 @@ export default function getRoutes(App) {
},
},
{
path: '/movies/polochon',
path: "/movies/polochon",
component: MovieList,
onEnter: function(nextState, replace, next) {
loginCheck(nextState, replace, next, function() {
store.dispatch(fetchMovies('/movies/polochon'));
store.dispatch(fetchMovies("/movies/polochon"));
});
},
},
{
path: '/movies/explore/:source/:category',
path: "/movies/explore/:source/:category",
component: MovieList,
onEnter: function(nextState, replace, next) {
loginCheck(nextState, replace, next, function() {
var state = store.getState();
// Fetch the explore options
if (Object.keys(state.movieStore.exploreOptions).length === 0) {
if (state.movieStore.get("exploreOptions").size === 0) {
store.dispatch(getMovieExploreOptions());
}
store.dispatch(fetchMovies(
@ -140,16 +138,16 @@ export default function getRoutes(App) {
},
},
{
path: '/movies/wishlist',
path: "/movies/wishlist",
component: MovieList,
onEnter: function(nextState, replace, next) {
loginCheck(nextState, replace, next, function() {
store.dispatch(fetchMovies('/wishlist/movies'));
store.dispatch(fetchMovies("/wishlist/movies"));
});
},
},
{
path: '/shows/search/:search',
path: "/shows/search/:search",
component: ShowList,
onEnter: function(nextState, replace, next) {
loginCheck(nextState, replace, next, function() {
@ -158,25 +156,25 @@ export default function getRoutes(App) {
},
},
{
path: '/shows/polochon',
path: "/shows/polochon",
component: ShowList,
onEnter: function(nextState, replace, next) {
loginCheck(nextState, replace, next, function() {
store.dispatch(fetchShows('/shows/polochon'));
store.dispatch(fetchShows("/shows/polochon"));
});
},
},
{
path: '/shows/wishlist',
path: "/shows/wishlist",
component: ShowList,
onEnter: function(nextState, replace, next) {
loginCheck(nextState, replace, next, function() {
store.dispatch(fetchShows('/wishlist/shows'));
store.dispatch(fetchShows("/wishlist/shows"));
});
},
},
{
path: '/shows/details/:imdbId',
path: "/shows/details/:imdbId",
component: ShowDetails,
onEnter: function(nextState, replace, next) {
loginCheck(nextState, replace, next, function() {
@ -185,13 +183,13 @@ export default function getRoutes(App) {
},
},
{
path: '/shows/explore/:source/:category',
path: "/shows/explore/:source/:category",
component: ShowList,
onEnter: function(nextState, replace, next) {
loginCheck(nextState, replace, next, function() {
var state = store.getState();
// Fetch the explore options
if (Object.keys(state.showsStore.exploreOptions).length === 0) {
if (state.showsStore.get("exploreOptions").size === 0) {
store.dispatch(getShowExploreOptions());
}
store.dispatch(fetchShows(
@ -201,7 +199,7 @@ export default function getRoutes(App) {
},
},
{
path: '/torrents',
path: "/torrents",
component: TorrentList,
onEnter: function(nextState, replace, next) {
loginCheck(nextState, replace, next, function() {

View File

@ -1,19 +1,19 @@
import { createStore, applyMiddleware, compose } from 'redux';
import { syncHistoryWithStore } from 'react-router-redux'
import { hashHistory } from 'react-router'
import thunk from 'redux-thunk'
import { routerMiddleware } from 'react-router-redux'
import { createStore, applyMiddleware, compose } from "redux";
import { syncHistoryWithStore } from "react-router-redux"
import { hashHistory } from "react-router"
import thunk from "redux-thunk"
import { routerMiddleware } from "react-router-redux"
// Import the root reducer
import rootReducer from './reducers/index'
import rootReducer from "./reducers/index"
const routingMiddleware = routerMiddleware(hashHistory)
const middlewares = [thunk, routingMiddleware];
// Only use in development mode (set in webpack)
if (process.env.NODE_ENV === `development`) {
const createLogger = require(`redux-logger`);
if (process.env.NODE_ENV === "development") {
const createLogger = require("redux-logger");
const logger = createLogger();
middlewares.push(logger);
}

679
yarn.lock

File diff suppressed because it is too large Load Diff