Update the movie store to be immutable

This commit is contained in:
Grégoire Delattre 2017-06-02 13:49:31 +02:00
parent 697997c0ae
commit 80db4383a3
15 changed files with 277 additions and 284 deletions

View File

@ -14,7 +14,18 @@ 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,
},
} }
} }
@ -28,7 +39,11 @@ export function getMovieExploreOptions() {
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,
}
) )
} }
@ -48,7 +63,6 @@ export function addMovieToWishlist(imdbId) {
'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),
], ],
) )
@ -59,7 +73,6 @@ export function deleteMovieFromWishlist(imdbId) {
'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),
], ],
) )

View File

@ -10,7 +10,7 @@ export function refreshSubtitles(type, id, season, episode) {
'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}`
@ -19,12 +19,12 @@ export function refreshSubtitles(type, id, season, episode) {
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

@ -42,7 +42,7 @@ 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 {

View File

@ -1,4 +1,5 @@
import React from 'react' import React from 'react'
import { toJS } from 'immutable'
import { Button, Dropdown, MenuItem, Modal } from 'react-bootstrap' import { Button, Dropdown, MenuItem, Modal } from 'react-bootstrap'
@ -69,20 +70,13 @@ export default class DownloadButton extends React.Component {
class Player extends React.Component { class Player extends React.Component {
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() {
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) { {this.props.subtitles !== undefined && subtitles.toKeyedSeq().map(function(el, index) {
return ( return (
<track <track
key={index} key={index}

View File

@ -9,9 +9,8 @@ export default class SubtitlesButton extends React.Component {
} }
handleClick(e, url) { handleClick(e, url) {
e.preventDefault(); e.preventDefault();
if (this.props.fetching) { if (this.props.fetching) { return }
return
}
// Refresh the subtitles // Refresh the subtitles
this.props.refreshSubtitles(this.props.type, this.props.resourceID, this.props.season, this.props.episode); this.props.refreshSubtitles(this.props.type, this.props.resourceID, this.props.season, this.props.episode);
} }
@ -35,16 +34,16 @@ export default class SubtitlesButton extends React.Component {
case 'action': case 'action':
return ( return (
<MenuItem key={index} onClick={(event) => this.handleClick(event, e.url)}> <MenuItem key={index} onClick={(event) => this.handleClick(event, e.url)}>
{this.props.fetchingSubtitles || {this.props.fetching ||
<span> <span>
<i className="fa fa-refresh"> <i className="fa fa-refresh">
</i> Refresh </i> Refresh
</span> </span>
} }
{this.props.fetchingSubtitles && {this.props.fetching &&
<span> <span>
<i className="fa fa-spin fa-refresh"> <i className="fa fa-spin fa-refresh">
</i> Refreshing </i> Refreshing
</span> </span>
} }
</MenuItem> </MenuItem>
@ -77,7 +76,7 @@ function buildMenuItems(subtitles) {
}); });
// If there is no subtitles, stop here // If there is no subtitles, stop here
if (!subtitles) { if (subtitles == undefined) {
return entries; return entries;
} }
@ -88,8 +87,8 @@ function buildMenuItems(subtitles) {
entries.push({ entries.push({
type: "entry", type: "entry",
// Take only the last part of fr_FR // Take only the last part of fr_FR
lang: sub.language.split("_")[1], lang: sub.get('language').split("_")[1],
url: sub.url, url: sub.get('url'),
}); });
} }

View File

@ -1,46 +1,47 @@
import React from 'react' import React from 'react'
export default function ListDetails(props) { export default function ListDetails(props) {
let genres; let genres = props.data.get('genres');
if (props.data.genres) { if (genres !== undefined) {
// Uppercase first genres // Uppercase first genres
genres = props.data.genres.map( genres = genres.toJS().map(
(word) => word[0].toUpperCase() + word.substr(1) (word) => word[0].toUpperCase() + word.substr(1)
).join(', '); ).join(', ');
} }
let wishlistStr = ""; let wishlistStr = "";
if (props.data.wishlisted === true) { if (props.data.get('wishlisted') === true) {
wishlistStr = "Wishlisted"; wishlistStr = "Wishlisted";
} }
if (props.data.tracked_episode !== null && props.data.tracked_season != null) {
let season = props.data.tracked_season; const trackedSeason = props.data.get('tracked_season');
let episode = props.data.tracked_episode; const trackedEpisode = props.data.get('tracked_episode');
if ((season === 0) && (episode === 0)) { if (trackedEpisode !== undefined && trackedSeason !== undefined) {
if ((trackedSeason === 0) && (trackedEpisode === 0)) {
wishlistStr = "Whole show tracked"; wishlistStr = "Whole show tracked";
} else { } else {
wishlistStr = `Tracked from season ${season} episode ${episode}`; wishlistStr = `Tracked from season ${trackedSeason} episode ${trackedEpisode}`;
} }
} }
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 !== "" && {wishlistStr !== "" &&
<span className="label label-default"> <span className="label label-default">
<i className="fa fa-bookmark"></i> {wishlistStr} <i className="fa fa-bookmark"></i> {wishlistStr}
</span> </span>
} }
<h4>{props.data.year}</h4> <h4>{props.data.get('year')}</h4>
{props.data.runtime && {props.data.get('runtime') !== undefined &&
<p> <p>
<i className="fa fa-clock-o"></i> <i className="fa fa-clock-o"></i>
&nbsp;{props.data.runtime} min &nbsp;{props.data.get('runtime')} min
</p> </p>
} }
{props.data.genres && {genres !== undefined &&
<p> <p>
<i className="fa fa-tags"></i> <i className="fa fa-tags"></i>
&nbsp;{genres} &nbsp;{genres}
@ -48,12 +49,12 @@ export default function ListDetails(props) {
} }
<p> <p>
<i className="fa fa-star-o"></i> <i className="fa fa-star-o"></i>
&nbsp;{Number(props.data.rating).toFixed(1)}&nbsp; &nbsp;{Number(props.data.get('rating')).toFixed(1)}&nbsp;
{props.data.votes && {props.data.get('votes') !== undefined &&
<small>({props.data.votes} counts)</small> <small>({props.data.get('votes')} counts)</small>
} }
</p> </p>
<p className="plot">{props.data.plot}</p> <p className="plot">{props.data.get('plot')}</p>
</div> </div>
{props.children} {props.children}
</div> </div>

View File

@ -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.entrySeq().map(function([source, categories]) {
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={this.props.placeHolder}
placeholder={props.controlPlaceHolder} onChange={this.handleChange}
updateOn="change" ref={(input) => this.input = input}
/> value={this.state.filter}
<span className="input-group-btn hidden-xs"> />
<button className="btn btn-default btn-sm" type="button">Filter</button> <span className="input-group-btn hidden-xs">
</span> <button className="btn btn-default btn-sm" type="button">Filter</button>
</Form> </span>
</form>
</div>
</div> </div>
</div> );
); }
} }

View File

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

View File

@ -1,4 +1,5 @@
import React from 'react' import React from 'react'
import { Map, List, fromJS } from 'immutable'
import fuzzy from 'fuzzy'; import fuzzy from 'fuzzy';
import InfiniteScroll from 'react-infinite-scroller'; import InfiniteScroll from 'react-infinite-scroller';
@ -24,14 +25,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 +48,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 } else {
}); elmts = elmts.slice(0, this.state.items);
elmts = filtered.map((el) => el.original); }
} else {
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}>
<ListFilter {displayFilter &&
listSize={listSize} <ListFilter
display={displayFilter} updateFilter={this.props.updateFilter}
formModel={this.props.formModel} placeHolder={this.props.placeHolder}
controlModel={this.props.filterControlModel} />
controlPlaceHolder={this.props.filterControlPlaceHolder} }
/>
<ExplorerOptions <ExplorerOptions
type={this.props.type} type={this.props.type}
display={!displayFilter} display={!displayFilter}
@ -105,39 +106,45 @@ export default class ListPosters extends React.Component {
} }
} }
function Posters(props) { class Posters extends React.PureComponent {
if (props.loading) { constructor(props) {
return (<Loader />); super(props);
} }
render() {
if (this.props.loading) {
return (<Loader />);
}
if (this.props.elmts.size === 0) {
return (
<div className="jumbotron">
<h2>No result</h2>
</div>
);
}
if (props.elmts.length === 0) {
return ( return (
<div className="jumbotron"> <div>
<h2>No result</h2> <InfiniteScroll
hasMore={this.props.hasMore}
loadMore={this.props.loadMore}
className="row"
>
{this.props.elmts.toIndexedSeq().map(function(movie, index) {
const imdbId = movie.get('imdb_id');
const selected = (imdbId === this.props.selectedImdbId) ? true : false;
return (
<ListPoster
index={index}
data={movie}
key={imdbId}
selected={selected}
onClick={() => this.props.onClick(imdbId)}
/>
)
} ,this)}
</InfiniteScroll>
</div> </div>
); );
} }
return (
<div>
<InfiniteScroll
hasMore={props.hasMore}
loadMore={props.loadMore}
className="row"
>
{props.elmts.map(function(el, index) {
const selected = (el.imdb_id === props.selectedImdbId) ? true : false;
return (
<ListPoster
index={index}
data={el}
key={el.imdb_id}
selected={selected}
onClick={() => props.onClick(el.imdb_id)}
/>
)}
)}
</InfiniteScroll>
</div>
);
} }

View File

@ -3,8 +3,8 @@ 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'
@ -14,48 +14,55 @@ import ListPosters from '../list/posters'
import ListDetails from '../list/details' 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 imdb_link = `http://www.imdb.com/title/${props.movie.get('imdb_id')}`;
const hasMovie = (props.movie.polochon_url !== ""); const hasMovie = (props.movie.get('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')}
/> />
<SubtitlesButton <SubtitlesButton
url={props.movie.polochon_url} fetching={props.movie.get('fetchingSubtitles')}
subtitles={props.movie.subtitles} url={props.movie.get('polochon_url')}
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"
/> />
@ -71,42 +78,36 @@ class MovieList extends React.Component {
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

@ -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,96 +1,63 @@
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) { export default function movieStore(state = defaultState, action) {
switch (action.type) { switch (action.type) {
case 'MOVIE_LIST_FETCH_PENDING': case 'MOVIE_LIST_FETCH_PENDING':
return Object.assign({}, state, { return state.set('loading', true);
loading: true,
})
case 'MOVIE_LIST_FETCH_FULFILLED': case 'MOVIE_LIST_FETCH_FULFILLED':
let movies = Map();
action.payload.response.data.map(function (movie) {
movie.fetchingDetails = false;
movie.fetchingSubtitles = false;
movies = movies.set(movie.imdb_id, fromJS(movie));
})
// 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,
selectedImdbId: selectedImdbId,
filter: defaultState.filter,
perPage: defaultState.perPage,
loading: false,
})
case 'MOVIE_GET_DETAILS_PENDING':
return Object.assign({}, state, {
fetchingDetails: true,
})
case 'MOVIE_GET_DETAILS_FULFILLED':
return Object.assign({}, state, {
movies: updateMovieDetails(state.movies.slice(), action.payload.response.data.imdb_id, action.payload.response.data),
fetchingDetails: false,
})
case 'MOVIE_UPDATE_STORE_WISHLIST':
return Object.assign({}, state, {
movies: updateStoreWishlist(state.movies.slice(), action.payload.imdbId, action.payload.wishlisted),
})
case 'MOVIE_GET_EXPLORE_OPTIONS_FULFILLED':
return Object.assign({}, state, {
exploreOptions: action.payload.response.data,
})
case 'UPDATE_LAST_MOVIE_FETCH_URL':
return Object.assign({}, state, {
lastFetchUrl: action.payload.url,
})
case 'MOVIE_SUBTITLES_UPDATE_PENDING':
return Object.assign({}, state, {
movies: updateMovieSubtitles(state.movies.slice(), state.selectedImdbId, true),
})
case 'MOVIE_SUBTITLES_UPDATE_FULFILLED':
console.log("payload :", action.payload);
return Object.assign({}, state, {
movies: updateMovieSubtitles(state.movies.slice(), state.selectedImdbId, false, action.payload.response.data),
})
case 'SELECT_MOVIE':
// Don't select the movie if we're fetching another movie's details
if (state.fetchingDetails) {
return state
} }
return Object.assign({}, state, { return state.delete('movies').merge(Map({
selectedImdbId: action.imdbId, movies: movies,
}) filter: "",
loading: false,
selectedImdbId: selectedImdbId,
}))
case 'MOVIE_GET_DETAILS_PENDING':
return state.setIn(['movies', action.payload.main.imdbId, 'fetchingDetails'], true);
case 'MOVIE_GET_DETAILS_FULFILLED':
let movie = action.payload.response.data;
movie.fetchingDetails = false;
movie.fetchingSubtitles = false;
return state.setIn(['movies', movie.imdb_id], fromJS(movie));
case 'MOVIE_UPDATE_STORE_WISHLIST':
return state.setIn(['movies', action.payload.imdbId, 'wishlisted'], action.payload.wishlisted);
case 'MOVIE_GET_EXPLORE_OPTIONS_FULFILLED':
return state.set('exploreOptions', fromJS(action.payload.response.data));
case 'UPDATE_LAST_MOVIE_FETCH_URL':
return state.set('lastFetchUrl', action.payload.url);
case 'MOVIE_SUBTITLES_UPDATE_PENDING':
return state.setIn(['movies', action.payload.main.imdbId, 'fetchingSubtitles'], true);
case 'MOVIE_SUBTITLES_UPDATE_FULFILLED':
return state.setIn(['movies', action.payload.main.imdbId, 'fetchingSubtitles'], false).setIn(['movies', action.payload.main.imdbId, 'subtitles'], fromJS(action.payload.data.response));
case 'SELECT_MOVIE':
return state.set('selectedImdbId', action.payload.imdbId);
case 'MOVIE_UPDATE_FILTER':
return state.set('filter', action.payload.filter);
default: default:
return state 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

@ -38,7 +38,8 @@ export function request(eventPrefix, promise, callbackEvents = null, mainPayload
message: response.data.message, message: response.data.message,
main: mainPayload, main: mainPayload,
} }
}) });
return;
} }
dispatch({ dispatch({
type: fulfilled, type: fulfilled,

View File

@ -130,7 +130,7 @@ export default function getRoutes(App) {
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(