Compare commits

..

No commits in common. "69115c731854c886579a1c7303e241062e54c19b" and "276e0e95911f67d868a66d70128cd0924b73311c" have entirely different histories.

98 changed files with 3293 additions and 3498 deletions

30
.eslintrc Normal file
View File

@ -0,0 +1,30 @@
{
"parser": "babel-eslint",
"extends": [
"eslint:recommended",
"plugin:react/recommended"
],
"env": {
"browser": true,
"node": true
},
"settings": {
"ecmascript": 6,
"jsx": true
},
"plugins": [
"react",
"react-hooks"
],
"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,
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}

View File

@ -1,46 +0,0 @@
{
"parser": "babel-eslint",
"extends": [
"eslint:recommended",
"plugin:import/recommended",
"plugin:react/recommended",
"plugin:prettier/recommended"
],
"settings": {
"react": {
"version": "detect"
}
},
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true,
"experimentalObjectRestSpread": true
}
},
"env": {
"browser": true,
"mocha": true,
"node": true
},
"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,
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"react/jsx-no-undef": 2,
"react/jsx-wrap-multilines": 2,
"react/no-string-refs": 0
},
"plugins": [
"import",
"react",
"react-hooks"
]
}

View File

@ -1,29 +1,42 @@
import { configureAxios, request } from "../requests"; import { configureAxios, request } from "../requests"
export function getUsers() { export function getUsers() {
return request("ADMIN_LIST_USERS", configureAxios().get("/admins/users")); return request(
"ADMIN_LIST_USERS",
configureAxios().get("/admins/users")
)
} }
export function getStats() { export function getStats() {
return request("ADMIN_GET_STATS", configureAxios().get("/admins/stats")); return request(
"ADMIN_GET_STATS",
configureAxios().get("/admins/stats")
)
} }
export function getAdminModules() { export function getAdminModules() {
return request("ADMIN_GET_MODULES", configureAxios().get("/admins/modules")); return request(
"ADMIN_GET_MODULES",
configureAxios().get("/admins/modules")
)
} }
export function updateUser(data) { export function updateUser(data) {
return request( return request(
"ADMIN_UPDATE_USER", "ADMIN_UPDATE_USER",
configureAxios().post("/admins/users", data), configureAxios().post("/admins/users", data),
[() => getUsers()] [
); () => getUsers(),
]
)
} }
export function deleteUser(username) { export function deleteUser(username) {
return request( return request(
"ADMIN_DELETE_USER", "ADMIN_DELETE_USER",
configureAxios().delete("/admins/users/" + username), configureAxios().delete("/admins/users/"+ username),
[() => getUsers()] [
); () => getUsers(),
]
)
} }

View File

@ -2,22 +2,22 @@ export function addAlertError(message) {
return { return {
type: "ADD_ALERT_ERROR", type: "ADD_ALERT_ERROR",
payload: { payload: {
message message,
} }
}; }
} }
export function addAlertOk(message) { export function addAlertOk(message) {
return { return {
type: "ADD_ALERT_OK", type: "ADD_ALERT_OK",
payload: { payload: {
message message,
} }
}; }
} }
export function dismissAlert() { export function dismissAlert() {
return { return {
type: "DISMISS_ALERT" type: "DISMISS_ALERT",
}; }
} }

View File

@ -1,40 +1,40 @@
import { configureAxios, request } from "../requests"; import { configureAxios, request } from "../requests"
import { addAlertOk } from "./alerts"; import { addAlertOk } from "./alerts"
import { sendNotification } from "./notifications"; import { sendNotification } from "./notifications"
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,
} },
}; }
} }
export function selectMovie(imdbId) { export function selectMovie(imdbId) {
return { return {
type: "SELECT_MOVIE", type: "SELECT_MOVIE",
payload: { payload: {
imdbId imdbId,
} },
}; }
} }
export function updateFilter(filter) { export function updateFilter(filter) {
return { return {
type: "MOVIE_UPDATE_FILTER", type: "MOVIE_UPDATE_FILTER",
payload: { payload: {
filter 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) {
@ -43,23 +43,27 @@ export function getMovieDetails(imdbId) {
configureAxios().post(`/movies/${imdbId}/refresh`), configureAxios().post(`/movies/${imdbId}/refresh`),
null, null,
{ {
imdbId imdbId,
} }
); )
} }
export function deleteMovie(imdbId, lastFetchUrl) { export function deleteMovie(imdbId, lastFetchUrl) {
return request("MOVIE_DELETE", configureAxios().delete(`/movies/${imdbId}`), [ return request(
fetchMovies(lastFetchUrl), "MOVIE_DELETE",
addAlertOk("Movie deleted") configureAxios().delete(`/movies/${imdbId}`),
]); [
fetchMovies(lastFetchUrl),
addAlertOk("Movie deleted"),
],
)
} }
export function movieWishlistToggle(imdbId, currentState) { export function movieWishlistToggle(imdbId, currentState) {
if (currentState == true) { if (currentState == true) {
return deleteMovieFromWishlist(imdbId); return deleteMovieFromWishlist(imdbId)
} else { } else {
return addMovieToWishlist(imdbId); return addMovieToWishlist(imdbId)
} }
} }
@ -67,16 +71,20 @@ 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}`),
[updateMovieWishlistStore(imdbId, true)] [
); updateMovieWishlistStore(imdbId, true),
],
)
} }
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}`),
[updateMovieWishlistStore(imdbId, false)] [
); updateMovieWishlistStore(imdbId, false),
],
)
} }
export function updateMovieWishlistStore(imdbId, wishlisted) { export function updateMovieWishlistStore(imdbId, wishlisted) {
@ -84,27 +92,29 @@ export function updateMovieWishlistStore(imdbId, wishlisted) {
type: "MOVIE_UPDATE_STORE_WISHLIST", type: "MOVIE_UPDATE_STORE_WISHLIST",
payload: { payload: {
imdbId, imdbId,
wishlisted wishlisted,
} }
}; }
} }
export function fetchMovies(url) { export function fetchMovies(url) {
return request("MOVIE_LIST_FETCH", configureAxios().get(url), [ return request(
updateLastMovieFetchUrl(url) "MOVIE_LIST_FETCH",
]); configureAxios().get(url),
[
updateLastMovieFetchUrl(url),
]
)
} }
export const newMovieEvent = data => { export const newMovieEvent = (data) => {
return dispatch => { return (dispatch) => {
dispatch( dispatch(sendNotification({
sendNotification({ icon: "film",
icon: "film", autohide: true,
autohide: true, delay: 10000,
delay: 10000, title: `${data.title} added to the library`,
title: `${data.title} added to the library`, imageUrl: data.thumb,
imageUrl: data.thumb }));
}) }
); }
};
};

View File

@ -1,9 +1,9 @@
export const sendNotification = data => ({ export const sendNotification = (data) => ({
type: "ADD_NOTIFICATION", type: "ADD_NOTIFICATION",
payload: data payload: data,
}); })
export const removeNotification = id => ({ export const removeNotification = (id) => ({
type: "REMOVE_NOTIFICATION", type: "REMOVE_NOTIFICATION",
payload: { id } payload: { id },
}); })

View File

@ -1,38 +1,50 @@
import { configureAxios, request } from "../requests"; import { configureAxios, request } from "../requests"
import { getUserInfos } from "./users"; import { getUserInfos } from "./users"
export const getPolochons = () => export const getPolochons = () => request(
request("PUBLIC_POLOCHON_LIST_FETCH", configureAxios().get("/polochons")); "PUBLIC_POLOCHON_LIST_FETCH",
configureAxios().get("/polochons"),
)
export const getManagedPolochons = () => export const getManagedPolochons = () => request(
request( "MANAGED_POLOCHON_LIST_FETCH",
"MANAGED_POLOCHON_LIST_FETCH", configureAxios().get("/users/polochons"),
configureAxios().get("/users/polochons") )
);
export const addPolochon = params => export const addPolochon = (params) => request(
request("ADD_POLOCHON", configureAxios().post("/polochons", params), [ "ADD_POLOCHON",
configureAxios().post("/polochons", params),
[
() => getPolochons(), () => getPolochons(),
() => getManagedPolochons() () => getManagedPolochons(),
]); ],
)
export const updatePolochon = ({ id, ...params }) => export const updatePolochon = ({ id, ...params }) => request(
request( "UPDATE_POLOCHON",
"UPDATE_POLOCHON", configureAxios().post(`/polochons/${id}`, params),
configureAxios().post(`/polochons/${id}`, params), [
[() => getPolochons(), () => getManagedPolochons()]
);
export const deletePolochon = id =>
request("DELETE_POLOCHON", configureAxios().delete(`/polochons/${id}`), [
() => getPolochons(), () => getPolochons(),
() => getManagedPolochons() () => getManagedPolochons(),
]); ],
)
export const editPolochonUser = ({ polochonId, id, ...params }) => export const deletePolochon = (id) => request(
request( "DELETE_POLOCHON",
"EDIT_POLOCHON_USER", configureAxios().delete(`/polochons/${id}`),
configureAxios().post(`/polochons/${polochonId}/users/${id}`, params), [
[() => getPolochons(), () => getManagedPolochons(), () => getUserInfos()] () => getPolochons(),
); () => getManagedPolochons(),
],
)
export const editPolochonUser = ({ polochonId, id, ...params }) => request(
"EDIT_POLOCHON_USER",
configureAxios().post(`/polochons/${polochonId}/users/${id}`, params),
[
() => getPolochons(),
() => getManagedPolochons(),
() => getUserInfos(),
],
)

View File

@ -1,12 +1,16 @@
import { configureAxios, request } from "../requests"; import { configureAxios, request } from "../requests"
import { prettyEpisodeName } from "../utils"; import { prettyEpisodeName } from "../utils"
import { sendNotification } from "./notifications"; import { sendNotification } from "./notifications"
export function fetchShows(url) { export function fetchShows(url) {
return request("SHOW_LIST_FETCH", configureAxios().get(url), [ return request(
updateLastShowsFetchUrl(url) "SHOW_LIST_FETCH",
]); configureAxios().get(url),
[
updateLastShowsFetchUrl(url),
]
)
} }
export function getShowDetails(imdbId) { export function getShowDetails(imdbId) {
@ -15,22 +19,21 @@ export function getShowDetails(imdbId) {
configureAxios().post(`/shows/${imdbId}/refresh`), configureAxios().post(`/shows/${imdbId}/refresh`),
null, null,
{ imdbId } { 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( configureAxios().post(`/shows/${imdbId}/seasons/${season}/episodes/${episode}`),
`/shows/${imdbId}/seasons/${season}/episodes/${episode}`
),
null, null,
{ {
imdbId, imdbId,
season, season,
episode episode,
} }
); )
} }
export function fetchShowDetails(imdbId) { export function fetchShowDetails(imdbId) {
@ -39,7 +42,7 @@ export function fetchShowDetails(imdbId) {
configureAxios().get(`/shows/${imdbId}`), configureAxios().get(`/shows/${imdbId}`),
null, null,
{ imdbId } { imdbId }
); )
} }
export function addShowToWishlist(imdbId, season = null, episode = null) { export function addShowToWishlist(imdbId, season = null, episode = null) {
@ -47,26 +50,25 @@ export function addShowToWishlist(imdbId, season = null, episode = null) {
"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,
}), }),
[updateShowWishlistStore(imdbId, true, season, episode)] [
); updateShowWishlistStore(imdbId, true, season, episode),
],
)
} }
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}`),
[updateShowWishlistStore(imdbId, false)] [
); updateShowWishlistStore(imdbId, false),
],
)
} }
export function showWishlistToggle( export function showWishlistToggle(currentState, imdbId, season = 0, episode = 0) {
currentState,
imdbId,
season = 0,
episode = 0
) {
if (currentState === true) { if (currentState === true) {
return deleteShowFromWishlist(imdbId); return deleteShowFromWishlist(imdbId);
} else { } else {
@ -74,70 +76,60 @@ export function showWishlistToggle(
} }
} }
export function updateShowWishlistStore( export function updateShowWishlistStore(imdbId, wishlisted, season = null, episode = null) {
imdbId,
wishlisted,
season = null,
episode = null
) {
return { return {
type: "SHOW_UPDATE_STORE_WISHLIST", type: "SHOW_UPDATE_STORE_WISHLIST",
payload: { payload: {
imdbId, imdbId,
season, season,
episode episode,
} }
}; }
} }
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",
payload: { payload: {
imdbId imdbId,
} }
}; }
} }
export function updateFilter(filter) { export function updateFilter(filter) {
return { return {
type: "SHOWS_UPDATE_FILTER", type: "SHOWS_UPDATE_FILTER",
payload: { payload: {
filter 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,
} },
}; }
} }
export const newEpisodeEvent = data => { export const newEpisodeEvent = (data) => {
return dispatch => { return (dispatch) => {
dispatch( dispatch(sendNotification({
sendNotification({ icon: "video-camera",
icon: "video-camera", autohide: true,
autohide: true, delay: 10000,
delay: 10000, title: `${prettyEpisodeName(data.show_title, data.season, data.episode)} added to the library`,
title: `${prettyEpisodeName( imageUrl: `img/shows/${data.show_imdb_id}-poster.jpg`,
data.show_title, }));
data.season, }
data.episode }
)} added to the library`,
imageUrl: `img/shows/${data.show_imdb_id}-poster.jpg`
})
);
};
};

View File

@ -1,13 +1,13 @@
import { configureAxios, request } from "../requests"; import { configureAxios, request } from "../requests"
export const searchMovieSubtitles = imdbId => { export const searchMovieSubtitles = (imdbId) => {
return request( return request(
"MOVIE_SUBTITLES_UPDATE", "MOVIE_SUBTITLES_UPDATE",
configureAxios().post(`/movies/${imdbId}/subtitles/refresh`), configureAxios().post(`/movies/${imdbId}/subtitles/refresh`),
null, null,
{ imdbId: imdbId } { imdbId: imdbId },
); )
}; }
export const searchEpisodeSubtitles = (imdbId, season, episode) => { export const searchEpisodeSubtitles = (imdbId, season, episode) => {
const url = `/shows/${imdbId}/seasons/${season}/episodes/${episode}`; const url = `/shows/${imdbId}/seasons/${season}/episodes/${episode}`;
@ -18,7 +18,7 @@ export const searchEpisodeSubtitles = (imdbId, season, episode) => {
{ {
imdbId: imdbId, imdbId: imdbId,
season: season, season: season,
episode: episode episode: episode,
} },
); );
}; }

View File

@ -1,29 +1,41 @@
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,
}), }),
[addAlertOk("Torrent added")] [
); addAlertOk("Torrent added"),
],
)
} }
export function removeTorrent(id) { export function removeTorrent(id) {
return request("REMOVE_TORRENT", configureAxios().delete(`/torrents/${id}`), [ return request(
() => fetchTorrents() "REMOVE_TORRENT",
]); configureAxios().delete(`/torrents/${id}`),
[
() => fetchTorrents(),
]
)
} }
export function fetchTorrents() { export function fetchTorrents() {
return request("TORRENTS_FETCH", configureAxios().get("/torrents")); return request(
"TORRENTS_FETCH",
configureAxios().get("/torrents")
)
} }
export function searchTorrents(url) { export function searchTorrents(url) {
return request("TORRENTS_SEARCH", configureAxios().get(url)); return request(
"TORRENTS_SEARCH",
configureAxios().get(url)
)
} }
export function setFetchedTorrents(torrents) { export function setFetchedTorrents(torrents) {
@ -31,8 +43,8 @@ export function setFetchedTorrents(torrents) {
type: "TORRENTS_FETCH_FULFILLED", type: "TORRENTS_FETCH_FULFILLED",
payload: { payload: {
response: { response: {
data: torrents data : torrents,
} },
} },
}; }
} }

View File

@ -1,31 +1,40 @@
import { configureAxios, request } from "../requests"; import { configureAxios, request } from "../requests"
import { addAlertOk, dismissAlert } from "./alerts"; import { addAlertOk, dismissAlert } from "./alerts"
import { getManagedPolochons } from "./polochon"; import { getManagedPolochons } from "./polochon"
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("/users/login", { configureAxios().post(
username: username.trim(), "/users/login",
password: password {
}), username: username.trim(),
[() => dismissAlert()] password: password,
); },
),
[
() => dismissAlert(),
]
)
} }
export function updateUser(config) { export function updateUser(config) {
return request("USER_UPDATE", configureAxios().post("/users/edit", config), [ return request(
addAlertOk("User updated"), "USER_UPDATE",
() => getManagedPolochons() configureAxios().post("/users/edit", config),
]); [
addAlertOk("User updated"),
() => getManagedPolochons(),
],
)
} }
export function userSignUp(config) { export function userSignUp(config) {
@ -35,39 +44,48 @@ export function userSignUp(config) {
return request( return request(
"USER_SIGNUP", "USER_SIGNUP",
configureAxios().post("/users/signup", config), configureAxios().post("/users/signup", config), [
[() => loginUser(config.username, config.password)] () => loginUser(config.username, config.password),
); ],
)
} }
export function getUserInfos() { export function getUserInfos() {
return request("GET_USER", configureAxios().get("/users/details")); return request(
"GET_USER",
configureAxios().get("/users/details")
)
} }
export function setUserToken(token) { export function setUserToken(token) {
return { return {
type: "USER_SET_TOKEN", type: "USER_SET_TOKEN",
payload: { payload: {
token: token token: token,
} },
}; }
} }
export function getUserTokens() { export function getUserTokens() {
return request("GET_USER_TOKENS", configureAxios().get("/users/tokens")); return request(
"GET_USER_TOKENS",
configureAxios().get("/users/tokens")
)
} }
export function deleteUserToken(token) { export function deleteUserToken(token) {
return request( return request(
"DELETE_USER_TOKEN", "DELETE_USER_TOKEN",
configureAxios().delete(`/users/tokens/${token}`), configureAxios().delete(`/users/tokens/${token}`),
[() => getUserTokens()] [
); () => getUserTokens(),
]
)
} }
export function getUserModules() { export function getUserModules() {
return request( return request(
"GET_USER_MODULES", "GET_USER_MODULES",
configureAxios().get("/users/modules/status") configureAxios().get("/users/modules/status")
); )
} }

View File

@ -1,47 +1,47 @@
// Import default image // Import default image
import "../img/noimage.png"; import "../img/noimage.png"
// Import favicon settings // Import favicon settings
import "../img/favicon-16x16.png"; import "../img/favicon-16x16.png"
import "../img/favicon-32x32.png"; import "../img/favicon-32x32.png"
import "../img/favicon.ico"; import "../img/favicon.ico"
import "../img/safari-pinned-tab.svg"; import "../img/safari-pinned-tab.svg"
// Styles // Styles
import "../scss/app.scss"; import "../scss/app.scss"
// React // React
import React from "react"; import React from "react"
import ReactDOM from "react-dom"; import ReactDOM from "react-dom"
import { Provider } from "react-redux"; import { Provider } from "react-redux"
import { Router, Route, Switch, Redirect } from "react-router-dom"; import { Router, Route, Switch, Redirect } from "react-router-dom"
import Container from "react-bootstrap/Container"; import Container from "react-bootstrap/Container"
// Auth // Auth
import { ProtectedRoute, AdminRoute } from "./auth"; import { ProtectedRoute, AdminRoute } from "./auth"
// Store // Store
import store, { history } from "./store"; import store, { history } from "./store"
// Components // Components
import { AdminPanel } from "./components/admins/panel"; import { AdminPanel } from "./components/admins/panel"
import { Notifications } from "./components/notifications/notifications"; import { Notifications } from "./components/notifications/notifications"
import Alert from "./components/alerts/alert"; import Alert from "./components/alerts/alert"
import MovieList from "./components/movies/list"; import MovieList from "./components/movies/list"
import MoviesRoute from "./components/movies/route"; import MoviesRoute from "./components/movies/route"
import NavBar from "./components/navbar"; import NavBar from "./components/navbar"
import WsHandler from "./components/websocket"; import WsHandler from "./components/websocket"
import { ShowDetails } from "./components/shows/details"; import { ShowDetails } from "./components/shows/details"
import ShowList from "./components/shows/list"; import ShowList from "./components/shows/list"
import ShowsRoute from "./components/shows/route"; import ShowsRoute from "./components/shows/route"
import TorrentList from "./components/torrents/list"; import TorrentList from "./components/torrents/list"
import TorrentSearch from "./components/torrents/search"; import TorrentSearch from "./components/torrents/search"
import UserActivation from "./components/users/activation"; import UserActivation from "./components/users/activation"
import UserLoginForm from "./components/users/login"; import UserLoginForm from "./components/users/login"
import UserLogout from "./components/users/logout"; import UserLogout from "./components/users/logout"
import UserProfile from "./components/users/profile"; import UserProfile from "./components/users/profile"
import UserSignUp from "./components/users/signup"; import UserSignUp from "./components/users/signup"
import UserTokens from "./components/users/tokens"; import UserTokens from "./components/users/tokens"
const App = () => ( const App = () => (
<div> <div>
@ -56,43 +56,25 @@ const App = () => (
<Route path="/users/tokens" exact component={UserTokens} /> <Route path="/users/tokens" exact component={UserTokens} />
<Route path="/torrents/list" exact component={TorrentList} /> <Route path="/torrents/list" exact component={TorrentList} />
<Route path="/torrents/search" exact component={TorrentSearch} /> <Route path="/torrents/search" exact component={TorrentSearch} />
<Route <Route path="/torrents/search/:type/:search" exact component={TorrentSearch} />
path="/torrents/search/:type/:search"
exact
component={TorrentSearch}
/>
<MoviesRoute path="/movies/polochon" exact component={MovieList} /> <MoviesRoute path="/movies/polochon" exact component={MovieList} />
<MoviesRoute path="/movies/wishlist" exact component={MovieList} /> <MoviesRoute path="/movies/wishlist" exact component={MovieList} />
<MoviesRoute <MoviesRoute path="/movies/search/:search" exact component={MovieList} />
path="/movies/search/:search" <MoviesRoute path="/movies/explore/:source/:category" exact component={MovieList} />
exact
component={MovieList}
/>
<MoviesRoute
path="/movies/explore/:source/:category"
exact
component={MovieList}
/>
<ShowsRoute path="/shows/polochon" exact component={ShowList} /> <ShowsRoute path="/shows/polochon" exact component={ShowList} />
<ShowsRoute path="/shows/wishlist" exact component={ShowList} /> <ShowsRoute path="/shows/wishlist" exact component={ShowList} />
<ShowsRoute path="/shows/search/:search" exact component={ShowList} /> <ShowsRoute path="/shows/search/:search" exact component={ShowList} />
<ShowsRoute <ShowsRoute path="/shows/explore/:source/:category" exact component={ShowList} />
path="/shows/explore/:source/:category" <ShowsRoute path="/shows/details/:imdbId" exact component={ShowDetails} />
exact <Route render={() =>
component={ShowList} <Redirect to="/movies/explore/yts/seeds" />
/> }/>
<ShowsRoute
path="/shows/details/:imdbId"
exact
component={ShowDetails}
/>
<Route render={() => <Redirect to="/movies/explore/yts/seeds" />} />
</Switch> </Switch>
</Container> </Container>
</div> </div>
); );
ReactDOM.render( ReactDOM.render((
<Provider store={store}> <Provider store={store}>
<Router history={history}> <Router history={history}>
<Switch> <Switch>
@ -103,6 +85,5 @@ ReactDOM.render(
<ProtectedRoute path="*" component={App} /> <ProtectedRoute path="*" component={App} />
</Switch> </Switch>
</Router> </Router>
</Provider>, </Provider>
document.getElementById("app") ),document.getElementById("app"));
);

View File

@ -1,8 +1,8 @@
import React from "react"; import React from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { connect } from "react-redux"; import { connect } from "react-redux"
import { Route, Redirect } from "react-router-dom"; import { Route, Redirect } from "react-router-dom"
import { setUserToken } from "./actions/users"; import { setUserToken } from "./actions/users"
const protectedRoute = ({ const protectedRoute = ({
component: Component, component: Component,
@ -22,64 +22,59 @@ const protectedRoute = ({
if (!isTokenSet) { if (!isTokenSet) {
setUserToken(token); setUserToken(token);
} }
return true; return true
} }
return false; return false
}; }
return ( return (
<Route <Route {...otherProps} render={(props) => {
{...otherProps} if (isAuthenticated()) {
render={props => { if (isActivated) {
if (isAuthenticated()) { return <Component {...props} />
if (isActivated) {
return <Component {...props} />;
} else {
return <Redirect to="/users/activation" />;
}
} else { } else {
return <Redirect to="/users/login" />; return <Redirect to="/users/activation" />
} }
}} } else {
/> return <Redirect to="/users/login" />
); }
}; }} />
)
}
protectedRoute.propTypes = { protectedRoute.propTypes = {
component: PropTypes.func, component: PropTypes.func,
isLogged: PropTypes.bool.isRequired, isLogged: PropTypes.bool.isRequired,
isActivated: PropTypes.bool.isRequired, isActivated: PropTypes.bool.isRequired,
isTokenSet: PropTypes.bool.isRequired, isTokenSet: PropTypes.bool.isRequired,
setUserToken: PropTypes.func.isRequired setUserToken: PropTypes.func.isRequired,
}; };
export const ProtectedRoute = connect( export const ProtectedRoute = connect((state) => ({
state => ({ isLogged: state.userStore.get("isLogged"),
isLogged: state.userStore.get("isLogged"), isAdmin: state.userStore.get("isLogged"),
isAdmin: state.userStore.get("isLogged"), isActivated: state.userStore.get("isActivated"),
isActivated: state.userStore.get("isActivated"), isTokenSet: state.userStore.get("isTokenSet"),
isTokenSet: state.userStore.get("isTokenSet") }), { setUserToken })(protectedRoute);
}),
{ setUserToken }
)(protectedRoute);
const adminRoute = ({ component: Component, isAdmin, ...otherProps }) => { const adminRoute = ({
component: Component,
isAdmin,
...otherProps
}) => {
return ( return (
<Route <Route {...otherProps} render={(props) => {
{...otherProps} if (isAdmin) {
render={props => { return <Component {...props} />
if (isAdmin) { } else {
return <Component {...props} />; return <Redirect to="/" />
} else { }
return <Redirect to="/" />; }} />
} )
}} }
/>
);
};
adminRoute.propTypes = { adminRoute.propTypes = {
component: PropTypes.func, component: PropTypes.func,
isAdmin: PropTypes.bool.isRequired isAdmin: PropTypes.bool.isRequired,
}; };
export const AdminRoute = connect(state => ({ export const AdminRoute = connect((state) => ({
isAdmin: state.userStore.get("isLogged") isAdmin: state.userStore.get("isLogged"),
}))(adminRoute); }))(adminRoute);

View File

@ -1,28 +1,28 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { connect } from "react-redux"; import { connect } from "react-redux"
import { getAdminModules } from "../../actions/admins"; import { getAdminModules} from "../../actions/admins"
import Modules from "../modules/modules"; import Modules from "../modules/modules"
const AdminModulesConnected = ({ modules, loading, getAdminModules }) => { const AdminModulesConnected = ({ modules, loading, getAdminModules }) => {
useEffect(() => { useEffect(() => {
getAdminModules(); getAdminModules();
}, [getAdminModules]); }, [getAdminModules])
return <Modules modules={modules} isLoading={loading} />; return (
}; <Modules modules={modules} isLoading={loading} />
)
}
AdminModulesConnected.propTypes = { AdminModulesConnected.propTypes = {
modules: PropTypes.object, modules: PropTypes.object,
loading: PropTypes.bool, loading: PropTypes.bool,
getAdminModules: PropTypes.func.isRequired getAdminModules: PropTypes.func.isRequired,
}; };
const mapStateToProps = state => ({ const mapStateToProps = state => ({
loading: state.adminStore.get("fetchingModules"), loading: state.adminStore.get("fetchingModules"),
modules: state.adminStore.get("modules") modules: state.adminStore.get("modules"),
}); });
export const AdminModules = connect(mapStateToProps, { getAdminModules })( export const AdminModules = connect(mapStateToProps, {getAdminModules})(AdminModulesConnected);
AdminModulesConnected
);

View File

@ -1,8 +1,8 @@
import React from "react"; import React from "react"
import { UserList } from "./userList"; import { UserList } from "./userList"
import { Stats } from "./stats"; import { Stats } from "./stats"
import { AdminModules } from "./modules"; import { AdminModules } from "./modules"
export const AdminPanel = () => ( export const AdminPanel = () => (
<React.Fragment> <React.Fragment>
@ -10,4 +10,4 @@ export const AdminPanel = () => (
<UserList /> <UserList />
<AdminModules /> <AdminModules />
</React.Fragment> </React.Fragment>
); )

View File

@ -1,7 +1,7 @@
import React from "react"; import React from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { TorrentsStat } from "./torrentsStat"; import { TorrentsStat } from "./torrentsStat"
export const Stat = ({ name, count, torrentCount, torrentCountById }) => ( export const Stat = ({ name, count, torrentCount, torrentCountById }) => (
<div className="col-12 col-md-4 my-2"> <div className="col-12 col-md-4 my-2">
@ -9,9 +9,7 @@ export const Stat = ({ name, count, torrentCount, torrentCountById }) => (
<div className="card-header"> <div className="card-header">
<h3> <h3>
{name} {name}
<span className="badge badge-pill badge-info pull-right"> <span className="badge badge-pill badge-info pull-right">{count}</span>
{count}
</span>
</h3> </h3>
</div> </div>
<div className="card-body"> <div className="card-body">
@ -23,10 +21,10 @@ export const Stat = ({ name, count, torrentCount, torrentCountById }) => (
</div> </div>
</div> </div>
</div> </div>
); )
Stat.propTypes = { Stat.propTypes = {
name: PropTypes.string, name: PropTypes.string,
count: PropTypes.number, count: PropTypes.number,
torrentCount: PropTypes.number, torrentCount: PropTypes.number,
torrentCountById: PropTypes.number torrentCountById: PropTypes.number,
}; };

View File

@ -1,15 +1,15 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { connect } from "react-redux"; import { connect } from "react-redux"
import { Stat } from "./stat"; import { Stat } from "./stat"
import { getStats } from "../../actions/admins"; import { getStats } from "../../actions/admins"
const StatsConnected = ({ stats, getStats }) => { const StatsConnected = ({ stats, getStats }) => {
useEffect(() => { useEffect(() => {
getStats(); getStats();
}, [getStats]); }, [getStats])
return ( return (
<div className="row d-flex flex-wrap"> <div className="row d-flex flex-wrap">
@ -32,15 +32,15 @@ const StatsConnected = ({ stats, getStats }) => {
torrentCountById={stats.get("episodes_torrents_count_by_id")} torrentCountById={stats.get("episodes_torrents_count_by_id")}
/> />
</div> </div>
); )
}; }
StatsConnected.propTypes = { StatsConnected.propTypes = {
stats: PropTypes.object, stats: PropTypes.object,
getStats: PropTypes.func getStats: PropTypes.func,
}; }
const mapStateToProps = state => ({ const mapStateToProps = state => ({
stats: state.adminStore.get("stats") stats: state.adminStore.get("stats"),
}); });
export const Stats = connect(mapStateToProps, { getStats })(StatsConnected); export const Stats = connect(mapStateToProps, {getStats})(StatsConnected);

View File

@ -1,9 +1,9 @@
import React from "react"; import React from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
export const TorrentsStat = ({ count, torrentCount, torrentCountById }) => { export const TorrentsStat = ({ count, torrentCount, torrentCountById }) => {
if (torrentCount === undefined) { if (torrentCount === undefined) {
return <span>No torrents</span>; return (<span>No torrents</span>);
} }
const percentage = Math.floor((torrentCountById * 100) / count); const percentage = Math.floor((torrentCountById * 100) / count);
@ -13,9 +13,9 @@ export const TorrentsStat = ({ count, torrentCount, torrentCountById }) => {
<small>&nbsp; - {torrentCount} total</small> <small>&nbsp; - {torrentCount} total</small>
</span> </span>
); );
}; }
TorrentsStat.propTypes = { TorrentsStat.propTypes = {
count: PropTypes.number, count: PropTypes.number,
torrentCount: PropTypes.number, torrentCount: PropTypes.number,
torrentCountById: PropTypes.number torrentCountById: PropTypes.number,
}; };

View File

@ -1,7 +1,7 @@
import React from "react"; import React from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { UserEdit } from "./userEdit"; import { UserEdit } from "./userEdit"
export const User = ({ export const User = ({
id, id,
@ -12,31 +12,27 @@ export const User = ({
polochonUrl, polochonUrl,
polochonName, polochonName,
polochonId, polochonId,
token token,
}) => { }) => {
return ( return (
<tr> <tr>
<td>{id}</td> <td>{id}</td>
<td>{name}</td> <td>{name}</td>
<td> <td>
<span <span className={activated ? "fa fa-check" : "fa fa-times text-danger"}></span>
className={activated ? "fa fa-check" : "fa fa-times text-danger"}
></span>
</td> </td>
<td> <td>
<span className={admin ? "fa fa-check" : "fa fa-times"}></span> <span className={admin ? "fa fa-check" : "fa fa-times"}></span>
</td> </td>
<td> <td>
{polochonName !== "" ? polochonName : "-"} {polochonName !== "" ? polochonName : "-"}
{polochonUrl !== "" && <small className="ml-1">({polochonUrl})</small>} {polochonUrl !== "" &&
<small className="ml-1">({polochonUrl})</small>
}
</td> </td>
<td>{token}</td> <td>{token}</td>
<td> <td>
<span <span className={polochonActivated ? "fa fa-check" : "fa fa-times text-danger"}></span>
className={
polochonActivated ? "fa fa-check" : "fa fa-times text-danger"
}
></span>
</td> </td>
<td> <td>
<UserEdit <UserEdit
@ -51,7 +47,7 @@ export const User = ({
</td> </td>
</tr> </tr>
); );
}; }
User.propTypes = { User.propTypes = {
id: PropTypes.string, id: PropTypes.string,
name: PropTypes.string, name: PropTypes.string,
@ -61,5 +57,5 @@ User.propTypes = {
token: PropTypes.string, token: PropTypes.string,
admin: PropTypes.bool, admin: PropTypes.bool,
activated: PropTypes.bool, activated: PropTypes.bool,
polochonActivated: PropTypes.bool polochonActivated: PropTypes.bool,
}; };

View File

@ -1,15 +1,15 @@
import React, { useState } from "react"; import React, { useState } from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { connect } from "react-redux"; import { connect } from "react-redux"
import { List } from "immutable"; import { List } from "immutable"
import { updateUser, deleteUser } from "../../actions/admins"; import { updateUser, deleteUser } from "../../actions/admins"
import Toggle from "react-bootstrap-toggle"; import Toggle from "react-bootstrap-toggle";
import { PolochonSelect } from "../polochons/select"; import { PolochonSelect } from "../polochons/select"
import { FormModal } from "../forms/modal"; import { FormModal } from "../forms/modal"
import { FormInput } from "../forms/input"; import { FormInput } from "../forms/input"
const UserEditConnect = ({ const UserEditConnect = ({
id, id,
@ -21,23 +21,19 @@ const UserEditConnect = ({
polochonActivated: initPolochonActivated, polochonActivated: initPolochonActivated,
updateUser, updateUser,
deleteUser, deleteUser,
publicPolochons publicPolochons,
}) => { }) => {
const [modal, setModal] = useState(false); const [modal, setModal] = useState(false);
const [admin, setAdmin] = useState(initAdmin); const [admin, setAdmin] = useState(initAdmin);
const [activated, setActivated] = useState(initActivated); const [activated, setActivated] = useState(initActivated);
const [token, setToken] = useState(polochonToken); const [token, setToken] = useState(polochonToken);
const [polochonId, setPolochonId] = useState(initPolochonId); const [polochonId, setPolochonId] = useState(initPolochonId);
const [polochonActivated, setPolochonActivated] = useState( const [polochonActivated, setPolochonActivated] = useState(initPolochonActivated);
initPolochonActivated
);
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [confirmDelete, setConfirmDelete] = useState(false); const [confirmDelete, setConfirmDelete] = useState(false);
const handleSubmit = e => { const handleSubmit = (e) => {
if (e) { if (e) { e.preventDefault(); }
e.preventDefault();
}
updateUser({ updateUser({
userId: id, userId: id,
polochonToken: token, polochonToken: token,
@ -45,26 +41,26 @@ const UserEditConnect = ({
activated, activated,
polochonId, polochonId,
polochonActivated, polochonActivated,
password password,
}); });
setModal(false); setModal(false);
}; };
const handleDeleteUser = e => { const handleDeleteUser = (e) => {
if (e) { if (e) { e.preventDefault(); }
e.preventDefault();
}
if (confirmDelete) { if (confirmDelete) {
deleteUser(name); deleteUser(name)
setModal(false); setModal(false)
} else { } else {
setConfirmDelete(true); setConfirmDelete(true)
} }
}; }
return ( return (
<span> <span>
<i className="fa fa-pencil clickable" onClick={() => setModal(true)} /> <i
className="fa fa-pencil clickable"
onClick={() => setModal(true)} />
<FormModal <FormModal
show={modal} show={modal}
@ -73,70 +69,41 @@ const UserEditConnect = ({
icon="edit" icon="edit"
handleSubmit={handleSubmit} handleSubmit={handleSubmit}
> >
<div className="form-group">
<label>Account status</label>
<Toggle
className="pull-right"
on="Activated"
off="Deactivated"
active={activated}
offstyle="danger"
handlestyle="secondary"
onClick={() => setActivated(!activated)}
/>
</div>
<div className="form-group"> <div className="form-group">
<label>Admin status</label> <label>Account status</label>
<Toggle <Toggle className="pull-right" on="Activated" off="Deactivated" active={activated}
className="pull-right" offstyle="danger" handlestyle="secondary" onClick={() => setActivated(!activated)}
on="Admin"
off="User"
active={admin}
offstyle="info"
handlestyle="secondary"
onClick={() => setAdmin(!admin)}
/>
</div>
<FormInput
label="Password"
value={password}
updateValue={setPassword}
/> />
</div>
<PolochonSelect <div className="form-group">
value={polochonId} <label>Admin status</label>
changeValue={setPolochonId} <Toggle className="pull-right" on="Admin" off="User" active={admin}
polochonList={publicPolochons} offstyle="info" handlestyle="secondary" onClick={() => setAdmin(!admin)} />
/> </div>
<FormInput
label="Polochon Token"
value={token}
updateValue={setToken}
/>
<div className="form-group">
<label>Polochon activated</label>
<Toggle
className="pull-right"
on="Activated"
off="Deactivated"
active={polochonActivated}
offstyle="danger"
handlestyle="secondary"
onClick={() => setPolochonActivated(!polochonActivated)}
/>
</div>
<div className="form-group"> <FormInput label="Password" value={password} updateValue={setPassword} />
<button className="btn btn-danger w-100" onClick={handleDeleteUser}>
{!confirmDelete ? "Delete user forever" : "Are you sure ?"} <PolochonSelect value={polochonId} changeValue={setPolochonId} polochonList={publicPolochons} />
</button> <FormInput label="Polochon Token" value={token} updateValue={setToken} />
</div> <div className="form-group">
</FormModal> <label>Polochon activated</label>
</span> <Toggle className="pull-right" on="Activated" off="Deactivated" active={polochonActivated}
offstyle="danger" handlestyle="secondary" onClick={() => setPolochonActivated(!polochonActivated)}
/>
</div>
<div className="form-group">
<button className="btn btn-danger w-100" onClick={handleDeleteUser}>
{!confirmDelete ? "Delete user forever" : "Are you sure ?"}
</button>
</div>
</FormModal>
</span>
); );
}; }
UserEditConnect.propTypes = { UserEditConnect.propTypes = {
id: PropTypes.string, id: PropTypes.string,
name: PropTypes.string, name: PropTypes.string,
@ -147,13 +114,11 @@ UserEditConnect.propTypes = {
polochonToken: PropTypes.string, polochonToken: PropTypes.string,
polochonId: PropTypes.string, polochonId: PropTypes.string,
polochonActivated: PropTypes.bool, polochonActivated: PropTypes.bool,
publicPolochons: PropTypes.instanceOf(List) publicPolochons: PropTypes.instanceOf(List),
}; };
const mapStateToProps = state => ({ const mapStateToProps = (state) => ({
publicPolochons: state.polochon.get("public") publicPolochons: state.polochon.get("public"),
}); });
export const UserEdit = connect(mapStateToProps, { updateUser, deleteUser })( export const UserEdit = connect(mapStateToProps, {updateUser, deleteUser})(UserEditConnect);
UserEditConnect
);

View File

@ -1,15 +1,15 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { List } from "immutable"; import { List } from "immutable"
import { connect } from "react-redux"; import { connect } from "react-redux"
import { User } from "./user"; import { User } from "./user"
import { getUsers } from "../../actions/admins"; import { getUsers } from "../../actions/admins"
import { getPolochons } from "../../actions/polochon"; import { getPolochons } from "../../actions/polochon"
const mapStateToProps = state => ({ const mapStateToProps = state => ({
users: state.adminStore.get("users") users: state.adminStore.get("users"),
}); });
const mapDispatchToProps = { getUsers, getPolochons }; const mapDispatchToProps = { getUsers, getPolochons };
@ -17,7 +17,7 @@ const UserListConnect = ({ users, getUsers, getPolochons }) => {
useEffect(() => { useEffect(() => {
getUsers(); getUsers();
getPolochons(); getPolochons();
}, [getUsers, getPolochons]); }, [getUsers, getPolochons])
return ( return (
<div className="table-responsive my-2"> <div className="table-responsive my-2">
@ -35,7 +35,7 @@ const UserListConnect = ({ users, getUsers, getPolochons }) => {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{users.map((el, index) => ( {users.map((el, index) =>
<User <User
key={index} key={index}
id={el.get("id")} id={el.get("id")}
@ -48,7 +48,7 @@ const UserListConnect = ({ users, getUsers, getPolochons }) => {
polochonUrl={el.getIn(["polochon", "url"], "")} polochonUrl={el.getIn(["polochon", "url"], "")}
polochonName={el.getIn(["polochon", "name"], "")} polochonName={el.getIn(["polochon", "name"], "")}
/> />
))} )}
</tbody> </tbody>
</table> </table>
</div> </div>
@ -57,10 +57,7 @@ const UserListConnect = ({ users, getUsers, getPolochons }) => {
UserListConnect.propTypes = { UserListConnect.propTypes = {
getUsers: PropTypes.func, getUsers: PropTypes.func,
getPolochons: PropTypes.func, getPolochons: PropTypes.func,
users: PropTypes.PropTypes.instanceOf(List) users: PropTypes.PropTypes.instanceOf(List),
}; };
export const UserList = connect( export const UserList = connect(mapStateToProps, mapDispatchToProps)(UserListConnect);
mapStateToProps,
mapDispatchToProps
)(UserListConnect);

View File

@ -1,35 +1,34 @@
import React, { useState } from "react"; import React, { useState } from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { List } from "immutable"; import { List } from "immutable"
import { Button, Modal } from "react-bootstrap"; import { Button, Modal } from "react-bootstrap"
import Toggle from "react-bootstrap-toggle"; import Toggle from "react-bootstrap-toggle";
export const UserList = props => ( export const UserList = props => (
<div className="table-responsive my-2"> <div className="table-responsive my-2">
<table className="table table-striped"> <table className="table table-striped">
<thead className="table-secondary"> <thead className="table-secondary">
<tr> <tr>
<th>#</th> <th>#</th>
<th>Name</th> <th>Name</th>
<th>Activated</th> <th>Activated</th>
<th>Admin</th> <th>Admin</th>
<th>Polochon URL</th> <th>Polochon URL</th>
<th>Polochon token</th> <th>Polochon token</th>
<th>Actions</th> <th>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{props.users.map((el, index) => ( {props.users.map((el, index) =>
<User key={index} data={el} updateUser={props.updateUser} /> <User key={index} data={el} updateUser={props.updateUser}/>)}
))}
</tbody> </tbody>
</table> </table>
</div> </div>
); );
UserList.propTypes = { UserList.propTypes = {
users: PropTypes.PropTypes.instanceOf(List), users: PropTypes.PropTypes.instanceOf(List),
updateUser: PropTypes.func updateUser: PropTypes.func,
}; };
const User = function(props) { const User = function(props) {
@ -40,12 +39,8 @@ const User = function(props) {
<tr> <tr>
<td>{props.data.get("id")}</td> <td>{props.data.get("id")}</td>
<td>{props.data.get("Name")}</td> <td>{props.data.get("Name")}</td>
<td> <td><UserActivationStatus activated={props.data.get("Activated")}/></td>
<UserActivationStatus activated={props.data.get("Activated")} /> <td><UserAdminStatus admin={props.data.get("Admin")}/></td>
</td>
<td>
<UserAdminStatus admin={props.data.get("Admin")} />
</td>
<td>{polochonUrl}</td> <td>{polochonUrl}</td>
<td>{polochonToken}</td> <td>{polochonToken}</td>
<td> <td>
@ -58,10 +53,10 @@ const User = function(props) {
</td> </td>
</tr> </tr>
); );
}; }
User.propTypes = { User.propTypes = {
data: PropTypes.object, data: PropTypes.object,
updateUser: PropTypes.func updateUser: PropTypes.func,
}; };
const UserAdminStatus = props => ( const UserAdminStatus = props => (
@ -70,9 +65,7 @@ const UserAdminStatus = props => (
UserAdminStatus.propTypes = { admin: PropTypes.bool.isRequired }; UserAdminStatus.propTypes = { admin: PropTypes.bool.isRequired };
const UserActivationStatus = props => ( const UserActivationStatus = props => (
<span <span className={props.activated ? "fa fa-check" : "fa fa-times text-danger"}></span>
className={props.activated ? "fa fa-check" : "fa fa-times text-danger"}
></span>
); );
UserActivationStatus.propTypes = { activated: PropTypes.bool.isRequired }; UserActivationStatus.propTypes = { activated: PropTypes.bool.isRequired };
@ -84,25 +77,20 @@ function UserEdit(props) {
const [token, setToken] = useState(props.polochonToken); const [token, setToken] = useState(props.polochonToken);
const handleSubmit = function(e) { const handleSubmit = function(e) {
if (e) { if (e) { e.preventDefault(); }
e.preventDefault();
}
props.updateUser({ props.updateUser({
userId: props.data.get("id"), userId: props.data.get("id"),
admin: admin, admin: admin,
activated: activated, activated: activated,
polochonUrl: url, polochonUrl: url,
polochonToken: token polochonToken: token,
}); });
setModal(false); setModal(false);
}; };
return ( return (
<span> <span>
<span <span className="fa fa-pencil clickable" onClick={() => setModal(true)}></span>
className="fa fa-pencil clickable"
onClick={() => setModal(true)}
></span>
<Modal show={modal} onHide={() => setModal(false)}> <Modal show={modal} onHide={() => setModal(false)}>
<Modal.Header closeButton> <Modal.Header closeButton>
<Modal.Title> <Modal.Title>
@ -111,53 +99,30 @@ function UserEdit(props) {
</Modal.Title> </Modal.Title>
</Modal.Header> </Modal.Header>
<Modal.Body bsPrefix="modal-body admin-edit-user-modal"> <Modal.Body bsPrefix="modal-body admin-edit-user-modal">
<form className="form-horizontal" onSubmit={ev => handleSubmit(ev)}> <form className="form-horizontal" onSubmit={(ev) => handleSubmit(ev)}>
<div className="form-group"> <div className="form-group">
<label>Account status</label> <label>Account status</label>
<Toggle <Toggle className="pull-right" on="Activated" off="Deactivated" active={activated}
className="pull-right" offstyle="danger" handlestyle="secondary" onClick={() => setActivated(!activated)}
on="Activated"
off="Deactivated"
active={activated}
offstyle="danger"
handlestyle="secondary"
onClick={() => setActivated(!activated)}
/> />
</div> </div>
<div className="form-group"> <div className="form-group">
<label>Admin status</label> <label>Admin status</label>
<Toggle <Toggle className="pull-right" on="Admin" off="User" active={admin}
className="pull-right" offstyle="info" handlestyle="secondary" onClick={() => setAdmin(!admin)} />
on="Admin"
off="User"
active={admin}
offstyle="info"
handlestyle="secondary"
onClick={() => setAdmin(!admin)}
/>
</div> </div>
<div className="form-group"> <div className="form-group">
<label className="control-label">Polochon URL</label> <label className="control-label">Polochon URL</label>
<input <input className="form-control" value={url} onChange={(e) => setUrl(e.target.value)}/>
className="form-control"
value={url}
onChange={e => setUrl(e.target.value)}
/>
</div> </div>
<div className="form-group"> <div className="form-group">
<label className="control-label">Polochon token</label> <label className="control-label">Polochon token</label>
<input <input className="form-control" value={token} onChange={(e) => setToken(e.target.value)} />
className="form-control"
value={token}
onChange={e => setToken(e.target.value)}
/>
</div> </div>
</form> </form>
</Modal.Body> </Modal.Body>
<Modal.Footer> <Modal.Footer>
<Button variant="success" onClick={handleSubmit}> <Button variant="success" onClick={handleSubmit}>Apply</Button>
Apply
</Button>
<Button onClick={() => setModal(false)}>Close</Button> <Button onClick={() => setModal(false)}>Close</Button>
</Modal.Footer> </Modal.Footer>
</Modal> </Modal>
@ -168,5 +133,5 @@ UserEdit.propTypes = {
data: PropTypes.object, data: PropTypes.object,
updateUser: PropTypes.func, updateUser: PropTypes.func,
polochonUrl: PropTypes.string, polochonUrl: PropTypes.string,
polochonToken: PropTypes.string polochonToken: PropTypes.string,
}; };

View File

@ -1,20 +1,20 @@
import React from "react"; import React from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import SweetAlert from "react-bootstrap-sweetalert"; import SweetAlert from "react-bootstrap-sweetalert";
import { connect } from "react-redux"; import { connect } from "react-redux"
import { dismissAlert } from "../../actions/alerts"; import { dismissAlert } from "../../actions/alerts"
const mapStateToProps = state => ({ const mapStateToProps = (state) => ({
show: state.alerts.get("show"), show: state.alerts.get("show"),
title: state.alerts.get("message"), title: state.alerts.get("message"),
type: state.alerts.get("type") type: state.alerts.get("type"),
}); });
const mapDispatchToProps = { dismissAlert }; const mapDispatchToProps = { dismissAlert };
const Alert = props => { const Alert = (props) => {
if (!props.show) { if (!props.show) {
return null; return null
} }
return ( return (
@ -23,13 +23,13 @@ const Alert = props => {
title={props.title} title={props.title}
onConfirm={props.dismissAlert} onConfirm={props.dismissAlert}
/> />
); )
}; }
Alert.propTypes = { Alert.propTypes = {
show: PropTypes.bool.isRequired, show: PropTypes.bool.isRequired,
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
dismissAlert: PropTypes.func.isRequired, dismissAlert: PropTypes.func.isRequired,
type: PropTypes.string type: PropTypes.string,
}; };
export default connect(mapStateToProps, mapDispatchToProps)(Alert); export default connect(mapStateToProps, mapDispatchToProps)(Alert);

View File

@ -1,13 +1,11 @@
import React, { useState } from "react"; import React, { useState } from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { List } from "immutable"; import { List } from "immutable"
import Modal from "react-bootstrap/Modal"; import Modal from "react-bootstrap/Modal"
export const DownloadAndStream = ({ url, name, subtitles }) => { export const DownloadAndStream = ({ url, name, subtitles }) => {
if (!url || url === "") { if (!url || url === "") { return null; }
return null;
}
return ( return (
<span> <span>
@ -15,11 +13,11 @@ export const DownloadAndStream = ({ url, name, subtitles }) => {
<StreamButton url={url} name={name} subtitles={subtitles} /> <StreamButton url={url} name={name} subtitles={subtitles} />
</span> </span>
); );
}; }
DownloadAndStream.propTypes = { DownloadAndStream.propTypes = {
url: PropTypes.string, url: PropTypes.string,
name: PropTypes.string, name: PropTypes.string,
subtitles: PropTypes.instanceOf(List) subtitles: PropTypes.instanceOf(List),
}; };
const DownloadButton = ({ url }) => ( const DownloadButton = ({ url }) => (
@ -41,22 +39,13 @@ const StreamButton = ({ name, url, subtitles }) => {
<a <a
href="#" href="#"
className="btn btn-sm btn-primary m-1 clickable" className="btn btn-sm btn-primary m-1 clickable"
onClick={e => { onClick={(e) => { e.preventDefault(); setShowModal(true) }}>
e.preventDefault();
setShowModal(true);
}}
>
<i className="fa fa-play-circle mr-1" /> <i className="fa fa-play-circle mr-1" />
Play Play
</a> </a>
</h5> </h5>
<Modal <Modal show={showModal} onHide={() => setShowModal(false)} size="lg" centered>
show={showModal}
onHide={() => setShowModal(false)}
size="lg"
centered
>
<Modal.Header closeButton>{name}</Modal.Header> <Modal.Header closeButton>{name}</Modal.Header>
<Modal.Body> <Modal.Body>
<Player url={url} subtitles={subtitles} /> <Player url={url} subtitles={subtitles} />
@ -64,11 +53,11 @@ const StreamButton = ({ name, url, subtitles }) => {
</Modal> </Modal>
</React.Fragment> </React.Fragment>
); );
}; }
StreamButton.propTypes = { StreamButton.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
url: PropTypes.string.isRequired, url: PropTypes.string.isRequired,
subtitles: PropTypes.instanceOf(List) subtitles: PropTypes.instanceOf(List),
}; };
const Player = ({ url, subtitles }) => { const Player = ({ url, subtitles }) => {
@ -76,27 +65,24 @@ const Player = ({ url, subtitles }) => {
return ( return (
<div className="embed-responsive embed-responsive-16by9"> <div className="embed-responsive embed-responsive-16by9">
<video className="embed-responsive-item" controls> <video className="embed-responsive-item" controls>
<source src={url} type="video/mp4" /> <source src={url} type="video/mp4"/>
{hasSubtitles && {hasSubtitles && subtitles.toIndexedSeq().map((el, index) => (
subtitles <track
.toIndexedSeq() key={index}
.map((el, index) => ( kind="subtitles"
<track label={el.get("language")}
key={index} src={el.get("vvt_file")}
kind="subtitles" srcLang={el.get("language")}
label={el.get("language")} />
src={el.get("vvt_file")} ))}
srcLang={el.get("language")}
/>
))}
</video> </video>
</div> </div>
); );
}; }
Player.propTypes = { Player.propTypes = {
subtitles: PropTypes.instanceOf(List), subtitles: PropTypes.instanceOf(List),
url: PropTypes.string.isRequired url: PropTypes.string.isRequired,
}; };
Player.defaultProps = { Player.defaultProps = {
subtitles: List() subtitles: List(),
}; };

View File

@ -1,22 +1,19 @@
import React from "react"; import React from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
export const ImdbBadge = ({ imdbId }) => { export const ImdbBadge = ({ imdbId }) => {
if (imdbId === undefined) { if (imdbId === undefined) { return null }
return null;
}
return ( return (
<h5 className="d-inline"> <h5 className="d-inline">
<a <a
className="btn btn-sm btn-warning m-1" className="btn btn-sm btn-warning m-1"
href={`https://www.imdb.com/title/${imdbId}`} href={`https://www.imdb.com/title/${imdbId}`}>
>
IMDb IMDb
<i className="ml-1 fa fa-external-link"></i> <i className="ml-1 fa fa-external-link"></i>
</a> </a>
</h5> </h5>
); );
}; }
ImdbBadge.propTypes = { imdbId: PropTypes.string }; ImdbBadge.propTypes = { imdbId: PropTypes.string };
ImdbBadge.defaultProps = { imdbId: undefined }; ImdbBadge.defaultProps = { imdbId: undefined };

View File

@ -1,11 +1,11 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
export const ShowMore = ({ children, id, inLibrary }) => { export const ShowMore = ({ children, id, inLibrary }) => {
const [display, setDisplay] = useState(!inLibrary); const [ display, setDisplay ] = useState(!inLibrary)
useEffect(() => { useEffect(() => {
setDisplay(!inLibrary); setDisplay(!inLibrary);
}, [id, inLibrary]); }, [id, inLibrary]);
if (!display) { if (!display) {
@ -18,18 +18,21 @@ export const ShowMore = ({ children, id, inLibrary }) => {
More options ... More options ...
</a> </a>
</span> </span>
); )
} }
return <React.Fragment>{children}</React.Fragment>; return (<React.Fragment>{children}</React.Fragment>)
}; }
ShowMore.propTypes = { ShowMore.propTypes = {
id: PropTypes.string, id: PropTypes.string,
inLibrary: PropTypes.bool.isRequired, inLibrary: PropTypes.bool.isRequired,
children: PropTypes.oneOf(PropTypes.object, PropTypes.array) children: PropTypes.oneOf(
}; PropTypes.object,
PropTypes.array,
),
}
ShowMore.defaultProps = { ShowMore.defaultProps = {
id: "", id: "",
inLibrary: false inLibrary: false,
}; }

View File

@ -1,72 +1,73 @@
import React, { useState } from "react"; import React, { useState } from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { List } from "immutable"; import { List } from "immutable"
import Dropdown from "react-bootstrap/Dropdown"; import Dropdown from "react-bootstrap/Dropdown"
export const SubtitlesButton = ({ export const SubtitlesButton = ({
subtitles, subtitles,
inLibrary, inLibrary,
searching, searching,
search search,
}) => { }) => {
if (inLibrary === false) { if (inLibrary === false) { return null }
return null;
}
/* eslint-disable */ /* eslint-disable */
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
/* eslint-enable */ /* eslint-enable */
const onSelect = eventKey => { const onSelect = (eventKey) => {
if (eventKey === null || eventKey != 1) { if (eventKey === null || eventKey != 1) {
setShow(false); setShow(false);
} }
}; }
const onToggle = (isOpen, event, metadata) => { const onToggle = (isOpen, event, metadata) => {
// Don't close on select // Don't close on select
if (metadata && metadata.source !== "select") { if (metadata && metadata.source !== "select") {
setShow(isOpen); setShow(isOpen);
} }
}; }
const count = subtitles && subtitles.size !== 0 ? subtitles.size : 0; const count = (subtitles && subtitles.size !== 0) ? subtitles.size : 0;
return ( return (
<span className="mr-1 mb-1"> <span className="mr-1 mb-1">
<Dropdown drop="up" show={show} onToggle={onToggle} onSelect={onSelect}> <Dropdown drop="up" show={show} onToggle={onToggle} onSelect={onSelect}>
<Dropdown.Toggle variant="secondary" bsPrefix="btn-sm w-md-100"> <Dropdown.Toggle variant="secondary" bsPrefix="btn-sm w-md-100">
<i className="fa fa-commenting mr-1" /> <i className="fa fa-commenting mr-1" />
Subtitles Subtitles
<span className="ml-1 badge badge-pill badge-info">{count}</span> <span className="ml-1 badge badge-pill badge-info">
{count}
</span>
</Dropdown.Toggle> </Dropdown.Toggle>
<Dropdown.Menu> <Dropdown.Menu>
<Dropdown.Item eventKey={1} onClick={search}> <Dropdown.Item eventKey={1} onClick={search} >
<i className={`fa ${searching ? "fa-spin" : ""} fa-refresh mr-1`} /> <i className={`fa ${ searching ? "fa-spin" : "" } fa-refresh mr-1`} />
Automatic search Automatic search
</Dropdown.Item> </Dropdown.Item>
{count > 0 && (
<React.Fragment>
<Dropdown.Divider />
<Dropdown.Header>
<span className="text-warning">Available subtitles</span>
</Dropdown.Header>
</React.Fragment>
)}
{count > 0 && {count > 0 &&
subtitles.toIndexedSeq().map((subtitle, index) => ( <React.Fragment>
<Dropdown.Item href={subtitle.get("url")} key={index}> <Dropdown.Divider />
{subtitle.get("language").split("_")[1]} <Dropdown.Header>
</Dropdown.Item> <span className="text-warning">
))} Available subtitles
</span>
</Dropdown.Header>
</React.Fragment>
}
{count > 0 && subtitles.toIndexedSeq().map((subtitle, index) => (
<Dropdown.Item href={subtitle.get("url")} key={index}>
{subtitle.get("language").split("_")[1]}
</Dropdown.Item>
))}
</Dropdown.Menu> </Dropdown.Menu>
</Dropdown> </Dropdown>
</span> </span>
); );
}; }
SubtitlesButton.propTypes = { SubtitlesButton.propTypes = {
subtitles: PropTypes.instanceOf(List), subtitles: PropTypes.instanceOf(List),
inLibrary: PropTypes.bool.isRequired, inLibrary: PropTypes.bool.isRequired,
searching: PropTypes.bool.isRequired, searching: PropTypes.bool.isRequired,
search: PropTypes.func.isRequired search: PropTypes.func.isRequired,
}; }

View File

@ -1,19 +1,19 @@
import React, { useState } from "react"; import React, { useState } from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { List } from "immutable"; import { List } from "immutable"
import { connect } from "react-redux"; import { connect } from "react-redux"
import { prettySize } from "../../utils"; import { prettySize } from "../../utils"
import { addTorrent } from "../../actions/torrents"; import { addTorrent } from "../../actions/torrents"
import Dropdown from "react-bootstrap/Dropdown"; import Dropdown from "react-bootstrap/Dropdown"
function buildMenuItems(torrents) { function buildMenuItems(torrents) {
if (!torrents || torrents.size === 0) { if (!torrents || torrents.size === 0) {
return []; return [];
} }
const t = torrents.groupBy(el => el.get("source")); const t = torrents.groupBy((el) => el.get("source"));
// Build the array of entries // Build the array of entries
let entries = []; let entries = [];
@ -22,7 +22,7 @@ function buildMenuItems(torrents) {
// Push the title // Push the title
entries.push({ entries.push({
type: "header", type: "header",
value: source value: source,
}); });
// Push the torrents // Push the torrents
@ -31,7 +31,7 @@ function buildMenuItems(torrents) {
type: "entry", type: "entry",
quality: torrent.get("quality"), quality: torrent.get("quality"),
url: torrent.get("url"), url: torrent.get("url"),
size: torrent.get("size") size: torrent.get("size"),
}); });
} }
@ -45,26 +45,32 @@ function buildMenuItems(torrents) {
return entries; return entries;
} }
const torrentsButton = ({ torrents, search, searching, addTorrent, url }) => { const torrentsButton = ({
torrents,
search,
searching,
addTorrent,
url,
}) => {
/* eslint-disable */ /* eslint-disable */
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
/* eslint-enable */ /* eslint-enable */
const entries = buildMenuItems(torrents); const entries = buildMenuItems(torrents);
const count = torrents && torrents.size !== 0 ? torrents.size : 0; const count = (torrents && torrents.size !== 0) ? torrents.size : 0;
const onSelect = eventKey => { const onSelect = (eventKey) => {
// Close the dropdown if the eventkey is not specified // Close the dropdown if the eventkey is not specified
if (eventKey === null) { if (eventKey === null) {
setShow(false); setShow(false);
} }
}; }
const onToggle = (isOpen, event, metadata) => { const onToggle = (isOpen, event, metadata) => {
// Don't close on select // Don't close on select
if (metadata && metadata.source !== "select") { if (metadata && metadata.source !== "select") {
setShow(isOpen); setShow(isOpen);
} }
}; }
return ( return (
<span className="mr-1 mb-1"> <span className="mr-1 mb-1">
@ -72,19 +78,23 @@ const torrentsButton = ({ torrents, search, searching, addTorrent, url }) => {
<Dropdown.Toggle variant="secondary" bsPrefix="btn-sm w-md-100"> <Dropdown.Toggle variant="secondary" bsPrefix="btn-sm w-md-100">
<i className="fa fa-magnet mr-1" /> <i className="fa fa-magnet mr-1" />
Torrents Torrents
<span className="ml-1 badge badge-pill badge-info">{count}</span> <span className="ml-1 badge badge-pill badge-info">
{count}
</span>
</Dropdown.Toggle> </Dropdown.Toggle>
<Dropdown.Menu> <Dropdown.Menu>
<Dropdown.Item eventKey={1} onClick={search}> <Dropdown.Item eventKey={1} onClick={search} >
<i className={`fa ${searching ? "fa-spin" : ""} fa-refresh mr-1`} /> <i className={`fa ${ searching ? "fa-spin" : "" } fa-refresh mr-1`} />
Automatic search Automatic search
</Dropdown.Item> </Dropdown.Item>
<Dropdown.Item href={url}> <Dropdown.Item href={url} >
<i className="fa fa-search mr-1" /> <i className="fa fa-search mr-1" />
Manual search Manual search
</Dropdown.Item> </Dropdown.Item>
{entries.length > 0 && <Dropdown.Divider />} {entries.length > 0 &&
<Dropdown.Divider />
}
{entries.map((e, index) => { {entries.map((e, index) => {
switch (e.type) { switch (e.type) {
case "header": case "header":
@ -94,14 +104,16 @@ const torrentsButton = ({ torrents, search, searching, addTorrent, url }) => {
</Dropdown.Header> </Dropdown.Header>
); );
case "divider": case "divider":
return <Dropdown.Divider key={index} />; return (
<Dropdown.Divider key={index}/>
);
case "entry": case "entry":
return ( return (
<Dropdown.Item key={index} onClick={() => addTorrent(e.url)}> <Dropdown.Item key={index} onClick={() => addTorrent(e.url)}>
{e.quality} {e.quality}
{e.size !== 0 && ( {e.size !== 0 &&
<small className="ml-1">({prettySize(e.size)})</small> <small className="ml-1">({prettySize(e.size)})</small>
)} }
</Dropdown.Item> </Dropdown.Item>
); );
} }
@ -110,16 +122,16 @@ const torrentsButton = ({ torrents, search, searching, addTorrent, url }) => {
</Dropdown> </Dropdown>
</span> </span>
); );
}; }
torrentsButton.propTypes = { torrentsButton.propTypes = {
torrents: PropTypes.instanceOf(List), torrents: PropTypes.instanceOf(List),
searching: PropTypes.bool, searching: PropTypes.bool,
search: PropTypes.func.isRequired, search: PropTypes.func.isRequired,
addTorrent: PropTypes.func.isRequired, addTorrent: PropTypes.func.isRequired,
url: PropTypes.string url: PropTypes.string,
}; }
torrentsButton.defaultProps = { torrentsButton.defaultProps = {
torrents: List() torrents: List(),
}; }
export const TorrentsButton = connect(null, { addTorrent })(torrentsButton); export const TorrentsButton = connect(null, {addTorrent})(torrentsButton);

View File

@ -1,5 +1,5 @@
import React from "react"; import React from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
export const WishlistButton = ({ wishlisted, wishlist }) => { export const WishlistButton = ({ wishlisted, wishlist }) => {
return ( return (
@ -10,8 +10,8 @@ export const WishlistButton = ({ wishlisted, wishlist }) => {
/> />
</span> </span>
); );
}; }
WishlistButton.propTypes = { WishlistButton.propTypes = {
wishlisted: PropTypes.bool.isRequired, wishlisted: PropTypes.bool.isRequired,
wishlist: PropTypes.func.isRequired wishlist: PropTypes.func.isRequired,
}; }

View File

@ -1,17 +1,14 @@
import React from "react"; import React from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { List } from "immutable"; import { List } from "immutable"
export const Genres = ({ genres }) => { export const Genres = ({ genres }) => {
if (genres.size === 0) { if (genres.size === 0) { return null }
return null;
}
// Uppercase first genres // Uppercase first genres
const prettyGenres = genres const prettyGenres = genres.toJS().map(
.toJS() (word) => word[0].toUpperCase() + word.substr(1)
.map(word => word[0].toUpperCase() + word.substr(1)) ).join(", ");
.join(", ");
return ( return (
<span> <span>
@ -19,6 +16,6 @@ export const Genres = ({ genres }) => {
{prettyGenres} {prettyGenres}
</span> </span>
); );
}; }
Genres.propTypes = { genres: PropTypes.instanceOf(List) }; Genres.propTypes = { genres: PropTypes.instanceOf(List) };
Genres.defaultProps = { genres: List() }; Genres.defaultProps = { genres: List() };

View File

@ -1,12 +1,12 @@
import React from "react"; import React from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
export const Plot = ({ plot }) => { export const Plot = ({ plot }) => {
if (plot === "") { if (plot === "") { return null }
return null;
}
return <span className="text text-break plot">{plot}</span>; return (
}; <span className="text text-break plot">{plot}</span>
);
}
Plot.propTypes = { plot: PropTypes.string }; Plot.propTypes = { plot: PropTypes.string };
Plot.defaultProps = { plot: "" }; Plot.defaultProps = { plot: "" };

View File

@ -1,20 +1,26 @@
import React from "react"; import React from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
export const PolochonMetadata = ({ export const PolochonMetadata = ({
quality, quality,
container, container,
videoCodec, videoCodec,
audioCodec, audioCodec,
releaseGroup releaseGroup,
}) => { }) => {
if (!quality || quality === "") { if (!quality || quality === "") {
return null; return null;
} }
const metadata = [quality, container, videoCodec, audioCodec, releaseGroup] const metadata = [
.filter(m => m && m !== "") quality,
.join(", "); container,
videoCodec,
audioCodec,
releaseGroup,
].
filter(m => (m && m !== "")).
join(", ")
return ( return (
<span> <span>
@ -22,11 +28,11 @@ export const PolochonMetadata = ({
{metadata} {metadata}
</span> </span>
); );
}; }
PolochonMetadata.propTypes = { PolochonMetadata.propTypes = {
quality: PropTypes.string, quality: PropTypes.string,
container: PropTypes.string, container: PropTypes.string,
videoCodec: PropTypes.string, videoCodec: PropTypes.string,
audioCodec: PropTypes.string, audioCodec: PropTypes.string,
releaseGroup: PropTypes.string releaseGroup: PropTypes.string,
}; };

View File

@ -1,24 +1,24 @@
import React from "react"; import React from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
export const Rating = ({ rating, votes }) => { export const Rating = ({ rating, votes }) => {
if (rating === 0) { if (rating === 0) { return null; }
return null;
}
return ( return (
<span> <span>
<i className="fa fa-star mr-1"></i> <i className="fa fa-star mr-1"></i>
{Number(rating).toFixed(1)} {Number(rating).toFixed(1)}
{votes !== 0 && <small className="ml-1">({votes} votes)</small>} {votes !== 0 &&
<small className="ml-1">({votes} votes)</small>
}
</span> </span>
); );
}; }
Rating.propTypes = { Rating.propTypes = {
rating: PropTypes.number, rating: PropTypes.number,
votes: PropTypes.number votes: PropTypes.number,
}; };
Rating.defaultProps = { Rating.defaultProps = {
rating: 0, rating: 0,
votes: 0 votes: 0,
}; };

View File

@ -1,42 +1,35 @@
import React from "react"; import React from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import moment from "moment"; import moment from "moment"
const prettyDate = input => { const prettyDate = (input) => {
switch (typeof input) { switch (typeof input) {
case "string": case "string":
if (input === "") { if (input === "") { return "" }
return "";
}
break; break;
case "number": case "number":
if (input === 0) { if (input === 0) { return "" }
return ""; return input
}
return input;
default: default:
return input; return input;
} }
const date = moment(input); const date = moment(input);
if (!date.isValid()) { if (!date.isValid()) { return "" }
return "";
}
let output = date.format("DD/MM/YYYY"); let output = date.format("DD/MM/YYYY");
if (date > moment().subtract(1, "month") && date < moment().add(1, "month")) { if ((date > moment().subtract(1, "month"))
output += " (" + date.fromNow() + ")"; && (date < moment().add(1, "month"))) {
output += " (" + date.fromNow() + ")"
} }
return output; return output
}; }
export const ReleaseDate = ({ date }) => { export const ReleaseDate = ({ date }) => {
const formattedDate = prettyDate(date); const formattedDate = prettyDate(date);
if (formattedDate === "") { if (formattedDate === "") { return null }
return null;
}
return ( return (
<span> <span>
@ -44,8 +37,11 @@ export const ReleaseDate = ({ date }) => {
{formattedDate} {formattedDate}
</span> </span>
); );
}; }
ReleaseDate.propTypes = { ReleaseDate.propTypes = {
date: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) date: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
}; };
ReleaseDate.defaultProps = { date: "" }; ReleaseDate.defaultProps = { date: "" };

View File

@ -1,12 +1,10 @@
import React from "react"; import React from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { prettyDurationFromMinutes } from "../../utils"; import { prettyDurationFromMinutes } from "../../utils"
export const Runtime = ({ runtime }) => { export const Runtime = ({ runtime }) => {
if (runtime === 0) { if (runtime === 0) { return null; }
return null;
}
return ( return (
<span> <span>
@ -14,6 +12,6 @@ export const Runtime = ({ runtime }) => {
&nbsp;{prettyDurationFromMinutes(runtime)} &nbsp;{prettyDurationFromMinutes(runtime)}
</span> </span>
); );
}; }
Runtime.propTypes = { runtime: PropTypes.number }; Runtime.propTypes = { runtime: PropTypes.number };
Runtime.defaultProps = { runtime: 0 }; Runtime.defaultProps = { runtime: 0 };

View File

@ -1,12 +1,10 @@
import React from "react"; import React from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { WishlistButton } from "../buttons/wishlist"; import { WishlistButton } from "../buttons/wishlist"
export const Title = ({ title, wishlist, wishlisted }) => { export const Title = ({ title, wishlist, wishlisted }) => {
if (title === "") { if (title === "") { return null; }
return null;
}
return ( return (
<span className="title"> <span className="title">
@ -14,12 +12,12 @@ export const Title = ({ title, wishlist, wishlisted }) => {
{title} {title}
</span> </span>
); );
}; }
Title.propTypes = { Title.propTypes = {
title: PropTypes.string, title: PropTypes.string,
wishlist: PropTypes.func, wishlist: PropTypes.func,
wishlisted: PropTypes.bool wishlisted: PropTypes.bool,
}; };
Title.defaultProps = { Title.defaultProps = {
title: "" title: "",
}; };

View File

@ -1,12 +1,7 @@
import React from "react"; import React from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
export const TrackingLabel = ({ export const TrackingLabel = ({ wishlisted, inLibrary, trackedSeason, trackedEpisode }) => {
wishlisted,
inLibrary,
trackedSeason,
trackedEpisode
}) => {
if (wishlisted === false) { if (wishlisted === false) {
return null; return null;
} }
@ -19,14 +14,14 @@ export const TrackingLabel = ({
return null; return null;
} }
let str = ""; let str = ""
if (trackedSeason === 0 && trackedEpisode === 0) { if (trackedSeason === 0 && trackedEpisode === 0) {
str = "All the episodes will be downloaded automatically"; str = "All the episodes will be downloaded automatically";
} else if (trackedSeason > 0 && trackedEpisode > 0) { } else if (trackedSeason > 0 && trackedEpisode > 0) {
str = `All the episodes will be downloaded automatically starting from str = `All the episodes will be downloaded automatically starting from
season ${trackedSeason} episode ${trackedEpisode}`; season ${trackedSeason} episode ${trackedEpisode}`;
} else { } else {
str = "This movie will be downloaded automatically"; str = "This movie will be downloaded automatically"
} }
return ( return (
@ -36,11 +31,11 @@ export const TrackingLabel = ({
{str} {str}
</small> </small>
</span> </span>
); )
}; }
TrackingLabel.propTypes = { TrackingLabel.propTypes = {
wishlisted: PropTypes.bool, wishlisted: PropTypes.bool,
inLibrary: PropTypes.bool, inLibrary: PropTypes.bool,
trackedSeason: PropTypes.number, trackedSeason: PropTypes.number,
trackedEpisode: PropTypes.number trackedEpisode: PropTypes.number,
}; };

View File

@ -1,20 +1,22 @@
import React from "react"; import React from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
export const FormInput = ({ label, value, updateValue }) => { export const FormInput = ({ label, value, updateValue }) => {
return ( return (
<div className="form-group"> <div className="form-group">
<label className="control-label">{label}</label> <label className="control-label">
{label}
</label>
<input <input
className="form-control" className="form-control"
value={value} value={value}
onChange={e => updateValue(e.target.value)} onChange={(e) => updateValue(e.target.value)}
/> />
</div> </div>
); )
}; }
FormInput.propTypes = { FormInput.propTypes = {
label: PropTypes.string, label: PropTypes.string,
value: PropTypes.string, value: PropTypes.string,
updateValue: PropTypes.func updateValue: PropTypes.func,
}; };

View File

@ -1,7 +1,7 @@
import React from "react"; import React from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { Modal } from "react-bootstrap"; import { Modal } from "react-bootstrap"
export const FormModal = ({ export const FormModal = ({
show, show,
@ -9,44 +9,41 @@ export const FormModal = ({
title, title,
icon, icon,
handleSubmit, handleSubmit,
children children,
}) => { }) => {
const submit = function(e) { const submit = function(e) {
if (e) { if (e) { e.preventDefault(); }
e.preventDefault();
}
handleSubmit(); handleSubmit();
}; };
return ( return (
<Modal show={show} onHide={() => setShow(false)}> <Modal show={show} onHide={() => setShow(false)}>
<Modal.Header closeButton> <Modal.Header closeButton>
<Modal.Title> <Modal.Title>
<i className={`fa fa-${icon} mr-1`} /> <i className={`fa fa-${icon} mr-1`} />
{title} {title}
</Modal.Title> </Modal.Title>
</Modal.Header> </Modal.Header>
<Modal.Body bsPrefix="modal-body"> <Modal.Body bsPrefix="modal-body">
<form className="form-horizontal" onSubmit={ev => submit(ev)}> <form className="form-horizontal" onSubmit={(ev) => submit(ev)}>
{children} {children}
</form> </form>
</Modal.Body> </Modal.Body>
<Modal.Footer> <Modal.Footer>
<div className="btn btn-success" onClick={submit}> <div className="btn btn-success" onClick={submit}>Apply</div>
Apply <div className="btn btn-danger" onClick={() => setShow(false)}>Close</div>
</div> </Modal.Footer>
<div className="btn btn-danger" onClick={() => setShow(false)}> </Modal>
Close )
</div> }
</Modal.Footer>
</Modal>
);
};
FormModal.propTypes = { FormModal.propTypes = {
show: PropTypes.bool, show: PropTypes.bool,
setShow: PropTypes.func, setShow: PropTypes.func,
icon: PropTypes.string, icon: PropTypes.string,
title: PropTypes.string, title: PropTypes.string,
handleSubmit: PropTypes.func, handleSubmit: PropTypes.func,
children: PropTypes.oneOf(PropTypes.object, PropTypes.array) children: PropTypes.oneOf(
PropTypes.object,
PropTypes.array,
),
}; };

View File

@ -1,28 +1,24 @@
import React from "react"; import React from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { Map } from "immutable"; import { Map } from "immutable"
import { inLibrary, isWishlisted } from "../../utils"; import { inLibrary, isWishlisted } from "../../utils"
import { DownloadAndStream } from "../buttons/download"; import { DownloadAndStream } from "../buttons/download"
import { ImdbBadge } from "../buttons/imdb"; import { ImdbBadge } from "../buttons/imdb"
import { TrackingLabel } from "../details/tracking"; import { TrackingLabel } from "../details/tracking"
import { Genres } from "../details/genres"; import { Genres } from "../details/genres"
import { Plot } from "../details/plot"; import { Plot } from "../details/plot"
import { PolochonMetadata } from "../details/polochon"; import { PolochonMetadata } from "../details/polochon"
import { Rating } from "../details/rating"; import { Rating } from "../details/rating"
import { ReleaseDate } from "../details/releaseDate"; import { ReleaseDate } from "../details/releaseDate"
import { Runtime } from "../details/runtime"; import { Runtime } from "../details/runtime"
import { Title } from "../details/title"; import { Title } from "../details/title"
const ListDetails = props => { const ListDetails = (props) => {
if (props.data === undefined) { if (props.data === undefined) { return null }
return null; if (props.loading) { return null }
}
if (props.loading) {
return null;
}
return ( return (
<div className="col-8 col-md-4 list-details pl-1 d-flex align-items-start flex-column"> <div className="col-8 col-md-4 list-details pl-1 d-flex align-items-start flex-column">
@ -65,11 +61,11 @@ const ListDetails = props => {
</div> </div>
</div> </div>
); );
}; }
ListDetails.propTypes = { ListDetails.propTypes = {
data: PropTypes.instanceOf(Map), data: PropTypes.instanceOf(Map),
wishlist: PropTypes.func, wishlist: PropTypes.func,
loading: PropTypes.bool, loading: PropTypes.bool,
children: PropTypes.object children: PropTypes.object,
}; };
export default ListDetails; export default ListDetails;

View File

@ -1,108 +1,101 @@
import React from "react"; import React from "react"
import PropTypes from "prop-types"; import { withRouter } from "react-router-dom"
import { withRouter } from "react-router-dom"; import { Form, FormGroup, FormControl, FormLabel } from "react-bootstrap"
import { Form, FormGroup, FormControl, FormLabel } from "react-bootstrap";
const ExplorerOptions = ({ display, params, options, type, history }) => { class ExplorerOptions extends React.PureComponent {
// Should this componennt be displayed constructor(props) {
if (!display) { super(props);
return null; this.handleSourceChange = this.handleSourceChange.bind(this);
this.handleCategoryChange = this.handleCategoryChange.bind(this);
} }
handleSourceChange(event) {
if (
!params ||
!params.source ||
!params.category ||
params.source === "" ||
params.category === ""
) {
return null;
}
const handleSourceChange = event => {
let source = event.target.value; let source = event.target.value;
let category = options.get(event.target.value).first(); let category = this.props.options.get(event.target.value).first();
history.push(`/${type}/explore/${source}/${category}`); this.props.history.push(`/${this.props.type}/explore/${source}/${category}`);
};
const handleCategoryChange = event => {
let source = params.source;
let category = event.target.value;
history.push(`/${type}/explore/${source}/${category}`);
};
const prettyName = name => {
return name
.replace("_", " ")
.split(" ")
.map(w => w[0].toUpperCase() + w.substr(1))
.join(" ");
};
// Options are not yet fetched
if (options.size === 0) {
return null;
} }
handleCategoryChange(event) {
let source = this.props.params.source;
let category = event.target.value;
this.props.history.push(`/${this.props.type}/explore/${source}/${category}`);
}
propsValid(props) {
if (!props.params
|| !props.params.source
|| !props.params.category
|| (props.params.source === "")
|| (props.params.category === "")) {
return false
}
return true;
}
prettyName(name) {
return name.replace("_", " ")
.split(" ")
.map((w) => w[0].toUpperCase() + w.substr(1))
.join(" ");
}
render() {
// Should this componennt be displayed
if (!this.props.display) {
return null;
}
let source = params.source; // Options are not yet fetched
let category = params.category; if (this.props.options.size === 0) {
let categories = options.get(params.source); return null;
}
return ( // Invalid props
<div className="row"> if (!this.propsValid(this.props)) {
<div className="col-xs-12 col-md-12"> return
<Form> }
<div className="row">
<div className="col-xs-12 col-md-6"> let source = this.props.params.source;
<FormGroup> let category = this.props.params.category;
<FormLabel>Source</FormLabel> let categories = this.props.options.get(this.props.params.source);
<FormControl
bsPrefix="form-control input-sm" return (
as="select" <div className="row">
onChange={handleSourceChange} <div className="col-xs-12 col-md-12">
value={source} <Form>
> <div className="row">
{options.keySeq().map(function(source) { <div className="col-xs-12 col-md-6">
return ( <FormGroup>
<option key={source} value={source}> <FormLabel>Source</FormLabel>
{prettyName(source)} <FormControl
</option> bsPrefix="form-control input-sm"
); as="select"
})} onChange={this.handleSourceChange}
</FormControl> value={source}
</FormGroup> >
{this.props.options.keySeq().map(function(source) {
return (
<option key={source} value={source}>{this.prettyName(source)}</option>)
}, this)}
</FormControl>
</FormGroup>
</div>
<div className="col-xs-12 col-md-6">
<FormGroup>
<FormLabel>Category</FormLabel>
<FormControl
bsPrefix="form-control input-sm"
as="select"
onChange={this.handleCategoryChange}
value={category}
>
{categories.map(function(category) {
return (<option key={category} value={category}>{this.prettyName(category)}</option>)
}, this)}
</FormControl>
</FormGroup>
</div>
</div> </div>
<div className="col-xs-12 col-md-6"> </Form>
<FormGroup> </div>
<FormLabel>Category</FormLabel>
<FormControl
bsPrefix="form-control input-sm"
as="select"
onChange={handleCategoryChange}
value={category}
>
{categories.map(function(category) {
return (
<option key={category} value={category}>
{prettyName(category)}
</option>
);
})}
</FormControl>
</FormGroup>
</div>
</div>
</Form>
</div> </div>
</div> );
); }
}; }
ExplorerOptions.propTypes = {
params: PropTypes.object,
history: PropTypes.object,
type: PropTypes.string,
options: PropTypes.object,
display: PropTypes.bool
};
export default withRouter(ExplorerOptions); export default withRouter(ExplorerOptions);

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
const ListFilter = ({ placeHolder, updateFilter }) => { const ListFilter = ({ placeHolder, updateFilter }) => {
const [filter, setFilter] = useState(""); const [filter, setFilter] = useState("");
@ -13,21 +13,18 @@ const ListFilter = ({ placeHolder, updateFilter }) => {
return ( return (
<div className="input-group input-group-sm"> <div className="input-group input-group-sm">
<input <input type="text" className="form-control"
type="text"
className="form-control"
placeholder={placeHolder} placeholder={placeHolder}
onChange={e => setFilter(e.target.value)} onChange={(e) => setFilter(e.target.value)}
value={filter} value={filter} />
/>
<div className="input-group-append d-none d-md-block"> <div className="input-group-append d-none d-md-block">
<span className="input-group-text">Filter</span> <span className="input-group-text">Filter</span>
</div> </div>
</div> </div>
); );
}; }
ListFilter.propTypes = { ListFilter.propTypes = {
updateFilter: PropTypes.func.isRequired, updateFilter: PropTypes.func.isRequired,
placeHolder: PropTypes.string.isRequired placeHolder: PropTypes.string.isRequired,
}; };
export default ListFilter; export default ListFilter;

View File

@ -1,13 +1,11 @@
import React from "react"; import React from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import EmptyImg from "../../../img/noimage.png"; import EmptyImg from "../../../img/noimage.png"
const Poster = ({ url, selected, onClick, onDoubleClick }) => { const Poster = ({ url, selected, onClick, onDoubleClick }) => {
const className = selected const className = selected ? "border-primary thumbnail-selected" : "border-secondary";
? "border-primary thumbnail-selected" const src = (url === "") ? EmptyImg : url;
: "border-secondary";
const src = url === "" ? EmptyImg : url;
return ( return (
<img <img
@ -17,12 +15,12 @@ const Poster = ({ url, selected, onClick, onDoubleClick }) => {
className={`my-1 m-md-2 img-thumbnail object-fit-cover ${className}`} className={`my-1 m-md-2 img-thumbnail object-fit-cover ${className}`}
/> />
); );
}; }
Poster.propTypes = { Poster.propTypes = {
url: PropTypes.string, url: PropTypes.string,
selected: PropTypes.bool.isRequired, selected: PropTypes.bool.isRequired,
onClick: PropTypes.func, onClick: PropTypes.func,
onDoubleClick: PropTypes.func onDoubleClick: PropTypes.func,
}; };
export default Poster; export default Poster;

View File

@ -1,18 +1,18 @@
import React, { useState, useEffect, useCallback } from "react"; import React, { useState, useEffect, useCallback } from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { OrderedMap, Map } from "immutable"; import { OrderedMap, Map } from "immutable"
import fuzzy from "fuzzy"; import fuzzy from "fuzzy";
import InfiniteScroll from "react-infinite-scroll-component"; import InfiniteScroll from "react-infinite-scroll-component";
import ListFilter from "./filter"; import ListFilter from "./filter"
import ExplorerOptions from "./explorerOptions"; import ExplorerOptions from "./explorerOptions"
import Poster from "./poster"; import Poster from "./poster"
import Loader from "../loader/loader"; import Loader from "../loader/loader"
const ListPosters = props => { const ListPosters = (props) => {
if (props.loading) { if (props.loading) {
return <Loader />; return (<Loader />);
} }
let elmts = props.data; let elmts = props.data;
@ -20,24 +20,23 @@ const ListPosters = props => {
// Filter the list of elements // Filter the list of elements
if (props.filter !== "") { if (props.filter !== "") {
elmts = elmts.filter(v => fuzzy.test(props.filter, v.get("title"))); elmts = elmts.filter((v) => fuzzy.test(props.filter, v.get("title")));
} else { } else {
elmts = elmts.slice(0, listSize.items); elmts = elmts.slice(0, listSize.items);
} }
// Chose when to display filter / explore options // Chose when to display filter / explore options
let displayFilter = true; let displayFilter = true;
if ( if ((props.params
(props.params && && props.params.category
props.params.category && && props.params.category !== ""
props.params.category !== "" && && props.params.source
props.params.source && && props.params.source !== "")
props.params.source !== "") || || (listSize === 0)) {
listSize === 0
) {
displayFilter = false; displayFilter = false;
} }
let displayExplorerOptions = false; let displayExplorerOptions = false;
if (listSize !== 0) { if (listSize !== 0) {
displayExplorerOptions = !displayFilter; displayExplorerOptions = !displayFilter;
@ -45,12 +44,12 @@ const ListPosters = props => {
return ( return (
<div className="col-4 col-md-8 px-1"> <div className="col-4 col-md-8 px-1">
{displayFilter && ( {displayFilter &&
<ListFilter <ListFilter
updateFilter={props.updateFilter} updateFilter={props.updateFilter}
placeHolder={props.placeHolder} placeHolder={props.placeHolder}
/> />
)} }
<ExplorerOptions <ExplorerOptions
type={props.type} type={props.type}
display={displayExplorerOptions} display={displayExplorerOptions}
@ -67,7 +66,7 @@ const ListPosters = props => {
/> />
</div> </div>
); );
}; }
ListPosters.propTypes = { ListPosters.propTypes = {
data: PropTypes.instanceOf(OrderedMap), data: PropTypes.instanceOf(OrderedMap),
onClick: PropTypes.func, onClick: PropTypes.func,
@ -80,39 +79,34 @@ ListPosters.propTypes = {
type: PropTypes.string.isRequired, type: PropTypes.string.isRequired,
placeHolder: PropTypes.string.isRequired, placeHolder: PropTypes.string.isRequired,
updateFilter: PropTypes.func.isRequired, updateFilter: PropTypes.func.isRequired,
filter: PropTypes.string filter: PropTypes.string,
}; };
export default ListPosters; export default ListPosters;
const Posters = props => { const Posters = (props) => {
const addMoreCount = 20; const addMoreCount = 20;
const [size, setSize] = useState(0); const [size, setSize] = useState(0);
const [postersPerRow, setPostersPerRow] = useState(0); const [postersPerRow, setPostersPerRow] = useState(0);
const [posterHeight, setPosterHeight] = useState(0); const [posterHeight, setPosterHeight] = useState(0);
const loadMore = () => { const loadMore = () => {
if (size === props.elmts.size) { if ((size === props.elmts.size)) { return }
return;
}
const newSize = const newSize = (((size + addMoreCount) >= props.elmts.size)
size + addMoreCount >= props.elmts.size ? props.elmts.size
? props.elmts.size : size + addMoreCount);
: size + addMoreCount;
setSize(newSize); setSize(newSize);
}; }
useEffect(() => { useEffect(() => {
loadMore(); loadMore()
}, [props.elmts.size]); }, [props.elmts.size]);
const move = event => { const move = (event) => {
// Only run the function if nothing else if actively focused // Only run the function if nothing else if actively focused
if (document.activeElement.tagName.toLowerCase() !== "body") { if (document.activeElement.tagName.toLowerCase() !== "body") { return }
return;
}
let diff = 0; let diff = 0;
let moveFocus = 0; let moveFocus = 0;
@ -127,11 +121,11 @@ const Posters = props => {
diff = -1; diff = -1;
break; break;
case "k": case "k":
diff = -1 * postersPerRow; diff = -1*postersPerRow;
moveFocus = -1 * posterHeight; moveFocus = -1*posterHeight;
break; break;
case "j": case "j":
diff = postersPerRow; diff = postersPerRow;
moveFocus = posterHeight; moveFocus = posterHeight;
break; break;
default: default:
@ -144,8 +138,8 @@ const Posters = props => {
var newIdx = idx + diff; var newIdx = idx + diff;
// Handle edge cases // Handle edge cases
if (newIdx > props.elmts.size - 1) { if (newIdx > props.elmts.size -1) {
newIdx = props.elmts.size - 1; newIdx = props.elmts.size -1;
} else if (newIdx < 0) { } else if (newIdx < 0) {
newIdx = 0; newIdx = 0;
} }
@ -158,38 +152,33 @@ const Posters = props => {
if (moveFocus !== 0) { if (moveFocus !== 0) {
window.scrollBy(0, moveFocus); window.scrollBy(0, moveFocus);
} }
}; }
const posterCount = useCallback(node => { const posterCount = useCallback(node => {
if (node === null) { if (node === null) { return }
return;
}
const parentWidth = node.getBoundingClientRect().width; const parentWidth = node.getBoundingClientRect().width;
const childContainer = node.getElementsByClassName("img-thumbnail"); const childContainer = node.getElementsByClassName("img-thumbnail");
let childWidth = 0; let childWidth = 0;
let posterHeight = 0; let posterHeight = 0;
if (childContainer !== null && childContainer.item(0) !== null) { if ((childContainer !== null) && (childContainer.item(0) !== null)) {
const child = childContainer.item(0); const child = childContainer.item(0);
childWidth = childWidth = child.getBoundingClientRect().width + child.getBoundingClientRect().left;
child.getBoundingClientRect().width +
child.getBoundingClientRect().left;
posterHeight = child.getBoundingClientRect().height; posterHeight = child.getBoundingClientRect().height;
} }
let numberPerRow = let numberPerRow = (childWidth >= parentWidth) ? 1 : Math.floor(parentWidth/childWidth);
childWidth >= parentWidth ? 1 : Math.floor(parentWidth / childWidth);
setPostersPerRow(numberPerRow); setPostersPerRow(numberPerRow);
setPosterHeight(posterHeight); setPosterHeight(posterHeight);
}); })
useEffect(() => { useEffect(() => {
document.onkeypress = move; document.onkeypress = move;
return () => { return () => {
document.onkeypress = null; document.onkeypress = null;
}; }
}, [move]); }, [move])
if (props.elmts.size === 0) { if (props.elmts.size === 0) {
return ( return (
<div className="jumbotron"> <div className="jumbotron">
<h2>No result</h2> <h2>No result</h2>
@ -203,35 +192,32 @@ const Posters = props => {
className="poster-list d-flex flex-column flex-sm-row flex-sm-wrap justify-content-around" className="poster-list d-flex flex-column flex-sm-row flex-sm-wrap justify-content-around"
dataLength={size} dataLength={size}
next={loadMore} next={loadMore}
hasMore={size !== props.elmts.size} hasMore={(size !== props.elmts.size)}
loader={<Loader />} loader={<Loader />}
> >
{props.elmts {props.elmts.slice(0, size).toIndexedSeq().map(function(el, index) {
.slice(0, size) const imdbId = el.get("imdb_id");
.toIndexedSeq() const selected = (imdbId === props.selectedImdbId) ? true : false;
.map(function(el, index) {
const imdbId = el.get("imdb_id");
const selected = imdbId === props.selectedImdbId ? true : false;
return ( return (
<Poster <Poster
url={el.get("poster_url")} url={el.get("poster_url")}
key={index} key={index}
selected={selected} selected={selected}
onClick={() => props.selectPoster(imdbId)} onClick={() => props.selectPoster(imdbId)}
onDoubleClick={() => props.onDoubleClick(imdbId)} onDoubleClick={() => props.onDoubleClick(imdbId)}
/> />
); )
}, this)} } ,this)}
</InfiniteScroll> </InfiniteScroll>
</div> </div>
); );
}; }
Posters.propTypes = { Posters.propTypes = {
elmts: PropTypes.instanceOf(OrderedMap), elmts: PropTypes.instanceOf(OrderedMap),
selectedImdbId: PropTypes.string, selectedImdbId: PropTypes.string,
loading: PropTypes.bool.isRequired, loading: PropTypes.bool.isRequired,
onDoubleClick: PropTypes.func, onDoubleClick: PropTypes.func,
onKeyEnter: PropTypes.func, onKeyEnter: PropTypes.func,
selectPoster: PropTypes.func selectPoster: PropTypes.func,
}; };

View File

@ -1,9 +1,14 @@
import React from "react"; import React from "react"
import Loading from "react-loading"; import Loading from "react-loading"
const Loader = () => ( const Loader = () => (
<div className="col-12 col-md-6 offset-md-3"> <div className="col-12 col-md-6 offset-md-3">
<Loading type="bars" height={"100%"} width={"100%"} color="#EBEBEB" /> <Loading
type="bars"
height={"100%"}
width={"100%"}
color="#EBEBEB"
/>
</div> </div>
); );
export default Loader; export default Loader;

View File

@ -1,40 +1,38 @@
import React from "react"; import React from "react"
import Loader from "../loader/loader"; import Loader from "../loader/loader"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { Map, List } from "immutable"; import { Map, List } from "immutable"
// TODO: udpate this // TODO: udpate this
import { OverlayTrigger, Tooltip } from "react-bootstrap"; import { OverlayTrigger, Tooltip } from "react-bootstrap"
const Modules = props => { const Modules = (props) => {
if (props.isLoading) { if (props.isLoading) {
return <Loader />; return <Loader />
} }
return ( return (
<div className="row"> <div className="row">
{props.modules && {props.modules && props.modules.keySeq().map((value, key) => (
props.modules <ModulesByVideoType
.keySeq() key={key}
.map((value, key) => ( videoType={value}
<ModulesByVideoType data={props.modules.get(value)}
key={key} />
videoType={value} ))}
data={props.modules.get(value)}
/>
))}
</div> </div>
); );
}; }
Modules.propTypes = { Modules.propTypes = {
isLoading: PropTypes.bool.isRequired, isLoading: PropTypes.bool.isRequired,
modules: PropTypes.instanceOf(Map) modules: PropTypes.instanceOf(Map),
}; };
export default Modules; export default Modules;
const capitalize = string => string.charAt(0).toUpperCase() + string.slice(1); const capitalize = (string) =>
string.charAt(0).toUpperCase() + string.slice(1);
const ModulesByVideoType = props => ( const ModulesByVideoType = (props) => (
<div className="col-12 col-md-6"> <div className="col-12 col-md-6">
<div className="card mb-3"> <div className="card mb-3">
<div className="card-header"> <div className="card-header">
@ -42,7 +40,11 @@ const ModulesByVideoType = props => (
</div> </div>
<div className="card-body"> <div className="card-body">
{props.data.keySeq().map((value, key) => ( {props.data.keySeq().map((value, key) => (
<ModuleByType key={key} type={value} data={props.data.get(value)} /> <ModuleByType
key={key}
type={value}
data={props.data.get(value)}
/>
))} ))}
</div> </div>
</div> </div>
@ -50,16 +52,22 @@ const ModulesByVideoType = props => (
); );
ModulesByVideoType.propTypes = { ModulesByVideoType.propTypes = {
videoType: PropTypes.string.isRequired, videoType: PropTypes.string.isRequired,
data: PropTypes.instanceOf(Map) data: PropTypes.instanceOf(Map),
}; };
const ModuleByType = props => ( const ModuleByType = (props) => (
<div> <div>
<h4>{capitalize(props.type)}</h4> <h4>{capitalize(props.type)}</h4>
<table className="table"> <table className="table">
<tbody> <tbody>
{props.data.map(function(value, key) { {props.data.map(function(value, key) {
return <Module key={key} type={key} data={value} />; return (
<Module
key={key}
type={key}
data={value}
/>
);
})} })}
</tbody> </tbody>
</table> </table>
@ -67,43 +75,41 @@ const ModuleByType = props => (
); );
ModuleByType.propTypes = { ModuleByType.propTypes = {
type: PropTypes.string.isRequired, type: PropTypes.string.isRequired,
data: PropTypes.instanceOf(List) data: PropTypes.instanceOf(List),
}; };
const Module = props => { const Module = (props) => {
let iconClass, prettyStatus, badgeClass; let iconClass, prettyStatus, badgeClass;
const name = props.data.get("name"); const name = props.data.get("name");
switch (props.data.get("status")) { switch(props.data.get("status")) {
case "ok": case "ok":
iconClass = "fa fa-check-circle"; iconClass = "fa fa-check-circle"
badgeClass = "badge badge-pill badge-success"; badgeClass = "badge badge-pill badge-success"
prettyStatus = "OK"; prettyStatus = "OK"
break; break;
case "fail": case "fail":
iconClass = "fa fa-times-circle"; iconClass = "fa fa-times-circle"
badgeClass = "badge badge-pill badge-danger"; badgeClass = "badge badge-pill badge-danger"
prettyStatus = "Fail"; prettyStatus = "Fail"
break; break;
case "not_implemented": case "not_implemented":
iconClass = "fa fa-question-circle"; iconClass = "fa fa-question-circle"
badgeClass = "badge badge-pill badge-default"; badgeClass = "badge badge-pill badge-default"
prettyStatus = "Not implemented"; prettyStatus = "Not implemented"
break; break;
default: default:
iconClass = "fa fa-question-circle"; iconClass = "fa fa-question-circle"
badgeClass = "badge badge-pill badge-warning"; badgeClass = "badge badge-pill badge-warning"
prettyStatus = "Unknown"; prettyStatus = "Unknown"
} }
const tooltip = ( const tooltip = (
<Tooltip id={`tooltip-status-${name}`}> <Tooltip id={`tooltip-status-${name}`}>
<p> <p><span className={badgeClass}>Status: {prettyStatus}</span></p>
<span className={badgeClass}>Status: {prettyStatus}</span> {props.data.get("error") !== "" &&
</p> <p>Error: {props.data.get("error")}</p>
{props.data.get("error") !== "" && ( }
<p>Error: {props.data.get("error")}</p>
)}
</Tooltip> </Tooltip>
); );
@ -119,7 +125,7 @@ const Module = props => {
</td> </td>
</tr> </tr>
); );
}; }
Module.propTypes = { Module.propTypes = {
data: PropTypes.instanceOf(Map) data: PropTypes.instanceOf(Map),
}; };

View File

@ -1,43 +1,38 @@
import React from "react"; import React from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { OrderedMap, Map } from "immutable"; import { OrderedMap, Map } from "immutable"
import { connect } from "react-redux"; import { connect } from "react-redux"
import { import { selectMovie, updateFilter, movieWishlistToggle } from "../../actions/movies"
selectMovie,
updateFilter,
movieWishlistToggle
} from "../../actions/movies";
import ListDetails from "../list/details"; import ListDetails from "../list/details"
import ListPosters from "../list/posters"; import ListPosters from "../list/posters"
import { inLibrary, isWishlisted } from "../../utils"; import { inLibrary, isWishlisted } from "../../utils"
import { ShowMore } from "../buttons/showMore"; import { ShowMore } from "../buttons/showMore"
import { MovieSubtitlesButton } from "./subtitlesButton"; import { MovieSubtitlesButton } from "./subtitlesButton"
import { MovieTorrentsButton } from "./torrentsButton"; import { MovieTorrentsButton } from "./torrentsButton"
function mapStateToProps(state) { function mapStateToProps(state) {
return { return {
loading: state.movieStore.get("loading"), loading : state.movieStore.get("loading"),
movies: state.movieStore.get("movies"), movies : state.movieStore.get("movies"),
filter: state.movieStore.get("filter"), filter : state.movieStore.get("filter"),
selectedImdbId: state.movieStore.get("selectedImdbId"), selectedImdbId : state.movieStore.get("selectedImdbId"),
exploreOptions: state.movieStore.get("exploreOptions") exploreOptions : state.movieStore.get("exploreOptions"),
}; };
} }
const mapDispatchToProps = { const mapDispatchToProps = {
selectMovie, selectMovie, updateFilter, movieWishlistToggle,
updateFilter,
movieWishlistToggle
}; };
const MovieList = props => { const MovieList = (props) => {
let selectedMovie = Map(); let selectedMovie = Map();
if (props.movies !== undefined && props.movies.has(props.selectedImdbId)) { if (props.movies !== undefined &&
selectedMovie = props.movies.get(props.selectedImdbId); props.movies.has(props.selectedImdbId)) {
} selectedMovie = props.movies.get(props.selectedImdbId);
}
return ( return (
<div className="row"> <div className="row">
@ -50,24 +45,15 @@ const MovieList = props => {
updateFilter={props.updateFilter} updateFilter={props.updateFilter}
filter={props.filter} filter={props.filter}
onClick={props.selectMovie} onClick={props.selectMovie}
onDoubleClick={function() { onDoubleClick={function() { return; }}
return; onKeyEnter={function() { return; }}
}}
onKeyEnter={function() {
return;
}}
params={props.match.params} params={props.match.params}
loading={props.loading} loading={props.loading}
/> />
<ListDetails <ListDetails
data={selectedMovie} data={selectedMovie}
loading={props.loading} loading={props.loading}
wishlist={() => wishlist={() => props.movieWishlistToggle(selectedMovie.get("imdb_id"), isWishlisted(selectedMovie))}
props.movieWishlistToggle(
selectedMovie.get("imdb_id"),
isWishlisted(selectedMovie)
)
}
> >
<ShowMore <ShowMore
id={selectedMovie.get("imdb_id")} id={selectedMovie.get("imdb_id")}
@ -89,7 +75,7 @@ const MovieList = props => {
</ListDetails> </ListDetails>
</div> </div>
); );
}; }
MovieList.propTypes = { MovieList.propTypes = {
movies: PropTypes.instanceOf(OrderedMap), movies: PropTypes.instanceOf(OrderedMap),
exploreOptions: PropTypes.instanceOf(Map), exploreOptions: PropTypes.instanceOf(Map),
@ -99,7 +85,7 @@ MovieList.propTypes = {
updateFilter: PropTypes.func, updateFilter: PropTypes.func,
movieWishlistToggle: PropTypes.func, movieWishlistToggle: PropTypes.func,
selectMovie: PropTypes.func, selectMovie: PropTypes.func,
match: PropTypes.object match: PropTypes.object,
}; };
export default connect(mapStateToProps, mapDispatchToProps)(MovieList); export default connect(mapStateToProps, mapDispatchToProps)(MovieList);

View File

@ -1,11 +1,11 @@
import React from "react"; import React from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { Route } from "react-router-dom"; import { Route } from "react-router-dom"
import { connect } from "react-redux"; import { connect } from "react-redux"
import { fetchMovies, getMovieExploreOptions } from "../../actions/movies"; import { fetchMovies, getMovieExploreOptions } from "../../actions/movies"
const mapStateToProps = state => ({ const mapStateToProps = (state) => ({
isExplorerFetched: state.movieStore.get("exploreOptions").size !== 0 isExplorerFetched: (state.movieStore.get("exploreOptions").size !== 0)
}); });
const mapDispatchToProps = { fetchMovies, getMovieExploreOptions }; const mapDispatchToProps = { fetchMovies, getMovieExploreOptions };
@ -17,49 +17,44 @@ const MoviesRoute = ({
...otherProps ...otherProps
}) => { }) => {
return ( return (
<Route <Route {...otherProps} render={(props) => {
{...otherProps} let fetchUrl = "";
render={props => { switch (props.match.path) {
let fetchUrl = ""; case "/movies/polochon":
switch (props.match.path) { fetchUrl = "/movies/polochon";
case "/movies/polochon": break;
fetchUrl = "/movies/polochon"; case "/movies/wishlist":
break; fetchUrl = "/wishlist/movies";
case "/movies/wishlist": break;
fetchUrl = "/wishlist/movies"; case "/movies/search/:search":
break; fetchUrl = "/movies/search/" + props.match.params.search;
case "/movies/search/:search": break;
fetchUrl = "/movies/search/" + props.match.params.search; case "/movies/explore/:source/:category":
break; if (!isExplorerFetched) {
case "/movies/explore/:source/:category": getMovieExploreOptions();
if (!isExplorerFetched) { }
getMovieExploreOptions(); fetchUrl = "/movies/explore?source=" +
} encodeURI(props.match.params.source) +
fetchUrl = "&category=" + encodeURI(props.match.params.category);
"/movies/explore?source=" + break;
encodeURI(props.match.params.source) + default:
"&category=" + break;
encodeURI(props.match.params.category); }
break;
default:
break;
}
if (fetchUrl != "") { if (fetchUrl != "") {
fetchMovies(fetchUrl); fetchMovies(fetchUrl);
} }
return <Component {...props} />; return <Component {...props} />
}} }} />
/> )
); }
};
MoviesRoute.propTypes = { MoviesRoute.propTypes = {
component: PropTypes.func, component: PropTypes.func,
match: PropTypes.object, match: PropTypes.object,
isExplorerFetched: PropTypes.bool.isRequired, isExplorerFetched: PropTypes.bool.isRequired,
fetchMovies: PropTypes.func.isRequired, fetchMovies: PropTypes.func.isRequired,
getMovieExploreOptions: PropTypes.func.isRequired getMovieExploreOptions: PropTypes.func.isRequired,
}; };
export default connect(mapStateToProps, mapDispatchToProps)(MoviesRoute); export default connect(mapStateToProps, mapDispatchToProps)(MoviesRoute);

View File

@ -1,18 +1,18 @@
import React from "react"; import React from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { List } from "immutable"; import { List } from "immutable"
import { connect } from "react-redux"; import { connect } from "react-redux"
import { searchMovieSubtitles } from "../../actions/subtitles"; import { searchMovieSubtitles } from "../../actions/subtitles"
import { SubtitlesButton } from "../buttons/subtitles"; import { SubtitlesButton } from "../buttons/subtitles"
const movieSubtitlesButton = ({ const movieSubtitlesButton = ({
inLibrary, inLibrary,
imdbId, imdbId,
searching, searching,
searchMovieSubtitles, searchMovieSubtitles,
subtitles subtitles,
}) => ( }) => (
<SubtitlesButton <SubtitlesButton
inLibrary={inLibrary} inLibrary={inLibrary}
@ -20,16 +20,14 @@ const movieSubtitlesButton = ({
subtitles={subtitles} subtitles={subtitles}
search={() => searchMovieSubtitles(imdbId)} search={() => searchMovieSubtitles(imdbId)}
/> />
); )
movieSubtitlesButton.propTypes = { movieSubtitlesButton.propTypes = {
searching: PropTypes.bool, searching: PropTypes.bool,
inLibrary: PropTypes.bool, inLibrary: PropTypes.bool,
imdbId: PropTypes.string, imdbId: PropTypes.string,
searchMovieSubtitles: PropTypes.func, searchMovieSubtitles: PropTypes.func,
subtitles: PropTypes.instanceOf(List) subtitles: PropTypes.instanceOf(List),
}; }
export const MovieSubtitlesButton = connect(null, { searchMovieSubtitles })( export const MovieSubtitlesButton = connect(null, {searchMovieSubtitles})(movieSubtitlesButton);
movieSubtitlesButton
);

View File

@ -1,33 +1,31 @@
import React from "react"; import React from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { List } from "immutable"; import { List } from "immutable"
import { connect } from "react-redux"; import { connect } from "react-redux"
import { getMovieDetails } from "../../actions/movies"; import { getMovieDetails } from "../../actions/movies"
import { TorrentsButton } from "../buttons/torrents"; import { TorrentsButton } from "../buttons/torrents"
const movieTorrentsButton = ({ const movieTorrentsButton = ({
torrents, torrents,
imdbId, imdbId,
title, title,
searching, searching,
getMovieDetails getMovieDetails,
}) => ( }) => (
<TorrentsButton <TorrentsButton
torrents={torrents} torrents={torrents}
searching={searching} searching={searching}
search={() => getMovieDetails(imdbId)} search={() => getMovieDetails(imdbId)}
url={`#/torrents/search/movies/${encodeURI(title)}`} url={`#/torrents/search/movies/${encodeURI(title)}`}
/> />
); )
movieTorrentsButton.propTypes = { movieTorrentsButton.propTypes = {
torrents: PropTypes.instanceOf(List), torrents: PropTypes.instanceOf(List),
imdbId: PropTypes.string, imdbId: PropTypes.string,
title: PropTypes.string, title: PropTypes.string,
searching: PropTypes.bool, searching: PropTypes.bool,
getMovieDetails: PropTypes.func getMovieDetails: PropTypes.func,
}; };
export const MovieTorrentsButton = connect(null, { getMovieDetails })( export const MovieTorrentsButton = connect(null, {getMovieDetails})(movieTorrentsButton);
movieTorrentsButton
);

View File

@ -1,41 +1,31 @@
import React, { useState } from "react"; import React, { useState } from "react"
import { Route } from "react-router-dom"; import { Route } from "react-router-dom"
import { connect } from "react-redux"; import { connect } from "react-redux"
import { LinkContainer } from "react-router-bootstrap"; import { LinkContainer } from "react-router-bootstrap"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import Nav from "react-bootstrap/Nav"; import Nav from "react-bootstrap/Nav"
import Navbar from "react-bootstrap/Navbar"; import Navbar from "react-bootstrap/Navbar"
import NavDropdown from "react-bootstrap/NavDropdown"; import NavDropdown from "react-bootstrap/NavDropdown"
const mapStateToProps = state => { const mapStateToProps = (state) => {
let torrentCount = 0; let torrentCount = 0;
if ( if (state.torrentStore.has("torrents") && state.torrentStore.get("torrents") !== undefined) {
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.get("username"), username: state.userStore.get("username"),
isAdmin: state.userStore.get("isAdmin"), isAdmin: state.userStore.get("isAdmin"),
torrentCount: torrentCount torrentCount: torrentCount,
}; }
}; };
const AppNavBar = props => { const AppNavBar = (props) => {
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
return ( return (
<Navbar <Navbar fixed="top" collapseOnSelect bg="dark" variant="dark"
fixed="top" expanded={expanded} expand="sm" onToggle={() => setExpanded(!expanded)}>
collapseOnSelect
bg="dark"
variant="dark"
expanded={expanded}
expand="sm"
onToggle={() => setExpanded(!expanded)}
>
<LinkContainer to="/"> <LinkContainer to="/">
<Navbar.Brand>Canapé</Navbar.Brand> <Navbar.Brand>Canapé</Navbar.Brand>
</LinkContainer> </LinkContainer>
@ -48,39 +38,36 @@ const AppNavBar = props => {
<TorrentsDropdown torrentsCount={props.torrentCount} /> <TorrentsDropdown torrentsCount={props.torrentCount} />
</Nav> </Nav>
<Nav> <Nav>
<Route <Route path="/movies" render={(props) =>
path="/movies"
render={props => (
<Search <Search
placeholder="Search movies" placeholder="Search movies"
path="/movies/search" path='/movies/search'
history={props.history} history={props.history}
setExpanded={setExpanded} setExpanded={setExpanded}
/> />
)} }/>
/> <Route path="/shows" render={(props) =>
<Route
path="/shows"
render={props => (
<Search <Search
placeholder="Search shows" placeholder="Search shows"
path="/shows/search" path='/shows/search'
history={props.history} history={props.history}
setExpanded={setExpanded} setExpanded={setExpanded}
/> />
)} }/>
<UserDropdown
username={props.username}
isAdmin={props.isAdmin}
/> />
<UserDropdown username={props.username} isAdmin={props.isAdmin} />
</Nav> </Nav>
</Navbar.Collapse> </Navbar.Collapse>
</Navbar> </Navbar>
); );
}; }
AppNavBar.propTypes = { AppNavBar.propTypes = {
torrentCount: PropTypes.number.isRequired, torrentCount: PropTypes.number.isRequired,
username: PropTypes.string.isRequired, username: PropTypes.string.isRequired,
isAdmin: PropTypes.bool.isRequired, isAdmin: PropTypes.bool.isRequired,
history: PropTypes.object history: PropTypes.object,
}; };
export default connect(mapStateToProps)(AppNavBar); export default connect(mapStateToProps)(AppNavBar);
@ -88,30 +75,30 @@ export default connect(mapStateToProps)(AppNavBar);
const Search = ({ path, placeholder, setExpanded, history }) => { const Search = ({ path, placeholder, setExpanded, history }) => {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const handleSearch = ev => { const handleSearch = (ev) => {
ev.preventDefault(); ev.preventDefault();
history.push(`${path}/${encodeURI(search)}`); history.push(`${path}/${encodeURI(search)}`);
setExpanded(false); setExpanded(false);
}; }
return ( return(
<div className="navbar-form navbar-right"> <div className="navbar-form navbar-right">
<form className="input-group" onSubmit={ev => handleSearch(ev)}> <form className="input-group" onSubmit={(ev) => handleSearch(ev)}>
<input <input
className="form-control" className="form-control"
placeholder={placeholder} placeholder={placeholder}
value={search} value={search}
onChange={e => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
/> />
</form> </form>
</div> </div>
); );
}; }
Search.propTypes = { Search.propTypes = {
placeholder: PropTypes.string.isRequired, placeholder: PropTypes.string.isRequired,
setExpanded: PropTypes.func.isRequired, setExpanded: PropTypes.func.isRequired,
path: PropTypes.string.isRequired, path: PropTypes.string.isRequired,
history: PropTypes.object history: PropTypes.object,
}; };
const MoviesDropdown = () => ( const MoviesDropdown = () => (
@ -123,7 +110,7 @@ const MoviesDropdown = () => (
<NavDropdown.Item>My movies</NavDropdown.Item> <NavDropdown.Item>My movies</NavDropdown.Item>
</LinkContainer> </LinkContainer>
</NavDropdown> </NavDropdown>
); )
const ShowsDropdown = () => ( const ShowsDropdown = () => (
<NavDropdown title="Shows" id="navbar-shows-dropdown"> <NavDropdown title="Shows" id="navbar-shows-dropdown">
@ -136,14 +123,14 @@ const ShowsDropdown = () => (
</NavDropdown> </NavDropdown>
); );
const UserDropdown = props => ( const UserDropdown = (props) => (
<Nav> <Nav>
<NavDropdown title={props.username} alignRight> <NavDropdown title={props.username} alignRight>
{props.isAdmin && ( {props.isAdmin &&
<LinkContainer to="/admin"> <LinkContainer to="/admin">
<NavDropdown.Item>Admin Panel</NavDropdown.Item> <NavDropdown.Item>Admin Panel</NavDropdown.Item>
</LinkContainer> </LinkContainer>
)} }
<LinkContainer to="/users/profile"> <LinkContainer to="/users/profile">
<NavDropdown.Item>Profile</NavDropdown.Item> <NavDropdown.Item>Profile</NavDropdown.Item>
</LinkContainer> </LinkContainer>
@ -155,10 +142,10 @@ const UserDropdown = props => (
</LinkContainer> </LinkContainer>
</NavDropdown> </NavDropdown>
</Nav> </Nav>
); )
UserDropdown.propTypes = { UserDropdown.propTypes = {
username: PropTypes.string.isRequired, username: PropTypes.string.isRequired,
isAdmin: PropTypes.bool.isRequired isAdmin: PropTypes.bool.isRequired,
}; };
const WishlistDropdown = () => ( const WishlistDropdown = () => (
@ -172,9 +159,9 @@ const WishlistDropdown = () => (
</NavDropdown> </NavDropdown>
); );
const TorrentsDropdown = props => { const TorrentsDropdown = (props) => {
const title = <TorrentsDropdownTitle torrentsCount={props.torrentsCount} />; const title = (<TorrentsDropdownTitle torrentsCount={props.torrentsCount} />)
return ( return(
<NavDropdown title={title} id="navbar-wishlit-dropdown"> <NavDropdown title={title} id="navbar-wishlit-dropdown">
<LinkContainer to="/torrents/list"> <LinkContainer to="/torrents/list">
<NavDropdown.Item>Downloads</NavDropdown.Item> <NavDropdown.Item>Downloads</NavDropdown.Item>
@ -184,27 +171,22 @@ const TorrentsDropdown = props => {
</LinkContainer> </LinkContainer>
</NavDropdown> </NavDropdown>
); );
}; }
TorrentsDropdown.propTypes = { torrentsCount: PropTypes.number.isRequired }; TorrentsDropdown.propTypes = { torrentsCount: PropTypes.number.isRequired };
const TorrentsDropdownTitle = props => { const TorrentsDropdownTitle = (props) => {
if (props.torrentsCount === 0) { if (props.torrentsCount === 0) {
return <span>Torrents</span>; return (
<span>Torrents</span>
);
} }
return ( return (
<span> <span> Torrents
{" "}
Torrents
<span> <span>
&nbsp;{" "} &nbsp; <span className="badge badge-info badge-pill">{props.torrentsCount}</span>
<span className="badge badge-info badge-pill">
{props.torrentsCount}
</span>
</span> </span>
</span> </span>
); );
}; }
TorrentsDropdownTitle.propTypes = { TorrentsDropdownTitle.propTypes = { torrentsCount: PropTypes.number.isRequired };
torrentsCount: PropTypes.number.isRequired
};

View File

@ -1,10 +1,10 @@
import React, { useState } from "react"; import React, { useState } from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { connect } from "react-redux"; import { connect } from "react-redux"
import { Toast } from "react-bootstrap"; import { Toast } from "react-bootstrap"
import { removeNotification } from "../../actions/notifications"; import { removeNotification } from "../../actions/notifications"
const NotificationConnected = ({ const NotificationConnected = ({
id, id,
@ -14,30 +14,39 @@ const NotificationConnected = ({
imageUrl, imageUrl,
autohide, autohide,
delay, delay,
removeNotification removeNotification,
}) => { }) => {
const [show, setShow] = useState(true); const [show, setShow] = useState(true)
const hide = () => { const hide = () => {
setShow(false); setShow(false);
setTimeout(() => removeNotification(id), 200); setTimeout(() => removeNotification(id), 200);
}; }
return ( return (
<Toast show={show} onClose={hide} autohide={autohide} delay={delay}> <Toast
show={show}
onClose={hide}
autohide={autohide}
delay={delay}
>
<Toast.Header> <Toast.Header>
{icon !== "" && <i className={`fa fa-${icon} mr-2`} />} {icon !== "" &&
<i className={`fa fa-${icon} mr-2`} />
}
<strong className="mr-auto">{title}</strong> <strong className="mr-auto">{title}</strong>
</Toast.Header> </Toast.Header>
<Toast.Body> <Toast.Body>
{message !== "" && <span>{message}</span>} {message !== "" &&
{imageUrl !== "" && ( <span>{message}</span>
<img src={imageUrl} className="img-fluid mt-2 mr-auto" /> }
)} {imageUrl !== "" &&
<img src={imageUrl} className="img-fluid mt-2 mr-auto" />
}
</Toast.Body> </Toast.Body>
</Toast> </Toast>
); )
}; }
NotificationConnected.propTypes = { NotificationConnected.propTypes = {
id: PropTypes.string, id: PropTypes.string,
icon: PropTypes.string, icon: PropTypes.string,
@ -46,7 +55,7 @@ NotificationConnected.propTypes = {
imageUrl: PropTypes.string, imageUrl: PropTypes.string,
autohide: PropTypes.bool, autohide: PropTypes.bool,
delay: PropTypes.number, delay: PropTypes.number,
removeNotification: PropTypes.func removeNotification: PropTypes.func,
}; };
NotificationConnected.defaultProps = { NotificationConnected.defaultProps = {
@ -55,9 +64,7 @@ NotificationConnected.defaultProps = {
icon: "", icon: "",
imageUrl: "", imageUrl: "",
title: "Info", title: "Info",
message: "" message: "",
}; };
export const Notification = connect(null, { removeNotification })( export const Notification = connect(null, {removeNotification})(NotificationConnected);
NotificationConnected
);

View File

@ -1,14 +1,14 @@
import React from "react"; import React from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { List } from "immutable"; import { List } from "immutable"
import { connect } from "react-redux"; import { connect } from "react-redux"
import { Notification } from "./notification"; import { Notification } from "./notification"
const NotificationsConnected = ({ notifications }) => { const NotificationsConnected = ({ notifications }) => {
return ( return (
<div className="notifications"> <div className="notifications">
{notifications.map(el => ( {notifications.map((el) => (
<Notification <Notification
key={el.get("id")} key={el.get("id")}
id={el.get("id")} id={el.get("id")}
@ -21,14 +21,14 @@ const NotificationsConnected = ({ notifications }) => {
/> />
))} ))}
</div> </div>
); )
}; }
NotificationsConnected.propTypes = { NotificationsConnected.propTypes = {
notifications: PropTypes.instanceOf(List) notifications: PropTypes.instanceOf(List),
}; }
const mapStateToProps = state => ({ const mapStateToProps = (state) => ({
notifications: state.notifications notifications: state.notifications,
}); });
export const Notifications = connect(mapStateToProps)(NotificationsConnected); export const Notifications = connect(mapStateToProps)(NotificationsConnected);

View File

@ -1,10 +1,10 @@
import React, { useState } from "react"; import React, { useState } from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { connect } from "react-redux"; import { connect } from "react-redux"
import { addPolochon } from "../../actions/polochon"; import { addPolochon } from "../../actions/polochon"
import { PolochonEdit } from "./edit"; import { PolochonEdit } from "./edit"
export const PolochonAddConnected = ({ addPolochon }) => { export const PolochonAddConnected = ({ addPolochon }) => {
const [modal, setModal] = useState(false); const [modal, setModal] = useState(false);
@ -24,10 +24,10 @@ export const PolochonAddConnected = ({ addPolochon }) => {
update={addPolochon} update={addPolochon}
/> />
</React.Fragment> </React.Fragment>
); )
}; }
PolochonAddConnected.propTypes = { PolochonAddConnected.propTypes = {
addPolochon: PropTypes.func addPolochon: PropTypes.func,
}; };
export const PolochonAdd = connect(null, { addPolochon })(PolochonAddConnected); export const PolochonAdd = connect(null, {addPolochon})(PolochonAddConnected);

View File

@ -1,8 +1,8 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { FormModal } from "../forms/modal"; import { FormModal } from "../forms/modal"
import { FormInput } from "../forms/input"; import { FormInput } from "../forms/input"
export const PolochonEdit = ({ export const PolochonEdit = ({
show, show,
@ -13,7 +13,7 @@ export const PolochonEdit = ({
initialName, initialName,
initialUrl, initialUrl,
initialToken, initialToken,
update update,
}) => { }) => {
const [name, setName] = useState(initialName); const [name, setName] = useState(initialName);
const [url, setUrl] = useState(initialUrl); const [url, setUrl] = useState(initialUrl);
@ -23,7 +23,7 @@ export const PolochonEdit = ({
setName(initialName); setName(initialName);
setUrl(initialUrl); setUrl(initialUrl);
setToken(initialToken); setToken(initialToken);
}, [id, initialName, initialUrl, initialToken]); }, [id, initialName, initialUrl, initialToken])
const handleSubmit = () => { const handleSubmit = () => {
update({ id, name, url, token }); update({ id, name, url, token });
@ -42,8 +42,8 @@ export const PolochonEdit = ({
<FormInput label="URL" value={url} updateValue={setUrl} /> <FormInput label="URL" value={url} updateValue={setUrl} />
<FormInput label="Token" value={token} updateValue={setToken} /> <FormInput label="Token" value={token} updateValue={setToken} />
</FormModal> </FormModal>
); )
}; }
PolochonEdit.propTypes = { PolochonEdit.propTypes = {
show: PropTypes.bool, show: PropTypes.bool,
title: PropTypes.string, title: PropTypes.string,
@ -53,7 +53,7 @@ PolochonEdit.propTypes = {
id: PropTypes.string, id: PropTypes.string,
initialName: PropTypes.string, initialName: PropTypes.string,
initialUrl: PropTypes.string, initialUrl: PropTypes.string,
initialToken: PropTypes.string initialToken: PropTypes.string,
}; };
PolochonEdit.defaultProps = { PolochonEdit.defaultProps = {
id: "", id: "",
@ -61,5 +61,5 @@ PolochonEdit.defaultProps = {
icon: "edit", icon: "edit",
initialName: "", initialName: "",
initialUrl: "", initialUrl: "",
initialToken: "" initialToken: "",
}; };

View File

@ -1,25 +1,29 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { connect } from "react-redux"; import { connect } from "react-redux"
import { List } from "immutable"; import { List } from "immutable"
import { getManagedPolochons } from "../../actions/polochon"; import { getManagedPolochons } from "../../actions/polochon"
import { Polochon } from "./polochon"; import { Polochon } from "./polochon"
import { PolochonAdd } from "./add"; import { PolochonAdd } from "./add"
const mapStateToProps = state => ({ const mapStateToProps = (state) => ({
managedList: state.polochon.get("managed") managedList: state.polochon.get("managed"),
}); });
const mapDispatchToProps = { const mapDispatchToProps = {
getManagedPolochons getManagedPolochons,
}; }
const PolochonListConnected = ({
getManagedPolochons,
managedList,
}) => {
const PolochonListConnected = ({ getManagedPolochons, managedList }) => {
useEffect(() => { useEffect(() => {
getManagedPolochons(); getManagedPolochons();
}, [getManagedPolochons]); }, [getManagedPolochons])
return ( return (
<div className="row mb-3"> <div className="row mb-3">
@ -42,14 +46,11 @@ const PolochonListConnected = ({ getManagedPolochons, managedList }) => {
<PolochonAdd /> <PolochonAdd />
</div> </div>
</div> </div>
); )
}; }
PolochonListConnected.propTypes = { PolochonListConnected.propTypes = {
getManagedPolochons: PropTypes.func, getManagedPolochons: PropTypes.func,
managedList: PropTypes.instanceOf(List) managedList: PropTypes.instanceOf(List),
}; };
export const PolochonList = connect( export const PolochonList = connect(mapStateToProps, mapDispatchToProps)(PolochonListConnected);
mapStateToProps,
mapDispatchToProps
)(PolochonListConnected);

View File

@ -1,12 +1,12 @@
import React, { useState } from "react"; import React, { useState } from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { List } from "immutable"; import { List } from "immutable"
import { connect } from "react-redux"; import { connect } from "react-redux"
import { PolochonUsers } from "./users"; import { PolochonUsers } from "./users"
import { PolochonEdit } from "./edit"; import { PolochonEdit } from "./edit"
import { updatePolochon, deletePolochon } from "../../actions/polochon"; import { updatePolochon, deletePolochon } from "../../actions/polochon"
export const PolochonConnected = ({ export const PolochonConnected = ({
id, id,
@ -16,7 +16,7 @@ export const PolochonConnected = ({
authToken, authToken,
users, users,
updatePolochon, updatePolochon,
deletePolochon deletePolochon,
}) => { }) => {
const [edit, setEdit] = useState(false); const [edit, setEdit] = useState(false);
@ -25,7 +25,9 @@ export const PolochonConnected = ({
<div className="card mb-2"> <div className="card mb-2">
<div className="card-header"> <div className="card-header">
{name !== "" ? name : "-"} {name !== "" ? name : "-"}
<small className="ml-1">({url !== "" ? url : "-"})</small> <small className="ml-1">
({url !== "" ? url : "-"})
</small>
<span className="pull-right"> <span className="pull-right">
<i <i
className="fa fa-edit mr-3 clickable" className="fa fa-edit mr-3 clickable"
@ -56,8 +58,8 @@ export const PolochonConnected = ({
initialToken={token} initialToken={token}
/> />
</React.Fragment> </React.Fragment>
); )
}; }
PolochonConnected.propTypes = { PolochonConnected.propTypes = {
id: PropTypes.string, id: PropTypes.string,
name: PropTypes.string, name: PropTypes.string,
@ -66,9 +68,7 @@ PolochonConnected.propTypes = {
authToken: PropTypes.string, authToken: PropTypes.string,
users: PropTypes.instanceOf(List), users: PropTypes.instanceOf(List),
updatePolochon: PropTypes.func, updatePolochon: PropTypes.func,
deletePolochon: PropTypes.func deletePolochon: PropTypes.func,
}; };
export const Polochon = connect(null, { updatePolochon, deletePolochon })( export const Polochon = connect(null, { updatePolochon, deletePolochon })(PolochonConnected);
PolochonConnected
);

View File

@ -1,26 +1,30 @@
import React from "react"; import React from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { List } from "immutable"; import { List } from "immutable"
export const PolochonSelect = ({ value, changeValue, polochonList }) => { export const PolochonSelect = ({
value,
changeValue,
polochonList,
}) => {
return ( return (
<select <select
className="form-control" className="form-control"
value={value} value={value}
onChange={e => onChange={(e) => changeValue(e.target.options[e.target.selectedIndex].value)}
changeValue(e.target.options[e.target.selectedIndex].value)
}
> >
{polochonList.map((el, index) => ( {polochonList.map((el, index) => (
<option value={el.get("id")} key={index}> <option
value={el.get("id")}
key={index}>
{el.get("name")} ({el.get("url")}) {el.get("name")} ({el.get("url")})
</option> </option>
))} ))}
</select> </select>
); );
}; }
PolochonSelect.propTypes = { PolochonSelect.propTypes = {
value: PropTypes.string, value: PropTypes.string,
changeValue: PropTypes.func, changeValue: PropTypes.func,
polochonList: PropTypes.instanceOf(List) polochonList: PropTypes.instanceOf(List),
}; };

View File

@ -1,13 +1,13 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { connect } from "react-redux"; import { connect } from "react-redux"
import { editPolochonUser } from "../../actions/polochon"; import { editPolochonUser } from "../../actions/polochon"
import Toggle from "react-bootstrap-toggle"; import Toggle from "react-bootstrap-toggle";
import { FormModal } from "../forms/modal"; import { FormModal } from "../forms/modal"
import { FormInput } from "../forms/input"; import { FormInput } from "../forms/input"
export const PolochonUserConnected = ({ export const PolochonUserConnected = ({
polochonId, polochonId,
@ -15,7 +15,7 @@ export const PolochonUserConnected = ({
name, name,
initialToken, initialToken,
initialActivated, initialActivated,
editPolochonUser editPolochonUser,
}) => { }) => {
const [edit, setEdit] = useState(false); const [edit, setEdit] = useState(false);
const [token, setToken] = useState(initialToken); const [token, setToken] = useState(initialToken);
@ -24,22 +24,26 @@ export const PolochonUserConnected = ({
useEffect(() => { useEffect(() => {
setActivated(initialActivated); setActivated(initialActivated);
setToken(initialToken); setToken(initialToken);
}, [initialActivated, initialToken]); }, [initialActivated, initialToken])
const handleSubmit = () => { const handleSubmit = () => {
editPolochonUser({ editPolochonUser({
polochonId, polochonId,
id, id,
token, token,
activated activated,
}); });
setEdit(false); setEdit(false);
}; }
return ( return (
<tr> <tr>
<td>{name}</td> <td>
<td>{activated ? "Activated" : "Not activated"}</td> {name}
</td>
<td>
{activated ? "Activated" : "Not activated"}
</td>
<td> <td>
<i className="fa fa-edit ml-2" onClick={() => setEdit(true)} /> <i className="fa fa-edit ml-2" onClick={() => setEdit(true)} />
<FormModal <FormModal
@ -59,23 +63,20 @@ export const PolochonUserConnected = ({
active={activated} active={activated}
offstyle="info" offstyle="info"
handlestyle="secondary" handlestyle="secondary"
onClick={() => setActivated(!activated)} onClick={() => setActivated(!activated)} />
/>
</div> </div>
</FormModal> </FormModal>
</td> </td>
</tr> </tr>
); )
}; }
PolochonUserConnected.propTypes = { PolochonUserConnected.propTypes = {
polochonId: PropTypes.string, polochonId: PropTypes.string,
id: PropTypes.string, id: PropTypes.string,
name: PropTypes.string, name: PropTypes.string,
initialToken: PropTypes.string, initialToken: PropTypes.string,
initialActivated: PropTypes.bool, initialActivated: PropTypes.bool,
editPolochonUser: PropTypes.func editPolochonUser: PropTypes.func,
}; };
export const PolochonUser = connect(null, { editPolochonUser })( export const PolochonUser = connect(null, {editPolochonUser})(PolochonUserConnected);
PolochonUserConnected
);

View File

@ -1,13 +1,11 @@
import React from "react"; import React from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { List } from "immutable"; import { List } from "immutable"
import { PolochonUser } from "./user"; import { PolochonUser } from "./user"
export const PolochonUsers = ({ id, users }) => { export const PolochonUsers = ({ id, users }) => {
if (users === null || users.size === 0) { if (users === null || users.size === 0) { return null }
return null;
}
return ( return (
<table className="table border border-light table-dark"> <table className="table border border-light table-dark">
@ -31,9 +29,9 @@ export const PolochonUsers = ({ id, users }) => {
))} ))}
</tbody> </tbody>
</table> </table>
); )
}; }
PolochonUsers.propTypes = { PolochonUsers.propTypes = {
id: PropTypes.string, id: PropTypes.string,
users: PropTypes.instanceOf(List) users: PropTypes.instanceOf(List),
}; };

View File

@ -1,22 +1,22 @@
import React from "react"; import React from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { Map } from "immutable"; import { Map } from "immutable"
import { connect } from "react-redux"; import { connect } from "react-redux"
import Loader from "../loader/loader"; import Loader from "../loader/loader"
import { Fanart } from "./details/fanart"; import { Fanart } from "./details/fanart"
import { Header } from "./details/header"; import { Header } from "./details/header"
import { SeasonsList } from "./details/seasons"; import { SeasonsList } from "./details/seasons"
const mapStateToProps = state => ({ const mapStateToProps = (state) => ({
loading: state.showStore.get("loading"), loading: state.showStore.get("loading"),
show: state.showStore.get("show") show: state.showStore.get("show"),
}); })
const showDetails = ({ show, loading }) => { const showDetails = ({ show, loading }) => {
if (loading === true) { if (loading === true) {
return <Loader />; return (<Loader />);
} }
return ( return (
@ -28,9 +28,9 @@ const showDetails = ({ show, loading }) => {
</div> </div>
</React.Fragment> </React.Fragment>
); );
}; }
showDetails.propTypes = { showDetails.propTypes = {
loading: PropTypes.bool, loading: PropTypes.bool,
show: PropTypes.instanceOf(Map) show: PropTypes.instanceOf(Map),
}; };
export const ShowDetails = connect(mapStateToProps)(showDetails); export const ShowDetails = connect(mapStateToProps)(showDetails);

View File

@ -1,35 +1,31 @@
import React from "react"; import React from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { Map } from "immutable"; import { Map } from "immutable"
import { connect } from "react-redux"; import { connect } from "react-redux"
import { showWishlistToggle } from "../../../actions/shows"; import { showWishlistToggle } from "../../../actions/shows"
import { import { inLibrary, isEpisodeWishlisted, prettyEpisodeName } from "../../../utils"
inLibrary,
isEpisodeWishlisted,
prettyEpisodeName
} from "../../../utils";
import { Plot } from "../../details/plot"; import { Plot } from "../../details/plot"
import { PolochonMetadata } from "../../details/polochon"; import { PolochonMetadata } from "../../details/polochon"
import { ReleaseDate } from "../../details/releaseDate"; import { ReleaseDate } from "../../details/releaseDate"
import { Runtime } from "../../details/runtime"; import { Runtime } from "../../details/runtime"
import { Title } from "../../details/title"; import { Title } from "../../details/title"
import { DownloadAndStream } from "../../buttons/download"; import { DownloadAndStream } from "../../buttons/download"
import { ShowMore } from "../../buttons/showMore"; import { ShowMore } from "../../buttons/showMore"
import { EpisodeSubtitlesButton } from "./subtitlesButton"; import { EpisodeSubtitlesButton } from "./subtitlesButton"
import { EpisodeThumb } from "./episodeThumb"; import { EpisodeThumb } from "./episodeThumb"
import { EpisodeTorrentsButton } from "./torrentsButton"; import { EpisodeTorrentsButton } from "./torrentsButton"
const mapStateToProps = state => ({ const mapStateToProps = (state) => ({
trackedSeason: state.showStore.getIn(["show", "tracked_season"], null), trackedSeason: state.showStore.getIn(["show", "tracked_season"], null),
trackedEpisode: state.showStore.getIn(["show", "tracked_episode"], null) trackedEpisode: state.showStore.getIn(["show", "tracked_episode"], null),
}); })
const episode = props => ( const episode = (props) => (
<div className="d-flex flex-column flex-lg-row mb-3 pb-3 border-bottom border-light"> <div className="d-flex flex-column flex-lg-row mb-3 pb-3 border-bottom border-light">
<EpisodeThumb url={props.data.get("thumb")} /> <EpisodeThumb url={props.data.get("thumb")} />
<div className="d-flex flex-column"> <div className="d-flex flex-column">
@ -38,26 +34,20 @@ const episode = props => (
wishlisted={isEpisodeWishlisted( wishlisted={isEpisodeWishlisted(
props.data, props.data,
props.trackedSeason, props.trackedSeason,
props.trackedEpisode props.trackedEpisode,
)}
wishlist={() => props.showWishlistToggle(
isEpisodeWishlisted(props.data),
props.data.get("show_imdb_id"),
props.data.get("season"),
props.data.get("episode"),
)} )}
wishlist={() =>
props.showWishlistToggle(
isEpisodeWishlisted(props.data),
props.data.get("show_imdb_id"),
props.data.get("season"),
props.data.get("episode")
)
}
/> />
<ReleaseDate date={props.data.get("aired")} /> <ReleaseDate date={props.data.get("aired")} />
<Runtime runtime={props.data.get("runtime")} /> <Runtime runtime={props.data.get("runtime")} />
<Plot plot={props.data.get("plot")} /> <Plot plot={props.data.get("plot")} />
<DownloadAndStream <DownloadAndStream
name={prettyEpisodeName( name={prettyEpisodeName(props.showName, props.data.get("season"), props.data.get("episode"))}
props.showName,
props.data.get("season"),
props.data.get("episode")
)}
url={props.data.get("polochon_url")} url={props.data.get("polochon_url")}
subtitles={props.data.get("subtitles")} subtitles={props.data.get("subtitles")}
/> />
@ -70,11 +60,7 @@ const episode = props => (
light light
/> />
<ShowMore <ShowMore
id={prettyEpisodeName( id={prettyEpisodeName(props.showName, props.data.get("season"), props.data.get("episode"))}
props.showName,
props.data.get("season"),
props.data.get("episode")
)}
inLibrary={inLibrary(props.data)} inLibrary={inLibrary(props.data)}
> >
<EpisodeTorrentsButton <EpisodeTorrentsButton
@ -96,15 +82,13 @@ const episode = props => (
</ShowMore> </ShowMore>
</div> </div>
</div> </div>
); )
episode.propTypes = { episode.propTypes = {
data: PropTypes.instanceOf(Map).isRequired, data: PropTypes.instanceOf(Map).isRequired,
trackedSeason: PropTypes.number, trackedSeason: PropTypes.number,
trackedEpisode: PropTypes.number, trackedEpisode: PropTypes.number,
showName: PropTypes.string.isRequired, showName: PropTypes.string.isRequired,
showWishlistToggle: PropTypes.func showWishlistToggle: PropTypes.func,
}; };
export const Episode = connect(mapStateToProps, { showWishlistToggle })( export const Episode = connect(mapStateToProps, {showWishlistToggle})(episode);
episode
);

View File

@ -1,15 +1,13 @@
import React from "react"; import React from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
export const EpisodeThumb = ({ url }) => { export const EpisodeThumb = ({ url }) => {
if (url === "") { if (url === "") { return null }
return null;
}
return ( return (
<div className="mr-0 mr-lg-2 mb-2 mb-lg-0 episode-thumb"> <div className="mr-0 mr-lg-2 mb-2 mb-lg-0 episode-thumb">
<img src={url} /> <img src={url} />
</div> </div>
); )
}; }
EpisodeThumb.propTypes = { url: PropTypes.string }; EpisodeThumb.propTypes = { url: PropTypes.string };
EpisodeThumb.defaultProps = { url: "" }; EpisodeThumb.defaultProps = { url: "" };

View File

@ -1,5 +1,5 @@
import React from "react"; import React from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
export const Fanart = ({ url }) => ( export const Fanart = ({ url }) => (
<div className="show-fanart mx-n3 mt-n1"> <div className="show-fanart mx-n3 mt-n1">
@ -8,7 +8,7 @@ export const Fanart = ({ url }) => (
src={url} src={url}
/> />
</div> </div>
); )
Fanart.propTypes = { Fanart.propTypes = {
url: PropTypes.string url: PropTypes.string,
}; }

View File

@ -1,21 +1,21 @@
import React from "react"; import React from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { Map } from "immutable"; import { Map } from "immutable"
import { connect } from "react-redux"; import { connect } from "react-redux"
import { isWishlisted } from "../../../utils"; import { isWishlisted } from "../../../utils"
import { showWishlistToggle } from "../../../actions/shows"; import { showWishlistToggle } from "../../../actions/shows"
import { Plot } from "../../details/plot"; import { Plot } from "../../details/plot"
import { Rating } from "../../details/rating"; import { Rating } from "../../details/rating"
import { ReleaseDate } from "../../details/releaseDate"; import { ReleaseDate } from "../../details/releaseDate"
import { Title } from "../../details/title"; import { Title } from "../../details/title"
import { TrackingLabel } from "../../details/tracking"; import { TrackingLabel } from "../../details/tracking"
import { ImdbBadge } from "../../buttons/imdb"; import { ImdbBadge } from "../../buttons/imdb"
export const header = props => ( export const header = (props) => (
<div className="card col-12 col-md-10 offset-md-1 mt-n3 mb-3"> <div className="card col-12 col-md-10 offset-md-1 mt-n3 mb-3">
<div className="d-flex flex-column flex-md-row"> <div className="d-flex flex-column flex-md-row">
<div className="d-flex justify-content-center"> <div className="d-flex justify-content-center">
@ -30,12 +30,9 @@ export const header = props => (
<Title <Title
title={props.data.get("title")} title={props.data.get("title")}
wishlisted={isWishlisted(props.data)} wishlisted={isWishlisted(props.data)}
wishlist={() => wishlist={() => props.showWishlistToggle(
props.showWishlistToggle( isWishlisted(props.data), props.data.get("imdb_id"),
isWishlisted(props.data), )}
props.data.get("imdb_id")
)
}
/> />
</p> </p>
<p className="card-text"> <p className="card-text">
@ -64,7 +61,7 @@ export const header = props => (
); );
header.propTypes = { header.propTypes = {
data: PropTypes.instanceOf(Map), data: PropTypes.instanceOf(Map),
showWishlistToggle: PropTypes.func showWishlistToggle: PropTypes.func,
}; };
export const Header = connect(null, { showWishlistToggle })(header); export const Header = connect(null, {showWishlistToggle})(header);

View File

@ -1,10 +1,10 @@
import React, { useState } from "react"; import React, { useState } from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { Map } from "immutable"; import { Map } from "immutable"
import { Episode } from "./episode"; import { Episode } from "./episode"
export const Season = props => { export const Season = (props) => {
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
return ( return (
@ -12,13 +12,8 @@ export const Season = props => {
<div className="card-header clickable" onClick={() => setShow(!show)}> <div className="card-header clickable" onClick={() => setShow(!show)}>
<h5 className="m-0"> <h5 className="m-0">
Season {props.season} Season {props.season}
<small className="text-primary"> <small className="text-primary"> ({props.data.toList().size} episodes)</small>
{" "} <i className={`float-right fa fa-chevron-${show ? "down" : "left"}`}></i>
({props.data.toList().size} episodes)
</small>
<i
className={`float-right fa fa-chevron-${show ? "down" : "left"}`}
></i>
</h5> </h5>
</div> </div>
<div className={`card-body ${show ? "d-flex flex-column" : "d-none"}`}> <div className={`card-body ${show ? "d-flex flex-column" : "d-none"}`}>
@ -34,12 +29,12 @@ export const Season = props => {
getEpisodeDetails={props.getEpisodeDetails} getEpisodeDetails={props.getEpisodeDetails}
refreshSubtitles={props.refreshSubtitles} refreshSubtitles={props.refreshSubtitles}
/> />
); )
})} })}
</div> </div>
</div> </div>
); )
}; }
Season.propTypes = { Season.propTypes = {
data: PropTypes.instanceOf(Map), data: PropTypes.instanceOf(Map),
season: PropTypes.number, season: PropTypes.number,
@ -47,5 +42,5 @@ Season.propTypes = {
addToWishlist: PropTypes.func, addToWishlist: PropTypes.func,
addTorrent: PropTypes.func, addTorrent: PropTypes.func,
refreshSubtitles: PropTypes.func, refreshSubtitles: PropTypes.func,
getEpisodeDetails: PropTypes.func getEpisodeDetails: PropTypes.func,
}; };

View File

@ -1,31 +1,26 @@
import React from "react"; import React from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { Map } from "immutable"; import { Map } from "immutable"
import { Season } from "./season"; import { Season } from "./season"
export const SeasonsList = props => ( export const SeasonsList = (props) => (
<div className="col col-12 col-md-10 offset-md-1"> <div className="col col-12 col-md-10 offset-md-1">
{props.data {props.data.get("seasons").entrySeq().map(function([season, data]) {
.get("seasons") if (season === 0) { return null }
.entrySeq() return (
.map(function([season, data]) { <Season
if (season === 0) { key={`season-list-key-${season}`}
return null; data={data}
} season={season}
return ( showName={props.data.get("title")}
<Season addTorrent={props.addTorrent}
key={`season-list-key-${season}`} addToWishlist={props.addToWishlist}
data={data} getEpisodeDetails={props.getEpisodeDetails}
season={season} refreshSubtitles={props.refreshSubtitles}
showName={props.data.get("title")} />
addTorrent={props.addTorrent} );
addToWishlist={props.addToWishlist} })}
getEpisodeDetails={props.getEpisodeDetails}
refreshSubtitles={props.refreshSubtitles}
/>
);
})}
</div> </div>
); );
SeasonsList.propTypes = { SeasonsList.propTypes = {
@ -33,5 +28,5 @@ SeasonsList.propTypes = {
addToWishlist: PropTypes.func, addToWishlist: PropTypes.func,
addTorrent: PropTypes.func, addTorrent: PropTypes.func,
refreshSubtitles: PropTypes.func, refreshSubtitles: PropTypes.func,
getEpisodeDetails: PropTypes.func getEpisodeDetails: PropTypes.func,
}; };

View File

@ -1,11 +1,11 @@
import React from "react"; import React from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { List } from "immutable"; import { List } from "immutable"
import { connect } from "react-redux"; import { connect } from "react-redux"
import { searchEpisodeSubtitles } from "../../../actions/subtitles"; import { searchEpisodeSubtitles } from "../../../actions/subtitles"
import { SubtitlesButton } from "../../buttons/subtitles"; import { SubtitlesButton } from "../../buttons/subtitles"
const episodeSubtitlesButton = ({ const episodeSubtitlesButton = ({
inLibrary, inLibrary,
@ -14,7 +14,7 @@ const episodeSubtitlesButton = ({
episode, episode,
searching, searching,
searchEpisodeSubtitles, searchEpisodeSubtitles,
subtitles subtitles,
}) => ( }) => (
<SubtitlesButton <SubtitlesButton
subtitles={subtitles} subtitles={subtitles}
@ -22,7 +22,7 @@ const episodeSubtitlesButton = ({
searching={searching} searching={searching}
search={() => searchEpisodeSubtitles(imdbId, season, episode)} search={() => searchEpisodeSubtitles(imdbId, season, episode)}
/> />
); )
episodeSubtitlesButton.propTypes = { episodeSubtitlesButton.propTypes = {
inLibrary: PropTypes.bool, inLibrary: PropTypes.bool,
@ -31,9 +31,7 @@ episodeSubtitlesButton.propTypes = {
season: PropTypes.number, season: PropTypes.number,
episode: PropTypes.number, episode: PropTypes.number,
searchEpisodeSubtitles: PropTypes.func, searchEpisodeSubtitles: PropTypes.func,
subtitles: PropTypes.instanceOf(List) subtitles: PropTypes.instanceOf(List),
}; }
export const EpisodeSubtitlesButton = connect(null, { searchEpisodeSubtitles })( export const EpisodeSubtitlesButton = connect(null, {searchEpisodeSubtitles})(episodeSubtitlesButton);
episodeSubtitlesButton
);

View File

@ -1,11 +1,11 @@
import React from "react"; import React from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { connect } from "react-redux"; import { connect } from "react-redux"
import { List } from "immutable"; import { List } from "immutable"
import { getEpisodeDetails } from "../../../actions/shows"; import { getEpisodeDetails } from "../../../actions/shows"
import { TorrentsButton } from "../../buttons/torrents"; import { TorrentsButton } from "../../buttons/torrents"
import { prettyEpisodeName } from "../../../utils"; import { prettyEpisodeName } from "../../../utils"
const episodeTorrentsButton = ({ const episodeTorrentsButton = ({
torrents, torrents,
@ -14,17 +14,17 @@ const episodeTorrentsButton = ({
episode, episode,
showName, showName,
searching, searching,
getEpisodeDetails getEpisodeDetails,
}) => ( }) => (
<TorrentsButton <TorrentsButton
torrents={torrents} torrents={torrents}
searching={searching} searching={searching}
search={() => getEpisodeDetails(imdbId, season, episode)} search={() => getEpisodeDetails(imdbId, season, episode)}
url={`#/torrents/search/shows/${encodeURI( url={`#/torrents/search/shows/${
prettyEpisodeName(showName, season, episode) encodeURI(prettyEpisodeName(showName, season, episode))
)}`} }`}
/> />
); )
episodeTorrentsButton.propTypes = { episodeTorrentsButton.propTypes = {
torrents: PropTypes.instanceOf(List), torrents: PropTypes.instanceOf(List),
showName: PropTypes.string.isRequired, showName: PropTypes.string.isRequired,
@ -32,9 +32,7 @@ episodeTorrentsButton.propTypes = {
episode: PropTypes.number.isRequired, episode: PropTypes.number.isRequired,
season: PropTypes.number.isRequired, season: PropTypes.number.isRequired,
searching: PropTypes.bool.isRequired, searching: PropTypes.bool.isRequired,
getEpisodeDetails: PropTypes.func.isRequired getEpisodeDetails: PropTypes.func.isRequired,
}; };
export const EpisodeTorrentsButton = connect(null, { getEpisodeDetails })( export const EpisodeTorrentsButton = connect(null, {getEpisodeDetails})(episodeTorrentsButton);
episodeTorrentsButton
);

View File

@ -1,39 +1,33 @@
import React from "react"; import React from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { Map } from "immutable"; import { Map } from "immutable"
import { connect } from "react-redux"; import { connect } from "react-redux"
import { import { selectShow, showWishlistToggle,
selectShow, getShowDetails, updateFilter } from "../../actions/shows"
showWishlistToggle,
getShowDetails,
updateFilter
} from "../../actions/shows";
import { isWishlisted } from "../../utils"; import { isWishlisted } from "../../utils"
import ListDetails from "../list/details"; import ListDetails from "../list/details"
import ListPosters from "../list/posters"; import ListPosters from "../list/posters"
function mapStateToProps(state) { function mapStateToProps(state) {
return { return {
loading: state.showsStore.get("loading"), loading : state.showsStore.get("loading"),
shows: state.showsStore.get("shows"), shows : state.showsStore.get("shows"),
filter: state.showsStore.get("filter"), filter : state.showsStore.get("filter"),
selectedImdbId: state.showsStore.get("selectedImdbId"), selectedImdbId : state.showsStore.get("selectedImdbId"),
exploreOptions: state.showsStore.get("exploreOptions") exploreOptions : state.showsStore.get("exploreOptions"),
}; };
} }
const mapDispatchToProps = { const mapDispatchToProps = {
selectShow, selectShow, showWishlistToggle,
showWishlistToggle, getShowDetails, updateFilter,
getShowDetails,
updateFilter
}; };
const ShowList = props => { const ShowList = (props) => {
const showDetails = imdbId => { const showDetails = (imdbId) => {
props.history.push("/shows/details/" + imdbId); props.history.push("/shows/details/" + imdbId);
}; }
let selectedShow = Map(); let selectedShow = Map();
if (props.selectedImdbId !== "") { if (props.selectedImdbId !== "") {
@ -59,18 +53,14 @@ const ShowList = props => {
<ListDetails <ListDetails
data={selectedShow} data={selectedShow}
loading={props.loading} loading={props.loading}
wishlist={() => wishlist={() => props.showWishlistToggle(
props.showWishlistToggle( isWishlisted(selectedShow), selectedShow.get("imdb_id"),
isWishlisted(selectedShow), )}
selectedShow.get("imdb_id")
)
}
> >
<span> <span>
<button <button onClick={
onClick={() => showDetails(selectedShow.get("imdb_id"))} () => showDetails(selectedShow.get("imdb_id"))}
className="btn btn-primary btn-sm w-md-100" className="btn btn-primary btn-sm w-md-100">
>
<i className="fa fa-external-link mr-1" /> <i className="fa fa-external-link mr-1" />
Details Details
</button> </button>
@ -78,7 +68,7 @@ const ShowList = props => {
</ListDetails> </ListDetails>
</div> </div>
); );
}; }
ShowList.propTypes = { ShowList.propTypes = {
match: PropTypes.object, match: PropTypes.object,
history: PropTypes.object, history: PropTypes.object,
@ -90,6 +80,6 @@ ShowList.propTypes = {
showWishlistToggle: PropTypes.func, showWishlistToggle: PropTypes.func,
selectShow: PropTypes.func, selectShow: PropTypes.func,
getShowDetails: PropTypes.func, getShowDetails: PropTypes.func,
updateFilter: PropTypes.func updateFilter: PropTypes.func,
}; };
export default connect(mapStateToProps, mapDispatchToProps)(ShowList); export default connect(mapStateToProps, mapDispatchToProps)(ShowList);

View File

@ -1,21 +1,13 @@
import React from "react"; import React from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { Route } from "react-router-dom"; import { Route } from "react-router-dom"
import { connect } from "react-redux"; import { connect } from "react-redux"
import { import { fetchShows, fetchShowDetails, getShowExploreOptions } from "../../actions/shows"
fetchShows,
fetchShowDetails,
getShowExploreOptions
} from "../../actions/shows";
const mapStateToProps = state => ({ const mapStateToProps = (state) => ({
isExplorerFetched: state.showsStore.get("exploreOptions").size !== 0 isExplorerFetched: (state.showsStore.get("exploreOptions").size !== 0)
}); });
const mapDispatchToProps = { const mapDispatchToProps = { fetchShows, fetchShowDetails, getShowExploreOptions };
fetchShows,
fetchShowDetails,
getShowExploreOptions
};
const ShowsRoute = ({ const ShowsRoute = ({
component: Component, component: Component,
@ -26,51 +18,46 @@ const ShowsRoute = ({
...otherProps ...otherProps
}) => { }) => {
return ( return (
<Route <Route {...otherProps} render={(props) => {
{...otherProps} let fetchUrl = "";
render={props => { switch (props.match.path) {
let fetchUrl = ""; case "/shows/polochon":
switch (props.match.path) { fetchUrl = "/shows/polochon";
case "/shows/polochon": break;
fetchUrl = "/shows/polochon"; case "/shows/wishlist":
break; fetchUrl = "/wishlist/shows";
case "/shows/wishlist": break;
fetchUrl = "/wishlist/shows"; case "/shows/search/:search":
break; fetchUrl = "/shows/search/" + props.match.params.search;
case "/shows/search/:search": break;
fetchUrl = "/shows/search/" + props.match.params.search; case "/shows/explore/:source/:category":
break; if (!isExplorerFetched) {
case "/shows/explore/:source/:category": getShowExploreOptions();
if (!isExplorerFetched) { }
getShowExploreOptions(); fetchUrl = "/shows/explore?source=" +
} encodeURI(props.match.params.source) +
fetchUrl = "&category=" + encodeURI(props.match.params.category);
"/shows/explore?source=" + break;
encodeURI(props.match.params.source) + case "/shows/details/:imdbId":
"&category=" + fetchShowDetails(props.match.params.imdbId);
encodeURI(props.match.params.category); fetchUrl = ""
break; }
case "/shows/details/:imdbId":
fetchShowDetails(props.match.params.imdbId);
fetchUrl = "";
}
if (fetchUrl != "") { if (fetchUrl != "") {
fetchShows(fetchUrl); fetchShows(fetchUrl);
} }
return <Component {...props} />; return <Component {...props} />
}} }} />
/> )
); }
};
ShowsRoute.propTypes = { ShowsRoute.propTypes = {
component: PropTypes.func, component: PropTypes.func,
match: PropTypes.object, match: PropTypes.object,
isExplorerFetched: PropTypes.bool.isRequired, isExplorerFetched: PropTypes.bool.isRequired,
fetchShows: PropTypes.func.isRequired, fetchShows: PropTypes.func.isRequired,
fetchShowDetails: PropTypes.func.isRequired, fetchShowDetails: PropTypes.func.isRequired,
getShowExploreOptions: PropTypes.func.isRequired getShowExploreOptions: PropTypes.func.isRequired,
}; };
export default connect(mapStateToProps, mapDispatchToProps)(ShowsRoute); export default connect(mapStateToProps, mapDispatchToProps)(ShowsRoute);

View File

@ -1,29 +1,26 @@
import React, { useState } from "react"; import React, { useState } from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { Map, List } from "immutable"; import { Map, List } from "immutable"
import { connect } from "react-redux"; import { connect } from "react-redux"
import { prettySize } from "../../utils"; import { prettySize } from "../../utils"
import { import { fetchTorrents, addTorrent, removeTorrent } from "../../actions/torrents"
fetchTorrents,
addTorrent,
removeTorrent
} from "../../actions/torrents";
const mapStateToProps = state => ({ const mapStateToProps = (state) => ({
torrents: state.torrentStore.get("torrents") torrents: state.torrentStore.get("torrents")
}); });
const mapDispatchToProps = { const mapDispatchToProps = {
fetchTorrents, fetchTorrents, addTorrent, removeTorrent,
addTorrent,
removeTorrent
}; };
const TorrentList = props => ( const TorrentList = (props) => (
<div className="row"> <div className="row">
<div className="col-12"> <div className="col-12">
<AddTorrent addTorrent={props.addTorrent} /> <AddTorrent addTorrent={props.addTorrent} />
<Torrents torrents={props.torrents} removeTorrent={props.removeTorrent} /> <Torrents
torrents={props.torrents}
removeTorrent={props.removeTorrent}
/>
</div> </div>
</div> </div>
); );
@ -31,40 +28,38 @@ TorrentList.propTypes = {
fetchTorrents: PropTypes.func.isRequired, fetchTorrents: PropTypes.func.isRequired,
addTorrent: PropTypes.func.isRequired, addTorrent: PropTypes.func.isRequired,
removeTorrent: PropTypes.func.isRequired, removeTorrent: PropTypes.func.isRequired,
torrents: PropTypes.instanceOf(List) torrents: PropTypes.instanceOf(List),
}; };
export default connect(mapStateToProps, mapDispatchToProps)(TorrentList); export default connect(mapStateToProps, mapDispatchToProps)(TorrentList);
const AddTorrent = props => { const AddTorrent = (props) => {
const [url, setUrl] = useState(""); const [url, setUrl] = useState("");
const handleSubmit = e => { const handleSubmit = (e) => {
e.preventDefault(); e.preventDefault();
if (url === "") { if (url === "") { return; }
return;
}
props.addTorrent(url); props.addTorrent(url);
setUrl(""); setUrl("");
}; }
return ( return (
<form onSubmit={e => handleSubmit(e)}> <form onSubmit={(e) => handleSubmit(e)}>
<input <input
type="text" type="text"
className="form-control mb-3 w-100" className="form-control mb-3 w-100"
placeholder="Add torrent URL" placeholder="Add torrent URL"
onSubmit={handleSubmit} onSubmit={handleSubmit}
value={url} value={url}
onChange={e => setUrl(e.target.value)} onChange={(e) => setUrl(e.target.value)}
/> />
</form> </form>
); );
}; }
AddTorrent.propTypes = { AddTorrent.propTypes = {
addTorrent: PropTypes.func.isRequired addTorrent: PropTypes.func.isRequired,
}; };
const Torrents = props => { const Torrents = (props) => {
if (props.torrents.size === 0) { if (props.torrents.size === 0) {
return ( return (
<div className="jumbotron"> <div className="jumbotron">
@ -76,29 +71,31 @@ const Torrents = props => {
return ( return (
<div className="d-flex flex-wrap"> <div className="d-flex flex-wrap">
{props.torrents.map((el, index) => ( {props.torrents.map((el, index) => (
<Torrent key={index} data={el} removeTorrent={props.removeTorrent} /> <Torrent
key={index}
data={el}
removeTorrent={props.removeTorrent}
/>
))} ))}
</div> </div>
); );
}; }
Torrents.propTypes = { Torrents.propTypes = {
removeTorrent: PropTypes.func.isRequired, removeTorrent: PropTypes.func.isRequired,
torrents: PropTypes.instanceOf(List) torrents: PropTypes.instanceOf(List),
}; };
const Torrent = props => { const Torrent = (props) => {
const handleClick = () => { const handleClick = () => {
props.removeTorrent(props.data.get("id")); props.removeTorrent(props.data.get("id"));
}; }
const done = props.data.get("is_finished"); const done = props.data.get("is_finished");
var progressStyle = done var progressStyle = done ? "success" : "info progress-bar-striped progress-bar-animated";
? "success"
: "info progress-bar-striped progress-bar-animated";
const progressBarClass = "progress-bar bg-" + progressStyle; const progressBarClass = "progress-bar bg-" + progressStyle;
var percentDone = props.data.get("percent_done"); var percentDone = props.data.get("percent_done");
const started = percentDone !== 0; const started = (percentDone !== 0);
if (started) { if (started) {
percentDone = Number(percentDone).toFixed(1) + "%"; percentDone = Number(percentDone).toFixed(1) + "%";
} }
@ -111,35 +108,32 @@ const Torrent = props => {
<div className="card w-100 mb-3"> <div className="card w-100 mb-3">
<h5 className="card-header"> <h5 className="card-header">
<span className="text text-break">{props.data.get("name")}</span> <span className="text text-break">{props.data.get("name")}</span>
<span <span className="fa fa-trash clickable pull-right" onClick={() => handleClick()}></span>
className="fa fa-trash clickable pull-right"
onClick={() => handleClick()}
></span>
</h5> </h5>
<div className="card-body pb-0"> <div className="card-body pb-0">
{started && ( {started &&
<React.Fragment> <React.Fragment>
<div className="progress bg-light"> <div className="progress bg-light">
<div <div
className={progressBarClass} className={progressBarClass}
style={{ width: percentDone }} style={{width: percentDone}}
role="progressbar" role="progressbar"
aria-valuenow={percentDone} aria-valuenow={percentDone}
aria-valuemin="0" aria-valuemin="0"
aria-valuemax="100" aria-valuemax="100"
></div> ></div>
</div> </div>
<p> <p>{downloadedSize} / {totalSize} - {percentDone} - {downloadRate}</p>
{downloadedSize} / {totalSize} - {percentDone} - {downloadRate} </React.Fragment>
</p> }
</React.Fragment> {!started &&
)} <p>Download not yet started</p>
{!started && <p>Download not yet started</p>} }
</div> </div>
</div> </div>
); );
}; }
Torrent.propTypes = { Torrent.propTypes = {
removeTorrent: PropTypes.func.isRequired, removeTorrent: PropTypes.func.isRequired,
data: PropTypes.instanceOf(Map) data: PropTypes.instanceOf(Map),
}; };

View File

@ -1,37 +1,34 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { connect } from "react-redux"; import { connect } from "react-redux"
import { addTorrent, searchTorrents } from "../../actions/torrents"; import { addTorrent, searchTorrents } from "../../actions/torrents"
import { Map, List } from "immutable"; import { Map, List } from "immutable"
import Loader from "../loader/loader"; import Loader from "../loader/loader"
import { OverlayTrigger, Tooltip } from "react-bootstrap"; import { OverlayTrigger, Tooltip } from "react-bootstrap"
import { prettySize } from "../../utils"; import { prettySize } from "../../utils"
const mapStateToProps = state => ({ const mapStateToProps = (state) => ({
searching: state.torrentStore.get("searching"), searching: state.torrentStore.get("searching"),
results: state.torrentStore.get("searchResults") results: state.torrentStore.get("searchResults"),
}); });
const mapDispatchToProps = { addTorrent, searchTorrents }; const mapDispatchToProps = { addTorrent, searchTorrents };
const TorrentSearch = props => { const TorrentSearch = (props) => {
const [search, setSearch] = useState(props.match.params.search || ""); const [search, setSearch] = useState(props.match.params.search || "");
const [type, setType] = useState(props.match.params.type || ""); const [type, setType] = useState(props.match.params.type || "");
const [url, setUrl] = useState(""); const [url, setUrl] = useState("");
const getUrl = () => `/torrents/search/${type}/${encodeURI(search)}`; const getUrl = () =>
`/torrents/search/${type}/${encodeURI(search)}`;
useEffect(() => { useEffect(() => {
if (search === "") { if (search === "") { return }
return; if (type === "") { return }
}
if (type === "") {
return;
}
const url = getUrl(); const url = getUrl();
props.searchTorrents(url); props.searchTorrents(url)
props.history.push(url); props.history.push(url);
}, [url]); }, [url]);
@ -43,7 +40,7 @@ const TorrentSearch = props => {
className="form-control mb-1 w-100 form-control-lg" className="form-control mb-1 w-100 form-control-lg"
placeholder="Search torrents" placeholder="Search torrents"
value={search} value={search}
onChange={e => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
/> />
<div className="mb-3 w-100 d-flex"> <div className="mb-3 w-100 d-flex">
<SearchButton <SearchButton
@ -53,8 +50,7 @@ const TorrentSearch = props => {
handleClick={() => { handleClick={() => {
setType("movies"); setType("movies");
setUrl(getUrl()); setUrl(getUrl());
}} }}/>
/>
<SearchButton <SearchButton
text="Search shows" text="Search shows"
type="shows" type="shows"
@ -62,8 +58,7 @@ const TorrentSearch = props => {
handleClick={() => { handleClick={() => {
setType("shows"); setType("shows");
setUrl(getUrl()); setUrl(getUrl());
}} }}/>
/>
</div> </div>
</div> </div>
<div className="col-12"> <div className="col-12">
@ -76,7 +71,7 @@ const TorrentSearch = props => {
</div> </div>
</div> </div>
); );
}; }
TorrentSearch.propTypes = { TorrentSearch.propTypes = {
searching: PropTypes.bool.isRequired, searching: PropTypes.bool.isRequired,
results: PropTypes.instanceOf(List), results: PropTypes.instanceOf(List),
@ -84,31 +79,32 @@ TorrentSearch.propTypes = {
match: PropTypes.object, match: PropTypes.object,
history: PropTypes.object, history: PropTypes.object,
addTorrent: PropTypes.func.isRequired, addTorrent: PropTypes.func.isRequired,
searchTorrents: PropTypes.func.isRequired searchTorrents: PropTypes.func.isRequired,
}; };
const SearchButton = props => {
const variant = props.type === props.typeFromURL ? "primary" : "secondary"; const SearchButton = (props) => {
const variant = (props.type === props.typeFromURL) ? "primary" : "secondary";
return ( return (
<button <button
type="button" type="button"
className={`w-50 btn m-1 btn-lg btn-${variant}`} className={`w-50 btn m-1 btn-lg btn-${variant}`}
onClick={props.handleClick} onClick={props.handleClick}
> >
<i className="fa fa-search" aria-hidden="true"></i> {props.text} <i className="fa fa-search" aria-hidden="true"></i> {props.text}
</button> </button>
); );
}; }
SearchButton.propTypes = { SearchButton.propTypes = {
type: PropTypes.string, type: PropTypes.string,
typeFromURL: PropTypes.string, typeFromURL: PropTypes.string,
text: PropTypes.string, text: PropTypes.string,
handleClick: PropTypes.func.isRequired handleClick: PropTypes.func.isRequired,
}; };
const TorrentList = props => { const TorrentList = (props) => {
if (props.searching) { if (props.searching) {
return <Loader />; return (<Loader />);
} }
if (props.searchFromURL === "") { if (props.searchFromURL === "") {
@ -126,65 +122,59 @@ const TorrentList = props => {
return ( return (
<React.Fragment> <React.Fragment>
{props.results.map(function(el, index) { {props.results.map(function(el, index) {
return <Torrent key={index} data={el} addTorrent={props.addTorrent} />; return (
<Torrent
key={index}
data={el}
addTorrent={props.addTorrent}
/>);
})} })}
</React.Fragment> </React.Fragment>
); );
}; }
TorrentList.propTypes = { TorrentList.propTypes = {
searching: PropTypes.bool.isRequired, searching: PropTypes.bool.isRequired,
results: PropTypes.instanceOf(List), results: PropTypes.instanceOf(List),
searchFromURL: PropTypes.string, searchFromURL: PropTypes.string,
addTorrent: PropTypes.func.isRequired addTorrent: PropTypes.func.isRequired,
}; };
const Torrent = props => ( const Torrent = (props) => (
<div className="alert d-flex border-bottom border-secondary align-items-center"> <div className="alert d-flex border-bottom border-secondary align-items-center">
<TorrentHealth <TorrentHealth
url={props.data.get("url")} url={props.data.get("url")}
seeders={props.data.get("seeders")} seeders={props.data.get("seeders")}
leechers={props.data.get("leechers")} leechers={props.data.get("leechers")}
/> />
<span className="mx-3 text text-start text-break flex-fill"> <span className="mx-3 text text-start text-break flex-fill">{props.data.get("name")}</span>
{props.data.get("name")}
</span>
<div> <div>
{props.data.get("size") !== 0 && ( {props.data.get("size") !== 0 &&
<span className="mx-1 badge badge-pill badge-warning"> <span className="mx-1 badge badge-pill badge-warning">
{prettySize(props.data.get("size"))} {prettySize(props.data.get("size"))}
</span> </span>
)} }
<span className="mx-1 badge badge-pill badge-warning"> <span className="mx-1 badge badge-pill badge-warning">{props.data.get("quality")}</span>
{props.data.get("quality")} <span className="mx-1 badge badge-pill badge-success">{props.data.get("source")}</span>
</span> <span className="mx-1 badge badge-pill badge-info">{props.data.get("upload_user")}</span>
<span className="mx-1 badge badge-pill badge-success">
{props.data.get("source")}
</span>
<span className="mx-1 badge badge-pill badge-info">
{props.data.get("upload_user")}
</span>
</div> </div>
<div className="align-self-end ml-3"> <div className="align-self-end ml-3">
<i <i className="fa fa-cloud-download clickable" onClick={() => props.addTorrent(props.data.get("url"))}></i>
className="fa fa-cloud-download clickable"
onClick={() => props.addTorrent(props.data.get("url"))}
></i>
</div> </div>
</div> </div>
); );
Torrent.propTypes = { Torrent.propTypes = {
data: PropTypes.instanceOf(Map), data: PropTypes.instanceOf(Map),
addTorrent: PropTypes.func.isRequired addTorrent: PropTypes.func.isRequired,
}; };
const TorrentHealth = props => { const TorrentHealth = (props) => {
const seeders = props.seeders || 0; const seeders = props.seeders || 0;
const leechers = props.leechers || 1; const leechers = props.leechers || 1;
let color; let color;
let health; let health;
let ratio = seeders / leechers; let ratio = seeders/leechers;
if (seeders > 20) { if (seeders > 20) {
health = "good"; health = "good";
@ -202,9 +192,7 @@ const TorrentHealth = props => {
const className = `align-self-start text text-center text-${color}`; const className = `align-self-start text text-center text-${color}`;
const tooltip = ( const tooltip = (
<Tooltip id={`tooltip-health-${props.url}`}> <Tooltip id={`tooltip-health-${props.url}`}>
<p> <p><span className={className}>Health: {health}</span></p>
<span className={className}>Health: {health}</span>
</p>
<p>Seeders: {seeders}</p> <p>Seeders: {seeders}</p>
<p>Leechers: {props.leechers}</p> <p>Leechers: {props.leechers}</p>
</Tooltip> </Tooltip>
@ -217,11 +205,11 @@ const TorrentHealth = props => {
</span> </span>
</OverlayTrigger> </OverlayTrigger>
); );
}; }
TorrentHealth.propTypes = { TorrentHealth.propTypes = {
url: PropTypes.string, url: PropTypes.string,
seeders: PropTypes.number, seeders: PropTypes.number,
leechers: PropTypes.number leechers: PropTypes.number,
}; };
export default connect(mapStateToProps, mapDispatchToProps)(TorrentSearch); export default connect(mapStateToProps, mapDispatchToProps)(TorrentSearch);

View File

@ -1,20 +1,20 @@
import React from "react"; import React from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { connect } from "react-redux"; import { connect } from "react-redux"
import { Redirect, Link } from "react-router-dom"; import { Redirect, Link } from "react-router-dom"
const mapStateToProps = state => ({ const mapStateToProps = (state) => ({
isActivated: state.userStore.get("isActivated"), isActivated: state.userStore.get("isActivated"),
isLogged: state.userStore.get("isLogged") isLogged: state.userStore.get("isLogged"),
}); });
const UserActivation = props => { const UserActivation = (props) => {
if (!props.isLogged) { if (!props.isLogged) {
return <Redirect to="/users/login" />; return (<Redirect to="/users/login"/>);
} }
if (props.isActivated) { if (props.isActivated) {
return <Redirect to="/" />; return (<Redirect to="/"/>);
} }
return ( return (
@ -22,18 +22,15 @@ const UserActivation = props => {
<div className="col-12 col-md-8 offset-md-2"> <div className="col-12 col-md-8 offset-md-2">
<h2>Waiting for activation</h2> <h2>Waiting for activation</h2>
<hr /> <hr />
<h3> <h3>Hang tight! Your user will soon be activated by the administrators of this site.</h3>
Hang tight! Your user will soon be activated by the administrators of
this site.
</h3>
<Link to="/users/logout">Logout</Link> <Link to="/users/logout">Logout</Link>
</div> </div>
</div> </div>
); );
}; }
UserActivation.propTypes = { UserActivation.propTypes = {
isActivated: PropTypes.bool.isRequired, isActivated: PropTypes.bool.isRequired,
isLogged: PropTypes.bool.isRequired isLogged: PropTypes.bool.isRequired,
}; };
export default connect(mapStateToProps)(UserActivation); export default connect(mapStateToProps)(UserActivation);

View File

@ -1,26 +1,24 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { connect } from "react-redux"; import { connect } from "react-redux"
import Loader from "../loader/loader"; import Loader from "../loader/loader"
import { List } from "immutable"; import { List } from "immutable"
import { getUserInfos, updateUser } from "../../actions/users"; import { getUserInfos, updateUser } from "../../actions/users"
import { getPolochons } from "../../actions/polochon"; import { getPolochons } from "../../actions/polochon"
import { PolochonSelect } from "../polochons/select"; import { PolochonSelect } from "../polochons/select"
const mapStateToProps = state => ({ const mapStateToProps = (state) => ({
loading: state.userStore.get("loading"), loading: state.userStore.get("loading"),
publicPolochons: state.polochon.get("public"), publicPolochons: state.polochon.get("public"),
polochonId: state.userStore.get("polochonId"), polochonId: state.userStore.get("polochonId"),
polochonActivated: state.userStore.get("polochonActivated") polochonActivated: state.userStore.get("polochonActivated"),
}); });
const mapDispatchToProps = { const mapDispatchToProps = {
updateUser, updateUser, getPolochons, getUserInfos,
getPolochons, }
getUserInfos
};
const UserEditConnect = ({ const UserEditConnect = ({
loading, loading,
@ -29,7 +27,7 @@ const UserEditConnect = ({
updateUser, updateUser,
getPolochons, getPolochons,
getUserInfos, getUserInfos,
publicPolochons publicPolochons,
}) => { }) => {
const [id, setId] = useState(polochonId); const [id, setId] = useState(polochonId);
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
@ -38,45 +36,37 @@ const UserEditConnect = ({
useEffect(() => { useEffect(() => {
getPolochons(); getPolochons();
getUserInfos(); getUserInfos();
}, [getPolochons, getUserInfos]); }, [getPolochons, getUserInfos])
useEffect(() => { useEffect(() => {
setId(polochonId); setId(polochonId);
}, [polochonId]); }, [polochonId])
const handleSubmit = ev => { const handleSubmit = (ev) => {
ev.preventDefault(); ev.preventDefault();
updateUser({ updateUser({
password: password, "password": password,
password_confirm: passwordConfirm, // eslint-disable-line camelcase "password_confirm": passwordConfirm,
polochon_id: id // eslint-disable-line camelcase "polochon_id": id,
}); });
};
if (loading) {
return <Loader />;
} }
if (loading) { return (<Loader />) }
return ( return (
<div className="row mb-3"> <div className="row mb-3">
<div className="col-12 col-md-8 offset-md-2"> <div className="col-12 col-md-8 offset-md-2">
<h2>Edit user</h2> <h2>Edit user</h2>
<hr /> <hr />
<form className="form-horizontal" onSubmit={ev => handleSubmit(ev)}> <form className="form-horizontal" onSubmit={(ev) => handleSubmit(ev)}>
<div className="form-group"> <div className="form-group">
<label className="control-label"> <label className="control-label">
Polochon Polochon
{polochonActivated || ( {polochonActivated ||
<span className="ml-1 text text-primary"> <span className="ml-1 text text-primary">(Needs activation from admin)</span>
(Needs activation from admin) }
</span>
)}
</label> </label>
<PolochonSelect <PolochonSelect value={id} changeValue={setId} polochonList={publicPolochons} />
value={id}
changeValue={setId}
polochonList={publicPolochons}
/>
</div> </div>
<hr /> <hr />
@ -88,7 +78,7 @@ const UserEditConnect = ({
type="password" type="password"
autoComplete="off" autoComplete="off"
value={password} value={password}
onChange={e => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
/> />
</div> </div>
@ -99,22 +89,18 @@ const UserEditConnect = ({
type="password" type="password"
autoComplete="off" autoComplete="off"
value={passwordConfirm} value={passwordConfirm}
onChange={e => setPasswordConfirm(e.target.value)} onChange={(e) => setPasswordConfirm(e.target.value)}
/> />
</div> </div>
<div> <div>
<input <input type="submit" className="btn btn-primary pull-right" value="Update"/>
type="submit"
className="btn btn-primary pull-right"
value="Update"
/>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
); );
}; }
UserEditConnect.propTypes = { UserEditConnect.propTypes = {
loading: PropTypes.bool.isRequired, loading: PropTypes.bool.isRequired,
polochonId: PropTypes.string, polochonId: PropTypes.string,
@ -122,10 +108,7 @@ UserEditConnect.propTypes = {
updateUser: PropTypes.func, updateUser: PropTypes.func,
getPolochons: PropTypes.func, getPolochons: PropTypes.func,
getUserInfos: PropTypes.func, getUserInfos: PropTypes.func,
publicPolochons: PropTypes.instanceOf(List) publicPolochons: PropTypes.instanceOf(List),
}; };
export const UserEdit = connect( export const UserEdit = connect(mapStateToProps, mapDispatchToProps)(UserEditConnect);
mapStateToProps,
mapDispatchToProps
)(UserEditConnect);

View File

@ -1,27 +1,27 @@
import React, { useState } from "react"; import React, { useState } from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { connect } from "react-redux"; import { connect } from "react-redux"
import { Redirect, Link } from "react-router-dom"; import { Redirect, Link } from "react-router-dom"
import { loginUser } from "../../actions/users"; import { loginUser } from "../../actions/users"
const mapStateToProps = state => ({ const mapStateToProps = (state) => ({
isLogged: state.userStore.get("isLogged"), isLogged: state.userStore.get("isLogged"),
isLoading: state.userStore.get("loading"), isLoading: state.userStore.get("loading"),
error: state.userStore.get("error") error: state.userStore.get("error"),
}); });
const mapDispatchToProps = { loginUser }; const mapDispatchToProps = { loginUser };
const UserLoginForm = props => { const UserLoginForm = (props) => {
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const handleSubmit = e => { const handleSubmit = (e) => {
e.preventDefault(); e.preventDefault();
if (!props.isLoading) { if (!props.isLoading) {
props.loginUser(username, password); props.loginUser(username, password);
} }
}; }
if (props.isLogged) { if (props.isLogged) {
return <Redirect to="/" />; return <Redirect to="/" />;
@ -31,33 +31,25 @@ const UserLoginForm = props => {
<div className="row"> <div className="row">
<div className="col-10 offset-1 col-md-6 offset-md-3"> <div className="col-10 offset-1 col-md-6 offset-md-3">
<h2>Log in</h2> <h2>Log in</h2>
<hr /> <hr/>
{props.error && props.error !== "" && ( {props.error && props.error !== "" &&
<div className="alert alert-danger">{props.error}</div> <div className="alert alert-danger">
)} {props.error}
<form className="form-horizontal" onSubmit={e => handleSubmit(e)}> </div>
}
<form className="form-horizontal" onSubmit={(e) => handleSubmit(e)}>
<div> <div>
<label>Username</label> <label>Username</label>
<br /> <br/>
<input <input className="form-control" type="username" autoFocus
className="form-control" value={username} onChange={(e) => setUsername(e.target.value)}/>
type="username"
autoFocus
value={username}
onChange={e => setUsername(e.target.value)}
/>
<p></p> <p></p>
</div> </div>
<div> <div>
<label>Password</label> <label>Password</label>
<br /> <br/>
<input <input className="form-control" type="password" autoComplete="new-password"
className="form-control" value={password} onChange={(e) => setPassword(e.target.value)}/>
type="password"
autoComplete="new-password"
value={password}
onChange={e => setPassword(e.target.value)}
/>
<p></p> <p></p>
</div> </div>
<div> <div>
@ -66,32 +58,28 @@ const UserLoginForm = props => {
No account yet ? <Link to="/users/signup">Create one</Link> No account yet ? <Link to="/users/signup">Create one</Link>
</small> </small>
</span> </span>
{props.isLoading && ( {props.isLoading &&
<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>
)} }
{props.isLoading || ( {props.isLoading ||
<span className="spaced-icons"> <span className="spaced-icons">
<input <input className="btn btn-primary pull-right" type="submit" value="Log in"/>
className="btn btn-primary pull-right" </span>
type="submit" }
value="Log in" <br/>
/>
</span>
)}
<br />
</div> </div>
</form> </form>
</div> </div>
</div> </div>
); );
}; }
UserLoginForm.propTypes = { UserLoginForm.propTypes = {
loginUser: PropTypes.func.isRequired, loginUser: PropTypes.func.isRequired,
isLoading: PropTypes.bool.isRequired, isLoading: PropTypes.bool.isRequired,
isLogged: PropTypes.bool.isRequired, isLogged: PropTypes.bool.isRequired,
error: PropTypes.string error: PropTypes.string,
}; };
export default connect(mapStateToProps, mapDispatchToProps)(UserLoginForm); export default connect(mapStateToProps, mapDispatchToProps)(UserLoginForm);

View File

@ -1,26 +1,28 @@
import React from "react"; import React from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { connect } from "react-redux"; import { connect } from "react-redux"
import { Redirect } from "react-router-dom"; import { Redirect } from "react-router-dom"
import { userLogout } from "../../actions/users"; import { userLogout } from "../../actions/users"
const mapStateToProps = state => ({ const mapStateToProps = (state) => ({
isLogged: state.userStore.get("isLogged") isLogged: state.userStore.get("isLogged"),
}); });
const mapDispatchToProps = { userLogout }; const mapDispatchToProps = { userLogout };
const UserLogout = props => { const UserLogout = (props) => {
if (props.isLogged) { if (props.isLogged) {
props.userLogout(); props.userLogout();
} }
return <Redirect to="/users/login" />; return (
}; <Redirect to="/users/login" />
);
}
UserLogout.propTypes = { UserLogout.propTypes = {
isLogged: PropTypes.bool.isRequired, isLogged: PropTypes.bool.isRequired,
userLogout: PropTypes.func.isRequired, userLogout: PropTypes.func.isRequired,
history: PropTypes.object history: PropTypes.object,
}; };
export default connect(mapStateToProps, mapDispatchToProps)(UserLogout); export default connect(mapStateToProps, mapDispatchToProps)(UserLogout);

View File

@ -1,38 +1,46 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { connect } from "react-redux"; import { connect } from "react-redux"
import { Map } from "immutable"; import { Map } from "immutable"
import { PolochonList } from "../polochons/list"; import { PolochonList } from "../polochons/list"
import { UserEdit } from "./edit"; import { UserEdit } from "./edit"
import { getUserModules } from "../../actions/users"; import { getUserModules } from "../../actions/users"
import Modules from "../modules/modules"; import Modules from "../modules/modules"
const mapStateToProps = state => ({ const mapStateToProps = (state) => ({
modules: state.userStore.get("modules"), modules : state.userStore.get("modules"),
modulesLoading: state.userStore.get("modulesLoading") modulesLoading : state.userStore.get("modulesLoading"),
}); });
const mapDispatchToProps = { getUserModules }; const mapDispatchToProps = { getUserModules }
const UserProfile = ({ modules, modulesLoading, getUserModules }) => { const UserProfile = ({
modules,
modulesLoading,
getUserModules
}) => {
useEffect(() => { useEffect(() => {
getUserModules(); getUserModules();
}, [getUserModules]); }, [getUserModules])
return ( return (
<div> <div>
<UserEdit /> <UserEdit />
<PolochonList /> <PolochonList />
<Modules modules={modules} isLoading={modulesLoading} /> <Modules
modules={modules}
isLoading={modulesLoading}
/>
</div> </div>
); )
}; }
UserProfile.propTypes = { UserProfile.propTypes = {
getUserModules: PropTypes.func.isRequired, getUserModules: PropTypes.func.isRequired,
modules: PropTypes.instanceOf(Map), modules: PropTypes.instanceOf(Map),
modulesLoading: PropTypes.bool.isRequired modulesLoading: PropTypes.bool.isRequired,
}; };
export default connect(mapStateToProps, mapDispatchToProps)(UserProfile); export default connect(mapStateToProps, mapDispatchToProps)(UserProfile);

View File

@ -1,100 +1,86 @@
import React, { useState } from "react"; import React, { useState } from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { connect } from "react-redux"; import { connect } from "react-redux"
import { Redirect } from "react-router-dom"; import { Redirect } from "react-router-dom"
import { userSignUp } from "../../actions/users"; import { userSignUp } from "../../actions/users"
const mapStateToProps = state => ({ const mapStateToProps = (state) => ({
isLogged: state.userStore.get("isLogged"), isLogged: state.userStore.get("isLogged"),
isLoading: state.userStore.get("loading"), isLoading: state.userStore.get("loading"),
error: state.userStore.get("error") error: state.userStore.get("error"),
}); });
const mapDispatchToProps = { userSignUp }; const mapDispatchToProps = { userSignUp };
const UserSignUp = props => { const UserSignUp = (props) => {
if (props.isLogged) {
return (<Redirect to="/"/>);
}
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [passwordConfirm, setPasswordConfirm] = useState(""); const [passwordConfirm, setPasswordConfirm] = useState("");
if (props.isLogged) { const handleSubmit = (e) => {
return <Redirect to="/" />;
}
const handleSubmit = e => {
e.preventDefault(); e.preventDefault();
props.userSignUp({ props.userSignUp({
username: username, "username": username,
password: password, "password": password,
password_confirm: passwordConfirm // eslint-disable-line camelcase "password_confirm": passwordConfirm,
}); });
}; }
return ( return (
<div className="Row"> <div className="Row">
<div className="col-10 offset-1 col-md-6 offset-md-3"> <div className="col-10 offset-1 col-md-6 offset-md-3">
<h2>Sign up</h2> <h2>Sign up</h2>
<hr /> <hr />
{props.error && props.error !== "" && ( {props.error && props.error !== "" &&
<div className="alert alert-danger">{props.error}</div> <div className="alert alert-danger">
)} {props.error}
</div>
}
<form className="form-horizontal" onSubmit={e => handleSubmit(e)}> <form className="form-horizontal" onSubmit={(e) => handleSubmit(e)}>
<div className="form-group"> <div className="form-group">
<label className="control-label">Username</label> <label className="control-label">Username</label>
<input <input autoFocus="autofocus" className="form-control"
autoFocus="autofocus" value={username} onChange={(e) => setUsername(e.target.value)} />
className="form-control"
value={username}
onChange={e => setUsername(e.target.value)}
/>
</div> </div>
<div className="form-group"> <div className="form-group">
<label className="control-label">Password</label> <label className="control-label">Password</label>
<input <input className="form-control" type="password"
className="form-control" value={password} onChange={(e) => setPassword(e.target.value)} />
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
/>
</div> </div>
<div className="form-group"> <div className="form-group">
<label className="control-label">Password confirm</label> <label className="control-label">Password confirm</label>
<input <input className="form-control" type="password"
className="form-control" value={passwordConfirm} onChange={(e) => setPasswordConfirm(e.target.value)} />
type="password"
value={passwordConfirm}
onChange={e => setPasswordConfirm(e.target.value)}
/>
</div> </div>
<div> <div>
{props.isLoading && ( {props.isLoading &&
<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>
)} }
{props.isLoading || ( {props.isLoading ||
<span> <span>
<input <input className="btn btn-primary pull-right" type="submit" value="Sign up"/>
className="btn btn-primary pull-right" </span>
type="submit" }
value="Sign up"
/>
</span>
)}
</div> </div>
</form> </form>
</div> </div>
</div> </div>
); );
}; }
UserSignUp.propTypes = { UserSignUp.propTypes = {
isLogged: PropTypes.bool.isRequired, isLogged: PropTypes.bool.isRequired,
isLoading: PropTypes.bool.isRequired, isLoading: PropTypes.bool.isRequired,
userSignUp: PropTypes.func.isRequired, userSignUp: PropTypes.func.isRequired,
error: PropTypes.string error: PropTypes.string,
}; };
export default connect(mapStateToProps, mapDispatchToProps)(UserSignUp); export default connect(mapStateToProps, mapDispatchToProps)(UserSignUp);

View File

@ -1,18 +1,18 @@
import React, { useState } from "react"; import React, { useState } from "react"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import { connect } from "react-redux"; import { connect } from "react-redux"
import { UAParser } from "ua-parser-js"; import { UAParser } from "ua-parser-js"
import moment from "moment"; import moment from "moment"
import { Map, List } from "immutable"; import { Map, List } from "immutable"
import { getUserTokens, deleteUserToken } from "../../actions/users"; import { getUserTokens, deleteUserToken } from "../../actions/users"
const mapStateToProps = state => ({ const mapStateToProps = (state) => ({
tokens: state.userStore.get("tokens") tokens: state.userStore.get("tokens"),
}); });
const mapDispatchToProps = { getUserTokens, deleteUserToken }; const mapDispatchToProps = { getUserTokens, deleteUserToken };
const UserTokens = props => { const UserTokens = (props) => {
const [fetched, setIsFetched] = useState(false); const [fetched, setIsFetched] = useState(false);
if (!fetched) { if (!fetched) {
props.getUserTokens(); props.getUserTokens();
@ -28,15 +28,15 @@ const UserTokens = props => {
</div> </div>
</div> </div>
); );
}; }
UserTokens.propTypes = { UserTokens.propTypes = {
tokens: PropTypes.instanceOf(List), tokens: PropTypes.instanceOf(List),
isLoading: PropTypes.bool, isLoading: PropTypes.bool.isRequired,
getUserTokens: PropTypes.func.isRequired, getUserTokens: PropTypes.func.isRequired,
deleteUserToken: PropTypes.func.isRequired deleteUserToken: PropTypes.func.isRequired,
}; };
const Token = props => { const Token = (props) => {
const ua = UAParser(props.data.get("user_agent")); const ua = UAParser(props.data.get("user_agent"));
return ( return (
<div className="card mt-3"> <div className="card mt-3">
@ -54,49 +54,43 @@ const Token = props => {
<p>Created: {moment(props.data.get("created_at")).fromNow()}</p> <p>Created: {moment(props.data.get("created_at")).fromNow()}</p>
</div> </div>
<div className="col-12 col-md-6"> <div className="col-12 col-md-6">
<p> <p>Device: <Device {...ua.device}/></p>
Device: <Device {...ua.device} /> <p>OS: <OS {...ua.os}/></p>
</p> <p>Browser: <Browser {...ua.browser}/></p>
<p>
OS: <OS {...ua.os} />
</p>
<p>
Browser: <Browser {...ua.browser} />
</p>
</div> </div>
</div> </div>
</div> </div>
); );
}; }
Token.propTypes = { Token.propTypes = {
data: PropTypes.instanceOf(Map).isRequired data: PropTypes.instanceOf(Map).isRequired,
}; };
const Actions = props => { const Actions = (props) => {
const handleClick = () => { const handleClick = () => {
props.deleteToken(props.data.get("token")); props.deleteToken(props.data.get("token"));
}; }
return ( return (
<span <span
className="fa fa-trash fa-lg pull-right clickable" className="fa fa-trash fa-lg pull-right clickable"
onClick={handleClick} onClick={handleClick}>
></span> </span>
); );
}; }
Actions.propTypes = { Actions.propTypes = {
data: PropTypes.instanceOf(Map).isRequired, data: PropTypes.instanceOf(Map).isRequired,
deleteToken: PropTypes.func.isRequired deleteToken: PropTypes.func.isRequired,
}; };
const Logo = ({ ua, device, browser }) => { const Logo = (props) => {
var className; var className;
if (ua === "canape-cli") { if (props.ua === "canape-cli") {
className = "terminal"; className = "terminal";
} else if (device.type == "mobile") { } else if (props.device.type == "mobile" ){
className = "mobile"; className = "mobile";
} else { } else {
switch (browser.name) { switch (props.browser.name) {
case "Chrome": case "Chrome":
case "chrome": case "chrome":
className = "chrome"; className = "chrome";
@ -115,60 +109,50 @@ const Logo = ({ ua, device, browser }) => {
} }
} }
return <span className={`fa fa-${className}`}></span>; return (<span className={`fa fa-${className}`}></span>);
}; }
Logo.propTypes = {
ua: PropTypes.string,
device: PropTypes.object,
browser: PropTypes.object
};
const OS = ({ name, version }) => { const OS = (props) => {
var osName = "-"; var osName = "-";
if (name !== undefined) { if (props.name !== undefined) {
osName = name; osName = props.name;
if (version !== undefined) { if (props.version !== undefined) {
osName += " " + version; osName += " " + props.version;
} }
} }
return <span> {osName}</span>; return (
}; <span> {osName}</span>
OS.propTypes = { );
name: PropTypes.string, }
version: PropTypes.string
};
const Device = ({ model }) => { const Device = (props) => {
var deviceName = "-"; var deviceName = "-";
if (model !== undefined) { if (props.model !== undefined) {
deviceName = model; deviceName = props.model;
} }
return <span> {deviceName}</span>; return (
}; <span> {deviceName}</span>
Device.propTypes = { );
model: PropTypes.string }
};
const Browser = ({ name, version }) => { const Browser = (props) => {
var browserName = "-"; var browserName = "-";
if (name !== undefined) { if (props.name !== undefined) {
browserName = name; browserName = props.name;
if (version !== undefined) { if (props.version !== undefined) {
browserName += " - " + version; browserName += " - " + props.version;
} }
} }
return <span> {browserName}</span>; return (
}; <span> {browserName}</span>
Browser.propTypes = { );
name: PropTypes.string, }
version: PropTypes.string
};
export default connect(mapStateToProps, mapDispatchToProps)(UserTokens); export default connect(mapStateToProps, mapDispatchToProps)(UserTokens);

View File

@ -1,26 +1,22 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react"
import { connect } from "react-redux"; import { connect } from "react-redux"
import { setFetchedTorrents } from "../actions/torrents"; import { setFetchedTorrents } from "../actions/torrents"
import { newMovieEvent } from "../actions/movies"; import { newMovieEvent } from "../actions/movies"
import { newEpisodeEvent } from "../actions/shows"; import { newEpisodeEvent } from "../actions/shows"
const mapStateToProps = state => ({ const mapStateToProps = (state) => ({
isLogged: state.userStore.get("isLogged") isLogged: state.userStore.get("isLogged"),
}); });
const mapDispatchToProps = { const mapDispatchToProps = { setFetchedTorrents, newMovieEvent, newEpisodeEvent };
setFetchedTorrents,
newMovieEvent,
newEpisodeEvent
};
const WsHandler = props => { const WsHandler = (props) => {
const [ws, setWs] = useState(null); const [ws, setWs] = useState(null);
useEffect(() => { useEffect(() => {
const intervalID = setInterval(() => { const intervalID = setInterval(() => {
if (!ws) { if (!ws) {
connect(); connect();
return; return
} }
if (ws.readyState === WebSocket.CLOSED) { if (ws.readyState === WebSocket.CLOSED) {
@ -42,51 +38,41 @@ const WsHandler = props => {
const stop = () => { const stop = () => {
if (!ws) { if (!ws) {
return; return
} }
ws.close(); ws.close()
setWs(null); setWs(null);
}; }
const connect = () => { const connect = () => {
if (ws) { if (ws) { return; }
return;
}
if (!props.isLogged) { if (!props.isLogged) {
stop(); stop();
} }
const type = location.protocol === "https:" ? "wss" : "ws"; const type = (location.protocol === "https:") ? "wss" : "ws";
const socket = new WebSocket(type + ":" + location.host + "/events"); const socket = new WebSocket(type + ":" + location.host + "/events")
socket.onopen = () => { socket.onopen = () => {
socket.send( socket.send(JSON.stringify({
JSON.stringify({ "type": "subscribe",
type: "subscribe", "message": "torrents",
message: "torrents" }))
}) socket.send(JSON.stringify({
); "type": "subscribe",
socket.send( "message": "newVideo",
JSON.stringify({ }))
type: "subscribe", }
message: "newVideo"
})
);
};
socket.onmessage = event => { socket.onmessage = (event) => {
if (!event.data) { if (!event.data) { return }
return;
}
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
if (!data.type) { if (!data.type){ return }
return;
}
if (data.status !== "ok") { if (data.status !== "ok") {
// TODO: handle this somehow // TODO: handle this somehow
return; return
} }
switch (data.type) { switch (data.type) {
@ -95,22 +81,20 @@ const WsHandler = props => {
break; break;
case "newVideo": case "newVideo":
if (data.message.type === "movie") { if (data.message.type === "movie") {
props.newMovieEvent(data.message.data); props.newMovieEvent(data.message.data)
} else if (data.message.type === "episode") { } else if (data.message.type === "episode") {
props.newEpisodeEvent(data.message.data); props.newEpisodeEvent(data.message.data)
} }
break; break;
} }
}; }
socket.onerror = () => { socket.onerror = () => { stop(); }
stop();
};
setWs(socket); setWs(socket)
}; }
return null; return null;
}; }
export default connect(mapStateToProps, mapDispatchToProps)(WsHandler); export default connect(mapStateToProps, mapDispatchToProps)(WsHandler);

View File

@ -1,26 +1,21 @@
import { Map, List, fromJS } from "immutable"; import { Map, List, fromJS } from "immutable"
export const defaultState = Map({ export const defaultState = Map({
fetchingModules: false, "fetchingModules": false,
users: List(), "users": List(),
stats: Map({}), "stats": Map({}),
modules: Map({}) "modules": Map({}),
}); });
const handlers = { const handlers = {
ADMIN_LIST_USERS_FULFILLED: (state, action) => "ADMIN_LIST_USERS_FULFILLED": (state, action) => state.set("users", fromJS(action.payload.response.data)),
state.set("users", fromJS(action.payload.response.data)), "ADMIN_GET_STATS_FULFILLED": (state, action) => state.set("stats", fromJS(action.payload.response.data)),
ADMIN_GET_STATS_FULFILLED: (state, action) => "ADMIN_GET_MODULES_PENDING": state => state.set("fetchingModules", true),
state.set("stats", fromJS(action.payload.response.data)), "ADMIN_GET_MODULES_FULFILLED": (state, action) => state.merge(fromJS({
ADMIN_GET_MODULES_PENDING: state => state.set("fetchingModules", true), "modules": action.payload.response.data,
ADMIN_GET_MODULES_FULFILLED: (state, action) => "fetchingModules": false,
state.merge( })),
fromJS({ }
modules: action.payload.response.data,
fetchingModules: false
})
)
};
export default (state = defaultState, action) => export default (state = defaultState, action) =>
handlers[action.type] ? handlers[action.type](state, action) : state; handlers[action.type] ? handlers[action.type](state, action) : state;

View File

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

View File

@ -1,14 +1,14 @@
import { combineReducers } from "redux"; import { combineReducers } from "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"
import adminStore from "./admins"; import adminStore from "./admins"
import polochon from "./polochon"; import polochon from "./polochon"
import notifications from "./notifications"; import notifications from "./notifications"
export default combineReducers({ export default combineReducers({
movieStore, movieStore,
@ -19,5 +19,5 @@ export default combineReducers({
torrentStore, torrentStore,
adminStore, adminStore,
polochon, polochon,
notifications notifications,
}); });

View File

@ -1,4 +1,4 @@
import { OrderedMap, Map, fromJS } from "immutable"; import { OrderedMap, Map, fromJS } from "immutable"
const defaultState = Map({ const defaultState = Map({
loading: false, loading: false,
@ -6,95 +6,61 @@ const defaultState = Map({
filter: "", filter: "",
selectedImdbId: "", selectedImdbId: "",
lastFetchUrl: "", lastFetchUrl: "",
exploreOptions: Map() exploreOptions: Map(),
}); });
const handlers = { const handlers = {
MOVIE_LIST_FETCH_PENDING: state => state.set("loading", true), "MOVIE_LIST_FETCH_PENDING": state => state.set("loading", true),
MOVIE_LIST_FETCH_ERROR: state => state.set("loading", false), "MOVIE_LIST_FETCH_ERROR": state => state.set("loading", false),
MOVIE_LIST_FETCH_FULFILLED: (state, action) => { "MOVIE_LIST_FETCH_FULFILLED": (state, action) => {
let allMoviesInPolochon = true; let allMoviesInPolochon = true
let movies = Map(); let movies = Map()
action.payload.response.data.map(function(movie) { action.payload.response.data.map(function (movie) {
movie.fetchingDetails = false; movie.fetchingDetails = false;
movie.fetchingSubtitles = false; movie.fetchingSubtitles = false;
if (movie.polochon_url === "") { if (movie.polochon_url === "") {
allMoviesInPolochon = false; allMoviesInPolochon = false
} }
movies = movies.set(movie.imdb_id, fromJS(movie)); movies = movies.set(movie.imdb_id, fromJS(movie));
}); })
// Select the first movie if the list is not empty // Select the first movie if the list is not empty
let selectedImdbId = ""; let selectedImdbId = "";
if (movies.size > 0) { if (movies.size > 0) {
// Sort by year // Sort by year
movies = movies.sort((a, b) => { movies = movies.sort((a,b) => {
if (!allMoviesInPolochon) { if (!allMoviesInPolochon) {
return b.get("year") - a.get("year"); return b.get("year") - a.get("year")
} }
const dateA = new Date(a.get("date_added")); const dateA = new Date(a.get("date_added"))
const dateB = new Date(b.get("date_added")); const dateB = new Date(b.get("date_added"))
return dateA > dateB ? -1 : dateA < dateB ? 1 : 0; return dateA > dateB ? -1 : dateA < dateB ? 1 : 0;
}); });
selectedImdbId = movies.first().get("imdb_id"); selectedImdbId = movies.first().get("imdb_id");
} }
return state.delete("movies").merge( return state.delete("movies").merge(Map({
Map({ movies: movies,
movies: movies, filter: "",
filter: "", loading: false,
loading: false, selectedImdbId: selectedImdbId,
selectedImdbId: selectedImdbId }))
})
);
}, },
MOVIE_GET_DETAILS_PENDING: (state, action) => "MOVIE_GET_DETAILS_PENDING" : (state, action) => state.setIn(["movies", action.payload.main.imdbId, "fetchingDetails"], true),
state.setIn( "MOVIE_GET_DETAILS_FULFILLED" : (state, action) => state.setIn(["movies", action.payload.response.data.imdb_id], fromJS(action.payload.response.data))
["movies", action.payload.main.imdbId, "fetchingDetails"], .setIn(["movies", action.payload.response.data.imdb_id, "fetchingDetails"], false)
true .setIn(["movies", action.payload.response.data.imdb_id, "fetchingSubtitles"], false),
), "MOVIE_UPDATE_STORE_WISHLIST" : (state, action) => state.setIn(["movies", action.payload.imdbId, "wishlisted"], action.payload.wishlisted),
MOVIE_GET_DETAILS_FULFILLED: (state, action) => "MOVIE_GET_EXPLORE_OPTIONS_FULFILLED" : (state, action) => state.set("exploreOptions", fromJS(action.payload.response.data)),
state "UPDATE_LAST_MOVIE_FETCH_URL" : (state, action) => state.set("lastFetchUrl", action.payload.url),
.setIn( "MOVIE_SUBTITLES_UPDATE_PENDING" : (state, action) => state.setIn(["movies", action.payload.main.imdbId, "fetchingSubtitles"], true),
["movies", action.payload.response.data.imdb_id], "MOVIE_SUBTITLES_UPDATE_FULFILLED" : (state, action) => state.setIn(["movies", action.payload.main.imdbId, "fetchingSubtitles"], false)
fromJS(action.payload.response.data) .setIn(["movies", action.payload.main.imdbId, "subtitles"], fromJS(action.payload.response.data)),
) "SELECT_MOVIE" : (state, action) => state.set("selectedImdbId", action.payload.imdbId),
.setIn( "MOVIE_UPDATE_FILTER" : (state, action) => state.set("filter", action.payload.filter),
["movies", action.payload.response.data.imdb_id, "fetchingDetails"], }
false
)
.setIn(
["movies", action.payload.response.data.imdb_id, "fetchingSubtitles"],
false
),
MOVIE_UPDATE_STORE_WISHLIST: (state, action) =>
state.setIn(
["movies", action.payload.imdbId, "wishlisted"],
action.payload.wishlisted
),
MOVIE_GET_EXPLORE_OPTIONS_FULFILLED: (state, action) =>
state.set("exploreOptions", fromJS(action.payload.response.data)),
UPDATE_LAST_MOVIE_FETCH_URL: (state, action) =>
state.set("lastFetchUrl", action.payload.url),
MOVIE_SUBTITLES_UPDATE_PENDING: (state, action) =>
state.setIn(
["movies", action.payload.main.imdbId, "fetchingSubtitles"],
true
),
MOVIE_SUBTITLES_UPDATE_FULFILLED: (state, action) =>
state
.setIn(["movies", action.payload.main.imdbId, "fetchingSubtitles"], false)
.setIn(
["movies", action.payload.main.imdbId, "subtitles"],
fromJS(action.payload.response.data)
),
SELECT_MOVIE: (state, action) =>
state.set("selectedImdbId", action.payload.imdbId),
MOVIE_UPDATE_FILTER: (state, action) =>
state.set("filter", action.payload.filter)
};
export default (state = defaultState, action) => export default (state = defaultState, action) =>
handlers[action.type] ? handlers[action.type](state, action) : state; handlers[action.type] ? handlers[action.type](state, action) : state;

View File

@ -1,20 +1,15 @@
import { List, fromJS } from "immutable"; import { List, fromJS } from "immutable"
const defaultState = List(); const defaultState = List();
const handlers = { const handlers = {
ADD_NOTIFICATION: (state, action) => "ADD_NOTIFICATION": (state, action) => state.push(fromJS({
state.push( id: Math.random().toString(36).substring(7),
fromJS({ ...action.payload
id: Math.random() })),
.toString(36) "REMOVE_NOTIFICATION": (state, action) =>
.substring(7), state.filter((e) => (e.get("id") !== action.payload.id)),
...action.payload }
})
),
REMOVE_NOTIFICATION: (state, action) =>
state.filter(e => e.get("id") !== action.payload.id)
};
export default (state = defaultState, action) => export default (state = defaultState, action) =>
handlers[action.type] ? handlers[action.type](state, action) : state; handlers[action.type] ? handlers[action.type](state, action) : state;

View File

@ -1,29 +1,28 @@
import { List, Map, fromJS } from "immutable"; import { List, Map, fromJS } from "immutable"
const defaultState = Map({ const defaultState = Map({
loadingPublic: false, loadingPublic: false,
loadingManaged: false, loadingManaged: false,
public: List(), public: List(),
managed: List() managed: List(),
}); });
const handlers = { const handlers = {
PUBLIC_POLOCHON_LIST_FETCH_PENDING: state => state.set("loadingPublic", true), "PUBLIC_POLOCHON_LIST_FETCH_PENDING": state => state.set("loadingPublic", true),
PUBLIC_POLOCHON_LIST_FETCH_FULFILLED: (state, action) => { "PUBLIC_POLOCHON_LIST_FETCH_FULFILLED": (state, action) => {
return state.merge({ return state.merge({
loadingPublic: false, loadingPublic: false,
public: List(fromJS(action.payload.response.data)) public: List(fromJS(action.payload.response.data)),
}); });
}, },
MANAGED_POLOCHON_LIST_FETCH_PENDING: state => "MANAGED_POLOCHON_LIST_FETCH_PENDING": state => state.set("loadingManaged", true),
state.set("loadingManaged", true), "MANAGED_POLOCHON_LIST_FETCH_FULFILLED": (state, action) => {
MANAGED_POLOCHON_LIST_FETCH_FULFILLED: (state, action) => {
return state.merge({ return state.merge({
loadingManaged: false, loadingManaged: false,
managed: List(fromJS(action.payload.response.data)) managed: List(fromJS(action.payload.response.data)),
}); });
} },
}; }
export default (state = defaultState, action) => export default (state = defaultState, action) =>
handlers[action.type] ? handlers[action.type](state, action) : state; handlers[action.type] ? handlers[action.type](state, action) : state;

View File

@ -1,82 +1,35 @@
import { OrderedMap, Map, fromJS } from "immutable"; import { OrderedMap, Map, fromJS } from "immutable"
const defaultState = Map({ const defaultState = Map({
loading: false, loading: false,
show: Map({ show: Map({
seasons: OrderedMap() seasons: OrderedMap(),
}) }),
}); });
const handlers = { const handlers = {
SHOW_FETCH_DETAILS_PENDING: state => state.set("loading", true), "SHOW_FETCH_DETAILS_PENDING": state => state.set("loading", true),
SHOW_FETCH_DETAILS_FULFILLED: (state, action) => "SHOW_FETCH_DETAILS_FULFILLED": (state, action) => sortEpisodes(state, action.payload.response.data),
sortEpisodes(state, action.payload.response.data), "SHOW_UPDATE_STORE_WISHLIST": (state, action) => {
SHOW_UPDATE_STORE_WISHLIST: (state, action) => { return state.mergeDeep(fromJS({
return state.mergeDeep( "show": {
fromJS({ "tracked_season": action.payload.season,
show: { "tracked_episode": action.payload.episode,
tracked_season: action.payload.season, // eslint-disable-line camelcase }
tracked_episode: action.payload.episode // eslint-disable-line camelcase }))},
} "EPISODE_GET_DETAILS_PENDING": (state, action) => state.setIn(["show", "seasons", action.payload.main.season, action.payload.main.episode, "fetching"], true),
}) "EPISODE_GET_DETAILS_FULFILLED": (state, action) => {
);
},
EPISODE_GET_DETAILS_PENDING: (state, action) =>
state.setIn(
[
"show",
"seasons",
action.payload.main.season,
action.payload.main.episode,
"fetching"
],
true
),
EPISODE_GET_DETAILS_FULFILLED: (state, action) => {
let data = action.payload.response.data; let data = action.payload.response.data;
if (!data) { if (!data) { return state }
return state;
}
data.fetching = false; data.fetching = false;
return state.setIn( return state.setIn(["show", "seasons", data.season, data.episode], fromJS(data));
["show", "seasons", data.season, data.episode],
fromJS(data)
);
}, },
EPISODE_SUBTITLES_UPDATE_PENDING: (state, action) => "EPISODE_SUBTITLES_UPDATE_PENDING" : (state, action) =>
state.setIn( state.setIn(["show", "seasons", action.payload.main.season, action.payload.main.episode, "fetchingSubtitles"], true),
[ "EPISODE_SUBTITLES_UPDATE_FULFILLED": (state, action) =>
"show", state.setIn(["show", "seasons", action.payload.main.season, action.payload.main.episode, "subtitles"], fromJS(action.payload.response.data))
"seasons", .setIn(["show", "seasons", action.payload.main.season, action.payload.main.episode, "fetchingSubtitles"], false),
action.payload.main.season, }
action.payload.main.episode,
"fetchingSubtitles"
],
true
),
EPISODE_SUBTITLES_UPDATE_FULFILLED: (state, action) =>
state
.setIn(
[
"show",
"seasons",
action.payload.main.season,
action.payload.main.episode,
"subtitles"
],
fromJS(action.payload.response.data)
)
.setIn(
[
"show",
"seasons",
action.payload.main.season,
action.payload.main.episode,
"fetchingSubtitles"
],
false
)
};
const sortEpisodes = (state, show) => { const sortEpisodes = (state, show) => {
let episodes = show.episodes; let episodes = show.episodes;
@ -99,19 +52,19 @@ const sortEpisodes = (state, show) => {
// 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) => export default (state = defaultState, action) =>
handlers[action.type] ? handlers[action.type](state, action) : state; handlers[action.type] ? handlers[action.type](state, action) : state;

View File

@ -1,4 +1,4 @@
import { OrderedMap, Map, fromJS } from "immutable"; import { OrderedMap, Map, fromJS } from "immutable"
const defaultState = Map({ const defaultState = Map({
loading: false, loading: false,
@ -6,14 +6,14 @@ const defaultState = Map({
filter: "", filter: "",
selectedImdbId: "", selectedImdbId: "",
lastFetchUrl: "", lastFetchUrl: "",
exploreOptions: Map() exploreOptions: Map(),
}); });
const handlers = { const handlers = {
SHOW_LIST_FETCH_PENDING: state => state.set("loading", true), "SHOW_LIST_FETCH_PENDING": state => state.set("loading", true),
SHOW_LIST_FETCH_FULFILLED: (state, action) => { "SHOW_LIST_FETCH_FULFILLED": (state, action) => {
let shows = Map(); let shows = Map();
action.payload.response.data.map(function(show) { action.payload.response.data.map(function (show) {
show.fetchingDetails = false; show.fetchingDetails = false;
show.fetchingSubtitles = false; show.fetchingSubtitles = false;
shows = shows.set(show.imdb_id, fromJS(show)); shows = shows.set(show.imdb_id, fromJS(show));
@ -22,30 +22,26 @@ const handlers = {
let selectedImdbId = ""; let selectedImdbId = "";
if (shows.size > 0) { if (shows.size > 0) {
// Sort by year // Sort by year
shows = shows.sort((a, b) => b.get("year") - a.get("year")); shows = shows.sort((a,b) => b.get("year") - a.get("year"));
selectedImdbId = shows.first().get("imdb_id"); selectedImdbId = shows.first().get("imdb_id");
} }
return state.delete("shows").merge( return state.delete("shows").merge(Map({
Map({ shows: shows,
shows: shows, filter: "",
filter: "", loading: false,
loading: false, selectedImdbId: selectedImdbId,
selectedImdbId: selectedImdbId }));
})
);
}, },
SHOW_GET_DETAILS_PENDING: (state, action) => "SHOW_GET_DETAILS_PENDING": (state, action) => state.setIn(["shows", action.payload.main.imdbId, "fetchingDetails"], true),
state.setIn(["shows", action.payload.main.imdbId, "fetchingDetails"], true), "SHOW_GET_DETAILS_FULFILLED": (state, action) => {
SHOW_GET_DETAILS_FULFILLED: (state, action) => {
let show = action.payload.response.data; let show = action.payload.response.data;
show.fetchingDetails = false; show.fetchingDetails = false;
show.fetchingSubtitles = false; show.fetchingSubtitles = false;
return state.setIn(["shows", show.imdb_id], fromJS(show)); return state.setIn(["shows", show.imdb_id], fromJS(show));
}, },
SHOW_GET_EXPLORE_OPTIONS_FULFILLED: (state, action) => "SHOW_GET_EXPLORE_OPTIONS_FULFILLED": (state, action) => state.set("exploreOptions", fromJS(action.payload.response.data)),
state.set("exploreOptions", fromJS(action.payload.response.data)), "SHOW_UPDATE_STORE_WISHLIST": (state, action) => {
SHOW_UPDATE_STORE_WISHLIST: (state, action) => {
let season = action.payload.season; let season = action.payload.season;
let episode = action.payload.episode; let episode = action.payload.episode;
if (action.payload.wishlisted) { if (action.payload.wishlisted) {
@ -57,21 +53,15 @@ const handlers = {
} }
} }
return state.mergeIn( return state.mergeIn(["shows", action.payload.imdbId], Map({
["shows", action.payload.imdbId], "tracked_season": season,
Map({ "tracked_episode": episode,
tracked_season: season, }));
tracked_episode: episode
})
);
}, },
UPDATE_LAST_SHOWS_FETCH_URL: (state, action) => "UPDATE_LAST_SHOWS_FETCH_URL": (state, action) => state.set("lastFetchUrl", action.payload.url),
state.set("lastFetchUrl", action.payload.url), "SELECT_SHOW": (state, action) => state.set("selectedImdbId", action.payload.imdbId),
SELECT_SHOW: (state, action) => "SHOWS_UPDATE_FILTER": (state, action) => state.set("filter", action.payload.filter),
state.set("selectedImdbId", action.payload.imdbId), }
SHOWS_UPDATE_FILTER: (state, action) =>
state.set("filter", action.payload.filter)
};
export default (state = defaultState, action) => export default (state = defaultState, action) =>
handlers[action.type] ? handlers[action.type](state, action) : state; handlers[action.type] ? handlers[action.type](state, action) : state;

View File

@ -1,30 +1,24 @@
import { Map, List, fromJS } from "immutable"; import { Map, List, fromJS } from "immutable"
const defaultState = Map({ const defaultState = Map({
fetching: false, "fetching": false,
searching: false, "searching": false,
torrents: List(), "torrents": List(),
searchResults: List() "searchResults": List(),
}); });
const handlers = { const handlers = {
TORRENTS_FETCH_PENDING: state => state.set("fetching", true), "TORRENTS_FETCH_PENDING": state => state.set("fetching", true),
TORRENTS_FETCH_FULFILLED: (state, action) => "TORRENTS_FETCH_FULFILLED": (state, action) => state.merge(fromJS({
state.merge( fetching: false,
fromJS({ torrents: action.payload.response.data,
fetching: false, })),
torrents: action.payload.response.data "TORRENTS_SEARCH_PENDING": state => state.set("searching", true),
}) "TORRENTS_SEARCH_FULFILLED": (state, action) => state.merge(fromJS({
), searching: false,
TORRENTS_SEARCH_PENDING: state => state.set("searching", true), searchResults: action.payload.response.data,
TORRENTS_SEARCH_FULFILLED: (state, action) => })),
state.merge( }
fromJS({
searching: false,
searchResults: action.payload.response.data
})
)
};
export default (state = defaultState, action) => export default (state = defaultState, action) =>
handlers[action.type] ? handlers[action.type](state, action) : state; handlers[action.type] ? handlers[action.type](state, action) : state;

View File

@ -1,7 +1,7 @@
import { Map, List, fromJS } from "immutable"; import { Map, List, fromJS } from "immutable"
import jwtDecode from "jwt-decode"; import jwtDecode from "jwt-decode"
import Cookies from "universal-cookie"; import Cookies from "universal-cookie"
const defaultState = Map({ const defaultState = Map({
error: "", error: "",
@ -18,66 +18,52 @@ const defaultState = Map({
polochonActivated: false, polochonActivated: false,
tokens: List(), tokens: List(),
modules: Map(), modules: Map(),
modulesLoading: false modulesLoading: false,
}); });
let initialState = defaultState; let initialState = defaultState
const token = localStorage.getItem("token"); const token = localStorage.getItem("token");
if (token && token !== "") { if (token && token !== "") {
initialState = updateFromToken(initialState, token); initialState = updateFromToken(initialState, token)
} }
const handlers = { const handlers = {
USER_LOGIN_PENDING: state => state.set("loading", true), "USER_LOGIN_PENDING": state => state.set("loading", true),
USER_LOGIN_ERROR: (state, action) => { "USER_LOGIN_ERROR": (state, action) => {
state.set("loading", false); state.set("loading", false);
return logoutUser(action.payload.response.message); return logoutUser(action.payload.response.message);
}, },
USER_LOGIN_FULFILLED: (state, action) => { "USER_LOGIN_FULFILLED": (state, action) => {
return updateFromToken(state, action.payload.response.data.token); return updateFromToken(state, action.payload.response.data.token)
}, },
USER_SIGNUP_PENDING: state => state.set("loading", true), "USER_SIGNUP_PENDING": state => state.set("loading", true),
USER_SIGNUP_ERROR: (state, action) => "USER_SIGNUP_ERROR": (state, action) => state.merge(Map({
state.merge( error: action.payload.response.message,
Map({ loading: false,
error: action.payload.response.message, })),
loading: false "USER_SIGNUP_FULFILLED": state => state.merge(Map({error: "", loading: false})),
}) "USER_SET_TOKEN": (state, action) => updateFromToken(state, action.payload.token),
), "USER_LOGOUT": () => logoutUser(),
USER_SIGNUP_FULFILLED: state => "GET_USER_PENDING": state => state.set("loading", true),
state.merge(Map({ error: "", loading: false })), "GET_USER_FULFILLED": (state, action) => state.merge(Map({
USER_SET_TOKEN: (state, action) => polochonToken: action.payload.response.data.token,
updateFromToken(state, action.payload.token), polochonUrl: action.payload.response.data.url,
USER_LOGOUT: () => logoutUser(), polochonName: action.payload.response.data.name,
GET_USER_PENDING: state => state.set("loading", true), polochonId: action.payload.response.data.id,
GET_USER_FULFILLED: (state, action) => polochonActivated: action.payload.response.data.activated,
state.merge( loading: false,
Map({ })),
polochonToken: action.payload.response.data.token, "GET_USER_TOKENS_PENDING": state => state.set("loading", true),
polochonUrl: action.payload.response.data.url, "GET_USER_TOKENS_FULFILLED": (state, action) => state.merge(Map({
polochonName: action.payload.response.data.name, "tokens": fromJS(action.payload.response.data),
polochonId: action.payload.response.data.id, "loading": false,
polochonActivated: action.payload.response.data.activated, })),
loading: false "GET_USER_MODULES_PENDING": state => state.set("modulesLoading", true),
}) "GET_USER_MODULES_FULFILLED": (state, action) => state.merge(Map({
), "modules": fromJS(action.payload.response.data),
GET_USER_TOKENS_PENDING: state => state.set("loading", true), "modulesLoading": false,
GET_USER_TOKENS_FULFILLED: (state, action) => })),
state.merge( }
Map({
tokens: fromJS(action.payload.response.data),
loading: false
})
),
GET_USER_MODULES_PENDING: state => state.set("modulesLoading", true),
GET_USER_MODULES_FULFILLED: (state, action) =>
state.merge(
Map({
modules: fromJS(action.payload.response.data),
modulesLoading: false
})
)
};
function logoutUser(error) { function logoutUser(error) {
localStorage.removeItem("token"); localStorage.removeItem("token");
@ -86,7 +72,7 @@ function logoutUser(error) {
if (error !== "") { if (error !== "") {
return defaultState.set("error", error); return defaultState.set("error", error);
} else { } else {
return defaultState; return defaultState
} }
} }
@ -97,16 +83,14 @@ function updateFromToken(state, token) {
const cookies = new Cookies(); const cookies = new Cookies();
cookies.set("token", token); cookies.set("token", token);
return state.merge( return state.merge(Map({
Map({ error: "",
error: "", isLogged: true,
isLogged: true, isTokenSet: true,
isTokenSet: true, isAdmin: decodedToken.isAdmin,
isAdmin: decodedToken.isAdmin, isActivated: decodedToken.isActivated,
isActivated: decodedToken.isActivated, username: decodedToken.username,
username: decodedToken.username }))
})
);
} }
export default (state = initialState, action) => export default (state = initialState, action) =>

View File

@ -1,4 +1,4 @@
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
@ -6,22 +6,17 @@ 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({
headers headers
}); })
} }
// This function takes en event prefix to dispatch evens during the life of the // This function takes en event prefix to dispatch evens during the life of the
// request, it also take a promise (axios request) // request, it also take a promise (axios request)
export function request( export function request(eventPrefix, promise, callbackEvents = null, mainPayload = null) {
eventPrefix,
promise,
callbackEvents = null,
mainPayload = null
) {
// Events // Events
const pending = `${eventPrefix}_PENDING`; const pending = `${eventPrefix}_PENDING`;
const fulfilled = `${eventPrefix}_FULFILLED`; const fulfilled = `${eventPrefix}_FULFILLED`;
@ -31,59 +26,60 @@ export function request(
dispatch({ dispatch({
type: pending, type: pending,
payload: { payload: {
main: mainPayload main: mainPayload,
} }
}); })
return promise return promise
.then(response => { .then(response => {
if (response.data.status === "error") { if (response.data.status === "error")
dispatch({ {
type: "ADD_ALERT_ERROR",
payload: {
message: response.data.message,
main: mainPayload
}
});
dispatch({
type: errored,
payload: {
response: response.data,
main: mainPayload
}
});
return;
}
dispatch({
type: fulfilled,
payload: {
response: response.data,
main: mainPayload
}
});
if (callbackEvents) {
for (let event of callbackEvents) {
if (typeof event === "function") {
event = event();
}
dispatch(event);
}
}
})
.catch(error => {
// Unauthorized
if (error.response && error.response.status == 401) {
dispatch({
type: "USER_LOGOUT"
});
}
dispatch({ dispatch({
type: "ADD_ALERT_ERROR", type: "ADD_ALERT_ERROR",
payload: { payload: {
message: error.response.data, message: response.data.message,
main: mainPayload main: mainPayload,
} }
}); });
}); dispatch({
}; type: errored,
payload: {
response: response.data,
main: mainPayload,
},
})
return;
}
dispatch({
type: fulfilled,
payload: {
response: response.data,
main: mainPayload,
},
})
if (callbackEvents) {
for (let event of callbackEvents) {
if (typeof event === "function") {
event = event();
}
dispatch(event);
}
}
})
.catch(error => {
// Unauthorized
if (error.response && error.response.status == 401) {
dispatch({
type: "USER_LOGOUT",
})
}
dispatch({
type: "ADD_ALERT_ERROR",
payload: {
message: error.response.data,
main: mainPayload,
}
})
})
}
} }

View File

@ -17,7 +17,12 @@ if (process.env.NODE_ENV === "development") {
// Export the store // Export the store
const store = createStore( const store = createStore(
rootReducer, rootReducer,
compose(applyMiddleware(...middlewares)) compose(
applyMiddleware(
...middlewares,
),
),
); );
export default store; export default store;

View File

@ -1,70 +1,65 @@
export const prettyDurationFromMinutes = runtime => { export const prettyDurationFromMinutes = (runtime) => {
const hours = Math.floor(runtime / 60); const hours = Math.floor(runtime / 60);
const minutes = runtime % 60; const minutes = (runtime % 60);
let duration = ""; let duration = "";
if (hours > 0) { if (hours > 0) { duration += hours + "h" }
duration += hours + "h"; if (minutes > 0) { duration += ("0" + minutes).slice(-2) }
} if (hours === 0) { duration += " min" }
if (minutes > 0) {
duration += ("0" + minutes).slice(-2);
}
if (hours === 0) {
duration += " min";
}
return duration; return duration;
}; }
const pad = d => (d < 10 ? "0" + d.toString() : d.toString()); const pad = (d) => (d < 10) ? "0" + d.toString() : d.toString();
export const prettyEpisodeName = (showName, season, episode) => export const prettyEpisodeName = (showName, season, episode) =>
`${showName} S${pad(season)}E${pad(episode)}`; `${showName} S${pad(season)}E${pad(episode)}`;
export const inLibrary = element => element.get("polochon_url", "") !== ""; export const inLibrary = (element) =>
element.get("polochon_url", "") !== "";
export const isWishlisted = element => { export const isWishlisted = (element) => {
const wishlisted = element.get("wishlisted", undefined); const wishlisted = element.get("wishlisted", undefined)
if (wishlisted !== undefined) { if (wishlisted !== undefined) {
return wishlisted; return wishlisted;
} }
const trackedSeason = element.get("tracked_season", null); const trackedSeason = element.get("tracked_season", null);
const trackedEpisode = element.get("tracked_episode", null); const trackedEpisode = element.get("tracked_episode", null);
if (trackedSeason !== null && trackedEpisode !== null) { if ((trackedSeason !== null) && (trackedEpisode !== null)) {
return true; return true;
} }
return false; return false
}; }
export const isEpisodeWishlisted = (element, trackedSeason, trackedEpisode) => { export const isEpisodeWishlisted = (element, trackedSeason, trackedEpisode) => {
if (trackedSeason === null && trackedEpisode === null) { if ((trackedSeason === null) && (trackedEpisode === null)) {
return false; return false;
} }
if (trackedSeason === 0 && trackedEpisode === 0) { if ((trackedSeason === 0) && (trackedEpisode === 0)) {
return true; return true
} }
const season = element.get("season", 0); const season = element.get("season", 0);
const episode = element.get("episode", 0); const episode = element.get("episode", 0);
if (season < trackedSeason) { if (season < trackedSeason) {
return false; return false
} else if (season > trackedSeason) { } else if (season > trackedSeason) {
return true; return true
} else { } else {
return episode >= trackedEpisode; return (episode >= trackedEpisode)
} }
}; }
export const prettySize = fileSizeInBytes => { export const 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++;
} while (fileSizeInBytes > 1024); } while (fileSizeInBytes > 1024);
return Math.max(fileSizeInBytes, 0.1).toFixed(1) + byteUnits[i]; return Math.max(fileSizeInBytes, 0.1).toFixed(1) + byteUnits[i];
}; }

File diff suppressed because it is too large Load Diff

View File

@ -17,20 +17,22 @@
"moment": "^2.20.1", "moment": "^2.20.1",
"popper.js": "^1.15.0", "popper.js": "^1.15.0",
"prop-types": "^15.6.0", "prop-types": "^15.6.0",
"react": "^16.13.0", "react": "^16.8.6",
"react-bootstrap": "^1.0.0-beta.9", "react-bootstrap": "^1.0.0-beta.9",
"react-bootstrap-sweetalert": "^5.1.9", "react-bootstrap-sweetalert": "^4.2.3",
"react-bootstrap-toggle": "^2.2.6", "react-bootstrap-toggle": "^2.2.6",
"react-dom": "^16.13.0", "react-dom": "^16.2.0",
"react-infinite-scroll-component": "^5.0.4", "react-infinite-scroll-component": "^4.5.2",
"react-loading": "2.0.3", "react-loading": "2.0.3",
"react-redux": "^7.2.0", "react-redux": "6.0.1",
"react-router": "5.0.1",
"react-router-bootstrap": "^0.25.0", "react-router-bootstrap": "^0.25.0",
"react-router-dom": "^5.0.1", "react-router-dom": "^5.0.1",
"redux": "^4.0.1", "redux": "^4.0.1",
"redux-logger": "^3.0.6", "redux-logger": "^3.0.6",
"redux-thunk": "^2.3.0", "redux-thunk": "^2.3.0",
"ua-parser-js": "^0.7.17" "ua-parser-js": "^0.7.17",
"universal-cookie": "^2.1.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.5.0", "@babel/core": "^7.5.0",
@ -38,27 +40,26 @@
"@babel/preset-react": "^7.0.0", "@babel/preset-react": "^7.0.0",
"autoprefixer": "^9.5.1", "autoprefixer": "^9.5.1",
"axios": "^0.19.2", "axios": "^0.19.2",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.0.1",
"babel-loader": "^8.0.6", "babel-loader": "^8.0.6",
"clean-webpack-plugin": "^3.0.0", "clean-webpack-plugin": "^3.0.0",
"css-loader": "^3.4.2", "css-loader": "^3.4.2",
"eslint": "^6.8.0", "del": "^3.0.0",
"eslint-config-prettier": "^6.10.0", "eslint": "^5.16.0",
"eslint-plugin-import": "^2.20.1",
"eslint-plugin-prettier": "^3.1.2",
"eslint-plugin-react": "^7.6.1", "eslint-plugin-react": "^7.6.1",
"eslint-plugin-react-hooks": "^2.5.0", "eslint-plugin-react-hooks": "^1.6.0",
"file-loader": "^5.1.0", "file-loader": "^3.0.1",
"html-webpack-plugin": "^3.2.0", "html-webpack-plugin": "^3.2.0",
"less": "^2.3.1",
"less-loader": "^4.0.5",
"node-sass": "^4.12.0", "node-sass": "^4.12.0",
"postcss-loader": "^3.0.0", "postcss-loader": "^3.0.0",
"prettier": "^1.19.1", "sass-loader": "^7.1.0",
"sass-loader": "^8.0.2", "style-loader": "^0.23.1",
"style-loader": "^1.1.3", "url-loader": "^1.1.2",
"universal-cookie": "^4.0.3",
"url-loader": "^3.0.0",
"webpack": "^4.31.0", "webpack": "^4.31.0",
"webpack-cli": "^3.3.11", "webpack-cli": "^3.3.2",
"webpack-pwa-manifest": "^4.0.0" "webpack-pwa-manifest": "^4.0.0",
"webpack-stream": "^5.2.1"
} }
} }

View File

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

View File

@ -1,6 +1,6 @@
var webpack = require("webpack"); var webpack = require("webpack");
var path = require("path"); var path = require("path");
var WebpackPwaManifest = require("webpack-pwa-manifest"); var WebpackPwaManifest = require("webpack-pwa-manifest")
var HtmlWebpackPlugin = require("html-webpack-plugin"); var HtmlWebpackPlugin = require("html-webpack-plugin");
var { CleanWebpackPlugin } = require("clean-webpack-plugin"); var { CleanWebpackPlugin } = require("clean-webpack-plugin");
@ -19,7 +19,7 @@ const config = {
entry: path.join(SRC_DIR, "js/app.js"), entry: path.join(SRC_DIR, "js/app.js"),
output: { output: {
path: BUILD_DIR, path: BUILD_DIR,
filename: "[contenthash]-app.js" filename: "[contenthash]-app.js",
}, },
optimization: { optimization: {
runtimeChunk: "single", runtimeChunk: "single",
@ -28,10 +28,10 @@ const config = {
vendor: { vendor: {
test: /[\\/]node_modules[\\/]/, test: /[\\/]node_modules[\\/]/,
name: "vendors", name: "vendors",
chunks: "all" chunks: "all",
} },
} },
} },
}, },
module: { module: {
rules: [ rules: [
@ -47,68 +47,73 @@ const config = {
}, },
{ {
test: /\.scss$/, test: /\.scss$/,
use: ["style-loader", "css-loader", "sass-loader", "postcss-loader"] use: [
"style-loader",
"css-loader",
"sass-loader",
"postcss-loader",
],
}, },
{ {
test: /\.(png|jpg|svg|ico)$/, test: /\.(png|jpg|svg|ico)$/,
use: ["file-loader?name=[hash]-[name].[ext]"] use: ["file-loader?name=[hash]-[name].[ext]"],
}, },
{ {
test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
use: ["url-loader?limit=10000&mimetype=application/font-woff"] use: ["url-loader?limit=10000&mimetype=application/font-woff"],
}, },
{ {
test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
use: ["file-loader"] use: ["file-loader"],
} }
] ]
}, },
plugins: [ plugins: [
new webpack.DefinePlugin({ new webpack.DefinePlugin({
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV) "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
}), }),
new webpack.HashedModuleIdsPlugin(), new webpack.HashedModuleIdsPlugin(),
new CleanWebpackPlugin({ new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: ["**/*", "!img/**/*", "!img"] cleanOnceBeforeBuildPatterns: ["**/*", "!img/**/*", "!img"],
}), }),
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
template: path.join(SRC_DIR, "index.html") template: path.join(SRC_DIR, "index.html"),
}), }),
new WebpackPwaManifest({ new WebpackPwaManifest({
fingerprints: true, fingerprints: true,
inject: true, inject: true,
ios: { ios: {
"apple-mobile-web-app-status-bar-style": "default", "apple-mobile-web-app-status-bar-style": "default",
"apple-mobile-web-app-title": "Canapé" "apple-mobile-web-app-title": "Canapé",
}, },
name: "Canapé", name: "Canapé",
short_name: "Canapé", // eslint-disable-line camelcase short_name: "Canapé",
background_color: "#4e5d6c", // eslint-disable-line camelcase background_color: "#4e5d6c",
theme_color: "#4e5d6c", // eslint-disable-line camelcase theme_color: "#4e5d6c",
display: "standalone", display: "standalone",
orientation: "omit", orientation: "omit",
scope: "/", scope: "/",
start_url: "/", // eslint-disable-line camelcase start_url: "/",
icons: [ icons: [
{ {
src: path.resolve(__dirname, "img/android-chrome-512x512.png"), src: path.resolve(__dirname, "img/android-chrome-512x512.png"),
sizes: [96, 128, 192, 256, 384, 512] sizes: [96, 128, 192, 256, 384, 512],
}, },
{ {
src: path.resolve(__dirname, "img/apple-touch-icon.png"), src: path.resolve(__dirname, "img/apple-touch-icon.png"),
sizes: [80, 120, 152, 167, 180], sizes: [80, 120, 152, 167, 180],
ios: true ios: true
} },
] ],
}) }),
], ],
resolve: { resolve: {
extensions: [".js"] extensions: [".js"],
} },
}; };
if (process.env.NODE_ENV !== "production") { if (process.env.NODE_ENV !== "production") {
config.devtool = "#cheap-module-source-map"; config.devtool = "#cheap-module-source-map"
} }
module.exports = config; module.exports = config;