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

View File

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

View File

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

View File

@ -1,30 +1,28 @@
import { configureAxios, request } from '../requests' import { configureAxios, request } from "../requests"
import { addAlertOk } from './alerts'
export function refreshSubtitles(type, id, season, episode) { export function refreshSubtitles(type, id, season, episode) {
switch (type) { switch (type) {
case 'movie': case "movie":
var resourceURL = `/movies/${id}` var resourceURL = `/movies/${id}`
return request( return request(
'MOVIE_SUBTITLES_UPDATE', "MOVIE_SUBTITLES_UPDATE",
configureAxios().post(`${resourceURL}/subtitles/refresh`), configureAxios().post(`${resourceURL}/subtitles/refresh`),
null, null,
{ imdb_id: id }, { imdbId: id },
) )
case 'episode': case "episode":
var resourceURL = `/shows/${id}/seasons/${season}/episodes/${episode}` var resourceURL = `/shows/${id}/seasons/${season}/episodes/${episode}`
return request( return request(
'EPISODE_SUBTITLES_UPDATE', "EPISODE_SUBTITLES_UPDATE",
configureAxios().post(`${resourceURL}/subtitles/refresh`), configureAxios().post(`${resourceURL}/subtitles/refresh`),
null, null,
{ {
imdb_id: id, imdbId: id,
season: season, season: season,
episode: episode, episode: episode,
}, },
) )
default: 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) { export function addTorrent(url) {
return request( return request(
'ADD_TORRENT', "ADD_TORRENT",
configureAxios().post('/torrents', { configureAxios().post("/torrents", {
url: url, url: url,
}), }),
[ [
@ -16,7 +16,7 @@ export function addTorrent(url) {
export function fetchTorrents() { export function fetchTorrents() {
return request( return request(
'TORRENTS_FETCH', "TORRENTS_FETCH",
configureAxios().get('/torrents') 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() { export function userLogout() {
return { return {
type: 'USER_LOGOUT', type: "USER_LOGOUT",
} }
} }
export function loginUser(username, password) { export function loginUser(username, password) {
return request( return request(
'USER_LOGIN', "USER_LOGIN",
configureAxios().post( configureAxios().post(
'/users/login', "/users/login",
{ {
username: username, username: username,
password: password, password: password,
@ -23,8 +23,8 @@ export function loginUser(username, password) {
export function updateUser(config) { export function updateUser(config) {
return request( return request(
'USER_UPDATE', "USER_UPDATE",
configureAxios().post('/users/edit', config), configureAxios().post("/users/edit", config),
[ [
addAlertOk("User updated"), addAlertOk("User updated"),
], ],
@ -33,14 +33,14 @@ export function updateUser(config) {
export function userSignUp(config) { export function userSignUp(config) {
return request( return request(
'USER_SIGNUP', "USER_SIGNUP",
configureAxios().post('/users/signup', config) configureAxios().post("/users/signup", config)
) )
} }
export function getUserInfos() { export function getUserInfos() {
return request( return request(
'GET_USER', "GET_USER",
configureAxios().get('/users/details') configureAxios().get("/users/details")
) )
} }

View File

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

View File

@ -1,17 +1,16 @@
import React from 'react' import React from "react"
import SweetAlert from "react-bootstrap-sweetalert";
import SweetAlert from 'react-bootstrap-sweetalert';
export default function Alert(props) { export default function Alert(props) {
if (!props.alerts.show) { if (!props.alerts.get("show")) {
return null return null
} }
return ( return (
<SweetAlert <SweetAlert
type={props.alerts.type} type={props.alerts.get("type")}
title={props.alerts.get("message")}
onConfirm={props.dismissAlert} 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) { constructor(props) {
super(props); super(props);
this.handleClick = this.handleClick.bind(this); 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) { constructor(props) {
super(props); super(props);
this.handleClick = this.handleClick.bind(this); 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) { constructor(props) {
super(props); super(props);
this.handleClick = this.handleClick.bind(this); this.handleClick = this.handleClick.bind(this);
@ -71,16 +73,7 @@ export class RefreshButton extends React.Component {
render() { render() {
return ( return (
<MenuItem onClick={this.handleClick}> <MenuItem onClick={this.handleClick}>
{this.props.fetching || <RefreshIndicator refresh={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>
}
</MenuItem> </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) { constructor(props) {
super(props); super(props);
this.showModal = this.showModal.bind(this); 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) { constructor(props) {
super(props); super(props);
var subtitles = [];
if (props.subtitles && props.subtitles.length) {
subtitles = props.subtitles;
}
this.state = {
subtitles: subtitles,
};
} }
render() { render() {
const subtitles = this.props.subtitles;
const hasSubtitles = !(subtitles === undefined || subtitles === null || subtitles.size === 0);
return ( return (
<div className="embed-responsive embed-responsive-16by9"> <div className="embed-responsive embed-responsive-16by9">
<video controls> <video controls>
<source src={this.props.url} type="video/mp4"/> <source src={this.props.url} type="video/mp4"/>
{this.props.subtitles.map(function(el, index) { {hasSubtitles && subtitles.toIndexedSeq().map(function(el, index) {
return ( return (
<track <track
key={index} key={index}
kind="subtitles" kind="subtitles"
label={el.language} label={el.get("language")}
src={el.vvt_file} src={el.get("vvt_file")}
srcLang={el.language} 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) { constructor(props) {
super(props); super(props);
this.handleClick = this.handleClick.bind(this); this.handleClick = this.handleClick.bind(this);
} }
handleClick(e, url) { handleClick(e) {
e.preventDefault(); e.preventDefault();
if (this.props.fetching) { if (this.props.fetching) {
return return
} }
// Refresh the subtitles this.props.refreshSubtitles(this.props.type, this.props.resourceID,
this.props.refreshSubtitles(this.props.type, this.props.resourceID, this.props.season, this.props.episode); this.props.season, this.props.episode);
} }
render() { 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 ( return (
<DropdownButton bsStyle="success" bsSize={btnSize} title="Subtitles" id="download-subtitles-button" dropup> <MenuItem onClick={this.handleClick}>
{entries.map(function(e, index) { <RefreshIndicator refresh={this.props.fetching} />
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> </MenuItem>
); );
} }
}, this)}
</DropdownButton>
);
}
}
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) { 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 ( return (
<div className="col-xs-7 col-md-4"> <div className="col-xs-7 col-md-4">
<div className="affix"> <div className="affix">
<h1 className="hidden-xs">{props.data.title}</h1> <h1 className="hidden-xs">{props.data.get("title")}</h1>
<h3 className="visible-xs">{props.data.title}</h3> <h3 className="visible-xs">{props.data.get("title")}</h3>
{wishlistStr !== "" && <TrackingLabel
<span className="label label-default"> wishlisted={props.data.get("wishlisted")}
<i className="fa fa-bookmark"></i> {wishlistStr} trackedSeason={props.data.get("tracked_season")}
</span> trackedEpisode={props.data.get("tracked_episode")}
} />
<h4>{props.data.year}</h4> <h4>{props.data.get("year")}</h4>
{props.data.runtime && <Runtime runtime={props.data.get("runtime")} />
<p> <Genres genres={props.data.get("genres")} />
<i className="fa fa-clock-o"></i> <Ratings
&nbsp;{props.data.runtime} min rating={props.data.get("rating")}
</p> votes={props.data.get("votes")}
} />
{props.data.genres && <p className="plot">{props.data.get("plot")}</p>
<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>
</div> </div>
{props.children} {props.children}
</div> </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 React from "react"
import { Form, FormGroup, FormControl, ControlLabel } from 'react-bootstrap' import { Form, FormGroup, FormControl, ControlLabel } from "react-bootstrap"
export default class ExplorerOptions extends React.Component { export default class ExplorerOptions extends React.PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.handleSourceChange = this.handleSourceChange.bind(this); this.handleSourceChange = this.handleSourceChange.bind(this);
@ -9,7 +9,7 @@ export default class ExplorerOptions extends React.Component {
} }
handleSourceChange(event) { handleSourceChange(event) {
let source = event.target.value; 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}`); this.props.router.push(`/${this.props.type}/explore/${source}/${category}`);
} }
handleCategoryChange(event) { handleCategoryChange(event) {
@ -40,7 +40,7 @@ export default class ExplorerOptions extends React.Component {
} }
// Options are not yet fetched // Options are not yet fetched
if (Object.keys(this.props.options).length === 0) { if (this.props.options.size === 0) {
return null; return null;
} }
@ -51,7 +51,7 @@ export default class ExplorerOptions extends React.Component {
let source = this.props.params.source; let source = this.props.params.source;
let category = this.props.params.category; 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 ( return (
<div className="row"> <div className="row">
@ -67,8 +67,9 @@ export default class ExplorerOptions extends React.Component {
onChange={this.handleSourceChange} onChange={this.handleSourceChange}
value={source} value={source}
> >
{Object.keys(this.props.options).map(function(source) { {this.props.options.keySeq().map(function(source) {
return (<option key={source} value={source}>{this.prettyName(source)}</option>) return (
<option key={source} value={source}>{this.prettyName(source)}</option>)
}, this)} }, this)}
</FormControl> </FormControl>
</FormGroup> </FormGroup>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,112 +1,112 @@
import React from 'react' import React from "react"
import { connect } from 'react-redux' import { connect } from "react-redux"
import { bindActionCreators } from 'redux' import { bindActionCreators } from "redux"
import { addTorrent } from '../../actions/torrents' import { addTorrent } from "../../actions/torrents"
import { refreshSubtitles } from '../../actions/subtitles' import { refreshSubtitles } from "../../actions/subtitles"
import { addMovieToWishlist, deleteMovieFromWishlist, import { addMovieToWishlist, deleteMovie, deleteMovieFromWishlist,
getMovieDetails, selectMovie } from '../../actions/movies' getMovieDetails, selectMovie, updateFilter } from "../../actions/movies"
import DownloadButton from '../buttons/download' import DownloadButton from "../buttons/download"
import SubtitlesButton from '../buttons/subtitles' import SubtitlesButton from "../buttons/subtitles"
import TorrentsButton from './torrents' import ImdbButton from "../buttons/imdb"
import ActionsButton from './actions' import TorrentsButton from "./torrents"
import ListPosters from '../list/posters' import ActionsButton from "./actions"
import ListDetails from '../list/details' import ListPosters from "../list/posters"
import ListDetails from "../list/details"
function mapStateToProps(state) { 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) => const mapDispatchToProps = (dipatch) =>
bindActionCreators({ selectMovie, getMovieDetails, addTorrent, bindActionCreators({ selectMovie, getMovieDetails, addTorrent,
addMovieToWishlist, deleteMovieFromWishlist, refreshSubtitles }, dipatch) addMovieToWishlist, deleteMovie, deleteMovieFromWishlist,
refreshSubtitles, updateFilter }, dipatch)
function MovieButtons(props) { function MovieButtons(props) {
const imdb_link = `http://www.imdb.com/title/${props.movie.imdb_id}`; const hasMovie = (props.movie.get("polochon_url") !== "");
const hasMovie = (props.movie.polochon_url !== "");
return ( return (
<div className="list-details-buttons btn-toolbar"> <div className="list-details-buttons btn-toolbar">
<ActionsButton <ActionsButton
fetching={props.fetching} fetching={props.movie.get("fetchingDetails")}
movieId={props.movie.imdb_id} movieId={props.movie.get("imdb_id")}
getDetails={props.getMovieDetails} getDetails={props.getMovieDetails}
deleteMovie={props.deleteMovie} deleteMovie={props.deleteMovie}
hasMovie={hasMovie} hasMovie={hasMovie}
wishlisted={props.movie.wishlisted} wishlisted={props.movie.get("wishlisted")}
addToWishlist={props.addToWishlist} addToWishlist={props.addToWishlist}
deleteFromWishlist={props.deleteFromWishlist} deleteFromWishlist={props.deleteFromWishlist}
lastFetchUrl={props.lastFetchUrl} lastFetchUrl={props.lastFetchUrl}
fetchMovies={props.fetchMovies}
/> />
{props.movie.torrents && {props.movie.get("torrents") !== null &&
<TorrentsButton <TorrentsButton
torrents={props.movie.torrents} torrents={props.movie.get("torrents")}
addTorrent={props.addTorrent} addTorrent={props.addTorrent}
/> />
} }
<DownloadButton <DownloadButton
url={props.movie.polochon_url} url={props.movie.get("polochon_url")}
subtitles={props.movie.subtitles} subtitles={props.movie.get("subtitles")}
/> />
{props.movie.get("polochon_url") !== "" &&
<SubtitlesButton <SubtitlesButton
url={props.movie.polochon_url} fetching={props.movie.get("fetchingSubtitles")}
subtitles={props.movie.subtitles} subtitles={props.movie.get("subtitles")}
refreshSubtitles={props.refreshSubtitles} refreshSubtitles={props.refreshSubtitles}
resourceID={props.movie.imdb_id} resourceID={props.movie.get("imdb_id")}
data={props.movie}
type="movie" type="movie"
/> />
}
<a type="button" className="btn btn-warning btn-sm" href={imdb_link}> <ImdbButton imdbId={props.movie.get("imdb_id")} size="sm"/>
<i className="fa fa-external-link"></i> IMDB
</a>
</div> </div>
); );
} }
class MovieList extends React.Component { class MovieList extends React.PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
} }
render() { render() {
const movies = this.props.movieStore.movies; let selectedMovie = undefined;
const selectedMovieId = this.props.movieStore.selectedImdbId; if (this.props.movies !== undefined && this.props.movies.has(this.props.selectedImdbId)) {
let index = movies.map((el) => el.imdb_id).indexOf(selectedMovieId); selectedMovie = this.props.movies.get(this.props.selectedImdbId);
if (index === -1) {
index = 0;
} }
const selectedMovie = movies[index];
return ( return (
<div className="row" id="container"> <div className="row" id="container">
<ListPosters <ListPosters
data={movies} data={this.props.movies}
type="movies" type="movies"
formModel="movieStore" placeHolder="Filter movies..."
filterControlModel="movieStore.filter" exploreOptions={this.props.exploreOptions}
filterControlPlaceHolder="Filter movies..." selectedImdbId={this.props.selectedImdbId}
exploreOptions={this.props.movieStore.exploreOptions} updateFilter={this.props.updateFilter}
selectedImdbId={selectedMovieId} filter={this.props.filter}
filter={this.props.movieStore.filter}
perPage={this.props.movieStore.perPage}
onClick={this.props.selectMovie} onClick={this.props.selectMovie}
params={this.props.params} params={this.props.params}
router={this.props.router} router={this.props.router}
loading={this.props.movieStore.loading} loading={this.props.loading}
/> />
{selectedMovie && {selectedMovie !== undefined &&
<ListDetails data={selectedMovie}> <ListDetails data={selectedMovie}>
<MovieButtons <MovieButtons
movie={selectedMovie} movie={selectedMovie}
fetching={this.props.movieStore.fetchingDetails}
getMovieDetails={this.props.getMovieDetails} getMovieDetails={this.props.getMovieDetails}
addTorrent={this.props.addTorrent} addTorrent={this.props.addTorrent}
deleteMovie={this.props.deleteMovie} deleteMovie={this.props.deleteMovie}
addToWishlist={this.props.addMovieToWishlist} addToWishlist={this.props.addMovieToWishlist}
deleteFromWishlist={this.props.deleteMovieFromWishlist} deleteFromWishlist={this.props.deleteMovieFromWishlist}
lastFetchUrl={this.props.movieStore.lastFetchUrl} lastFetchUrl={this.props.lastFetchUrl}
refreshSubtitles={this.props.refreshSubtitles} refreshSubtitles={this.props.refreshSubtitles}
/> />
</ListDetails> </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) { constructor(props) {
super(props); super(props);
this.handleClick = this.handleClick.bind(this); 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> <DropdownButton className="btn btn-default btn-sm" title="Torrents" id="download-torrents-button" dropup>
{entries.map(function(e, index) { {entries.map(function(e, index) {
switch (e.type) { switch (e.type) {
case 'header': case "header":
return ( return (
<MenuItem key={index} className="text-warning" header> <MenuItem key={index} className="text-warning" header>
{e.value} {e.value}
</MenuItem> </MenuItem>
); );
case 'divider': case "divider":
return ( return (
<MenuItem key={index} divider></MenuItem> <MenuItem key={index} divider></MenuItem>
); );
case 'entry': case "entry":
return ( return (
<MenuItem key={index} href={e.url} onClick={(event) => this.handleClick(event, e.url)}> <MenuItem key={index} href={e.url} onClick={(event) => this.handleClick(event, e.url)}>
{e.quality} {e.quality}
@ -41,20 +41,12 @@ export default class TorrentsButton extends React.Component {
} }
function buildMenuItems(torrents) { function buildMenuItems(torrents) {
// Organise by source const t = torrents.groupBy((el) => el.get("source"));
let sources = {}
for (let torrent of torrents) {
if (!sources[torrent.source]) {
sources[torrent.source] = [];
}
sources[torrent.source].push(torrent);
}
// Build the array of entries // Build the array of entries
let entries = []; let entries = [];
let sourceNames = Object.keys(sources); let dividerCount = t.size - 1;
let dividerCount = sourceNames.length - 1; for (let [source, torrentList] of t.entrySeq()) {
for (let source of sourceNames) {
// Push the title // Push the title
entries.push({ entries.push({
type: "header", type: "header",
@ -62,11 +54,11 @@ function buildMenuItems(torrents) {
}); });
// Push the torrents // Push the torrents
for (let torrent of sources[source]) { for (let torrent of torrentList) {
entries.push({ entries.push({
type: "entry", type: "entry",
quality: torrent.quality, quality: torrent.get("quality"),
url: torrent.url, url: torrent.get("url"),
}); });
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,30 +1,29 @@
const defaultState = { import { Map } from "immutable"
const defaultState = Map({
show: false, show: false,
message: "", message: "",
type: "", type: "",
}; });
export default function Alert(state = defaultState, action) {
switch (action.type) { const handlers = {
case 'ADD_ALERT_ERROR': "ADD_ALERT_ERROR": (state, action) => state.merge(Map({
return Object.assign({}, state, {
message: action.payload.message, message: action.payload.message,
show: true, show: true,
type: "error", type: "error",
}) })),
case 'ADD_ALERT_OK': "ADD_ALERT_OK": (state, action) => state.merge(Map({
return Object.assign({}, state, {
message: action.payload.message, message: action.payload.message,
show: true, show: true,
type: "success", type: "success",
}) })),
case 'DISMISS_ALERT': "DISMISS_ALERT": state => state.merge(Map({
return Object.assign({}, state, {
message: "", message: "",
show: false, show: false,
type: "", 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 { combineReducers } from "redux";
import { routerReducer } from 'react-router-redux' import { routerReducer } from "react-router-redux"
import movieStore from './movies' import movieStore from "./movies"
import showsStore from './shows' import showsStore from "./shows"
import showStore from './show' import showStore from "./show"
import userStore from './users' import userStore from "./users"
import alerts from './alerts' import alerts from "./alerts"
import torrentStore from './torrents' import torrentStore from "./torrents"
// Use combine form form react-redux-form, it's a thin wrapper arround the const rootReducer = combineReducers({
// default combinedReducers provided with React. It allows the forms to be
// linked directly to the store.
const rootReducer = combineForms({
routing: routerReducer, routing: routerReducer,
movieStore, movieStore,
showsStore, showsStore,

View File

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

View File

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

View File

@ -1,87 +1,50 @@
const defaultState = { import { OrderedMap, Map, fromJS } from "immutable"
const defaultState = Map({
loading: false, loading: false,
shows: [], shows: OrderedMap(),
filter: "", filter: "",
perPage: 30,
selectedImdbId: "", selectedImdbId: "",
getDetails: false, lastFetchUrl: "",
lastShowsFetchUrl: "", exploreOptions: Map(),
exploreOptions: {}, });
};
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));
});
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 = ""; let selectedImdbId = "";
// Select the first show if (shows.size > 0) {
console.log("Hey", action.payload); // Sort by year
if (action.payload.response.data.length > 0) { shows = shows.sort((a,b) => b.get("year") - a.get("year"));
selectedImdbId = action.payload.response.data[0].imdb_id; selectedImdbId = shows.first().get("imdb_id");
} }
return Object.assign({}, state, {
shows: action.payload.response.data, return state.delete("shows").merge(Map({
shows: shows,
filter: "",
loading: false,
selectedImdbId: selectedImdbId, selectedImdbId: selectedImdbId,
filter: defaultState.filter, }));
loading: false, },
}) "SHOW_GET_DETAILS_PENDING": (state, action) => state.setIn(["shows", action.payload.main.imdbId, "fetchingDetails"], true),
case 'SHOW_GET_DETAILS_PENDING': "SHOW_GET_DETAILS_FULFILLED": (state, action) => {
return Object.assign({}, state, { let show = action.payload.response.data;
getDetails: true, show.fetchingDetails = false;
}) show.fetchingSubtitles = false;
case 'SHOW_GET_DETAILS_FULFILLED': return state.setIn(["shows", show.imdb_id], fromJS(show));
return Object.assign({}, state, { },
shows: updateShowDetails(state.shows.slice(), action.payload.response.data), "SHOW_GET_EXPLORE_OPTIONS_FULFILLED": (state, action) => state.set("exploreOptions", fromJS(action.payload.response.data)),
getDetails: false, "SHOW_UPDATE_STORE_WISHLIST": (state, action) => {
}) let season = action.payload.season;
case 'EXPLORE_SHOWS_PENDING': let episode = action.payload.episode;
return Object.assign({}, state, { if (action.payload.wishlisted) {
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
}
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) { if (season === null) {
season = 0; season = 0;
} }
@ -89,13 +52,16 @@ function updateShowsStoreWishlist(shows, payload) {
episode = 0; episode = 0;
} }
} }
shows[index].tracked_season = season;
shows[index].tracked_episode = episode; return state.mergeIn(["shows", action.payload.imdbId], Map({
return shows "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) { export default (state = defaultState, action) =>
let index = shows.map((el) => el.imdb_id).indexOf(data.imdb_id); handlers[action.type] ? handlers[action.type](state, action) : state;
shows[index] = data;
return shows
}

View File

@ -1,20 +1,17 @@
import { Map, List, fromJS } from 'immutable' import { Map, List, fromJS } from "immutable"
const defaultState = Map({ const defaultState = Map({
'fetching': false, "fetching": false,
'torrents': List(), "torrents": List(),
}); });
export default function torrentStore(state = defaultState, action) { const handlers = {
switch (action.type) { "TORRENTS_FETCH_PENDING": state => state.set("fetching", false),
case 'TORRENTS_FETCH_PENDING': "TORRENTS_FETCH_FULFILLED": (state, action) => state.merge(fromJS({
return state.set('fetching', false);
case 'TORRENTS_FETCH_FULFILLED':
return state.merge(fromJS({
fetching: false, fetching: false,
torrents: action.payload.response.data, torrents: action.payload.response.data,
})); })),
default:
return state
}
} }
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 { Map } from "immutable"
import Cookies from 'universal-cookie'
const defaultState = { import jwtDecode from "jwt-decode"
userLoading: false, import Cookies from "universal-cookie"
const defaultState = Map({
loading: false,
username: "", username: "",
isAdmin: false, isAdmin: false,
isLogged: false, isLogged: false,
polochonToken: "", polochonToken: "",
polochonUrl: "", polochonUrl: "",
}; });
export default function userStore(state = defaultState, action) { const handlers = {
switch (action.type) { "USER_LOGIN_PENDING": state => state.set("loading", true),
case 'USER_LOGIN_PENDING': "USER_LOGIN_FULFILLED": (state, action) => {
return Object.assign({}, state, {
userLoading: true,
})
case 'USER_LOGIN_FULFILLED':
if (action.payload.response.status === "error") { if (action.payload.response.status === "error") {
return logoutUser(state) return logoutUser()
} }
return updateFromToken(state, action.payload.response.data.token) return updateFromToken(state, action.payload.response.data.token)
case 'USER_SET_TOKEN': },
return updateFromToken(state, action.payload.token) "USER_SET_TOKEN": (state, action) => updateFromToken(state, action.payload.token),
case 'USER_LOGOUT': "USER_LOGOUT": () => logoutUser(),
return logoutUser(state) "GET_USER_FULFILLED": (state, action) => state.merge(Map({
case 'GET_USER_FULFILLED':
return Object.assign({}, state, {
polochonToken: action.payload.response.data.token, polochonToken: action.payload.response.data.token,
polochonUrl: action.payload.response.data.url, polochonUrl: action.payload.response.data.url,
}) })),
default:
return state;
}
} }
function logoutUser(state) { function logoutUser() {
localStorage.removeItem('token'); localStorage.removeItem("token");
const cookies = new Cookies(); const cookies = new Cookies();
cookies.remove('token'); cookies.remove("token");
return defaultState
return Object.assign({}, state, defaultState)
} }
function updateFromToken(state, token) { function updateFromToken(state, token) {
const decodedToken = jwtDecode(token); const decodedToken = jwtDecode(token);
localStorage.setItem('token', token); localStorage.setItem("token", token);
const cookies = new Cookies(); const cookies = new Cookies();
cookies.set('token', token); cookies.set("token", token);
return Object.assign({}, state, { return state.merge(Map({
userLoading: false, userLoading: false,
isLogged: true, isLogged: true,
isAdmin: decodedToken.isAdmin, isAdmin: decodedToken.isAdmin,
username: decodedToken.username, 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 // This functions returns an axios instance, the token is added to the
// configuration if found in the localStorage // configuration if found in the localStorage
export function configureAxios(headers = {}) { export function configureAxios(headers = {}) {
// Get the token from the localStorate // Get the token from the localStorate
const token = localStorage.getItem('token'); const token = localStorage.getItem("token");
if (token) { if (token) {
headers = { 'Authorization': `Bearer ${token}` }; headers = { "Authorization": `Bearer ${token}` };
} }
return axios.create({ return axios.create({
@ -30,15 +30,16 @@ export function request(eventPrefix, promise, callbackEvents = null, mainPayload
}) })
promise promise
.then(response => { .then(response => {
if (response.data.status === 'error') if (response.data.status === "error")
{ {
dispatch({ dispatch({
type: 'ADD_ALERT_ERROR', type: "ADD_ALERT_ERROR",
payload: { payload: {
message: response.data.message, message: response.data.message,
main: mainPayload, main: mainPayload,
} }
}) });
return;
} }
dispatch({ dispatch({
type: fulfilled, type: fulfilled,
@ -57,11 +58,11 @@ export function request(eventPrefix, promise, callbackEvents = null, mainPayload
// Unauthorized // Unauthorized
if (error.response && error.response.status == 401) { if (error.response && error.response.status == 401) {
dispatch({ dispatch({
type: 'USER_LOGOUT', type: "USER_LOGOUT",
}) })
} }
dispatch({ dispatch({
type: 'ADD_ALERT_ERROR', type: "ADD_ALERT_ERROR",
payload: { payload: {
message: error.response.data, message: error.response.data,
main: mainPayload, 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 { fetchTorrents } from "./actions/torrents"
import ShowList from './components/shows/list' import { userLogout, getUserInfos } from "./actions/users"
import ShowDetails from './components/shows/details' import { fetchMovies, getMovieExploreOptions } from "./actions/movies"
import UserLoginForm from './components/users/login' import { fetchShows, fetchShowDetails, getShowExploreOptions } from "./actions/shows"
import UserEdit from './components/users/edit'
import UserSignUp from './components/users/signup'
import TorrentList from './components/torrents/list'
import { fetchTorrents } from './actions/torrents' import store from "./store"
import { userLogout, getUserInfos } from './actions/users'
import { fetchMovies, getMovieExploreOptions } from './actions/movies'
import { fetchShows, fetchShowDetails, getShowExploreOptions } from './actions/shows'
import store from './store'
// This function returns true if the user is logged in, false otherwise // This function returns true if the user is logged in, false otherwise
function isLoggedIn() { function isLoggedIn() {
const state = store.getState(); const state = store.getState();
const isLogged = state.userStore.isLogged; 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 // 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 // in. If that's not the case he will be logged out on the fisrt query
if (token && token !== "") { if (token && token !== "") {
store.dispatch({ store.dispatch({
type: 'USER_SET_TOKEN', type: "USER_SET_TOKEN",
payload: { payload: {
token: token, token: token,
}, },
@ -43,7 +41,7 @@ var pollingTorrentsId;
const loginCheck = function(nextState, replace, next, f = null) { const loginCheck = function(nextState, replace, next, f = null) {
const loggedIn = isLoggedIn(); const loggedIn = isLoggedIn();
if (!loggedIn) { if (!loggedIn) {
replace('/users/login'); replace("/users/login");
} else { } else {
if (f) { if (f) {
f(); f();
@ -61,19 +59,19 @@ const loginCheck = function(nextState, replace, next, f = null) {
next(); next();
} }
const defaultRoute = '/movies/explore/yts/seeds'; const defaultRoute = "/movies/explore/yts/seeds";
export default function getRoutes(App) { export default function getRoutes(App) {
return { return {
path: '/', path: "/",
component: App, component: App,
indexRoute: {onEnter: ({params}, replace) => replace(defaultRoute)}, indexRoute: {onEnter: ({params}, replace) => replace(defaultRoute)},
childRoutes: [ childRoutes: [
{ {
path: '/users/signup', path: "/users/signup",
component: UserSignUp component: UserSignUp
}, },
{ {
path: '/users/login', path: "/users/login",
component: UserLoginForm, component: UserLoginForm,
onEnter: function(nextState, replace, next) { onEnter: function(nextState, replace, next) {
if (isLoggedIn()) { if (isLoggedIn()) {
@ -84,7 +82,7 @@ export default function getRoutes(App) {
}, },
}, },
{ {
path: '/users/edit', path: "/users/edit",
component: UserEdit, component: UserEdit,
onEnter: function(nextState, replace, next) { onEnter: function(nextState, replace, next) {
loginCheck(nextState, replace, next, function() { 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) { onEnter: function(nextState, replace, next) {
// Stop polling // Stop polling
if (pollingTorrentsId !== null) { if (pollingTorrentsId !== null) {
@ -101,12 +99,12 @@ export default function getRoutes(App) {
pollingTorrentsId = null; pollingTorrentsId = null;
} }
store.dispatch(userLogout()); store.dispatch(userLogout());
replace('/users/login'); replace("/users/login");
next(); next();
}, },
}, },
{ {
path: '/movies/search/:search', path: "/movies/search/:search",
component: MovieList, component: MovieList,
onEnter: function(nextState, replace, next) { onEnter: function(nextState, replace, next) {
loginCheck(nextState, replace, next, function() { loginCheck(nextState, replace, next, function() {
@ -115,22 +113,22 @@ export default function getRoutes(App) {
}, },
}, },
{ {
path: '/movies/polochon', path: "/movies/polochon",
component: MovieList, component: MovieList,
onEnter: function(nextState, replace, next) { onEnter: function(nextState, replace, next) {
loginCheck(nextState, replace, next, function() { 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, component: MovieList,
onEnter: function(nextState, replace, next) { onEnter: function(nextState, replace, next) {
loginCheck(nextState, replace, next, function() { loginCheck(nextState, replace, next, function() {
var state = store.getState(); var state = store.getState();
// Fetch the explore options // Fetch the explore options
if (Object.keys(state.movieStore.exploreOptions).length === 0) { if (state.movieStore.get("exploreOptions").size === 0) {
store.dispatch(getMovieExploreOptions()); store.dispatch(getMovieExploreOptions());
} }
store.dispatch(fetchMovies( store.dispatch(fetchMovies(
@ -140,16 +138,16 @@ export default function getRoutes(App) {
}, },
}, },
{ {
path: '/movies/wishlist', path: "/movies/wishlist",
component: MovieList, component: MovieList,
onEnter: function(nextState, replace, next) { onEnter: function(nextState, replace, next) {
loginCheck(nextState, replace, next, function() { 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, component: ShowList,
onEnter: function(nextState, replace, next) { onEnter: function(nextState, replace, next) {
loginCheck(nextState, replace, next, function() { loginCheck(nextState, replace, next, function() {
@ -158,25 +156,25 @@ export default function getRoutes(App) {
}, },
}, },
{ {
path: '/shows/polochon', path: "/shows/polochon",
component: ShowList, component: ShowList,
onEnter: function(nextState, replace, next) { onEnter: function(nextState, replace, next) {
loginCheck(nextState, replace, next, function() { loginCheck(nextState, replace, next, function() {
store.dispatch(fetchShows('/shows/polochon')); store.dispatch(fetchShows("/shows/polochon"));
}); });
}, },
}, },
{ {
path: '/shows/wishlist', path: "/shows/wishlist",
component: ShowList, component: ShowList,
onEnter: function(nextState, replace, next) { onEnter: function(nextState, replace, next) {
loginCheck(nextState, replace, next, function() { 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, component: ShowDetails,
onEnter: function(nextState, replace, next) { onEnter: function(nextState, replace, next) {
loginCheck(nextState, replace, next, function() { 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, component: ShowList,
onEnter: function(nextState, replace, next) { onEnter: function(nextState, replace, next) {
loginCheck(nextState, replace, next, function() { loginCheck(nextState, replace, next, function() {
var state = store.getState(); var state = store.getState();
// Fetch the explore options // Fetch the explore options
if (Object.keys(state.showsStore.exploreOptions).length === 0) { if (state.showsStore.get("exploreOptions").size === 0) {
store.dispatch(getShowExploreOptions()); store.dispatch(getShowExploreOptions());
} }
store.dispatch(fetchShows( store.dispatch(fetchShows(
@ -201,7 +199,7 @@ export default function getRoutes(App) {
}, },
}, },
{ {
path: '/torrents', path: "/torrents",
component: TorrentList, component: TorrentList,
onEnter: function(nextState, replace, next) { onEnter: function(nextState, replace, next) {
loginCheck(nextState, replace, next, function() { loginCheck(nextState, replace, next, function() {

View File

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

679
yarn.lock

File diff suppressed because it is too large Load Diff