From 4b26080193ecbd704d0774d11daae7104ce941f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Delattre?= Date: Tue, 7 Apr 2020 18:22:26 +0200 Subject: [PATCH] Update redux state management Use immer with native javascript objects instead of immutablejs. --- frontend/.eslintrc | 1 + frontend/js/actions/shows.js | 9 - frontend/js/app.js | 24 +- frontend/js/auth.js | 10 +- frontend/js/components/admins/modules.js | 6 +- frontend/js/components/admins/stat.js | 14 +- frontend/js/components/admins/stats.js | 20 +- frontend/js/components/admins/torrentsStat.js | 11 +- frontend/js/components/admins/user.js | 52 +--- frontend/js/components/admins/userEdit.js | 39 +-- frontend/js/components/admins/userList.js | 21 +- frontend/js/components/admins/users.js | 172 ------------ frontend/js/components/alerts/alert.js | 22 +- frontend/js/components/buttons/download.js | 34 +-- frontend/js/components/buttons/subtitles.js | 11 +- frontend/js/components/buttons/torrents.js | 47 ++-- frontend/js/components/details/genres.js | 9 +- frontend/js/components/list/details.js | 52 ++-- .../js/components/list/explorerOptions.js | 8 +- frontend/js/components/list/filter.js | 2 + frontend/js/components/list/keyboard.js | 106 +++++++ frontend/js/components/list/posters.js | 262 ++++++------------ frontend/js/components/modules/modules.js | 57 ++-- frontend/js/components/movies/list.js | 48 +--- .../js/components/movies/subtitlesButton.js | 28 +- .../js/components/movies/torrentsButton.js | 24 +- frontend/js/components/navbar.js | 33 +-- .../components/notifications/notification.js | 52 ++-- .../components/notifications/notifications.js | 23 +- frontend/js/components/polochons/list.js | 16 +- frontend/js/components/polochons/polochon.js | 3 +- frontend/js/components/polochons/select.js | 15 +- frontend/js/components/polochons/users.js | 13 +- frontend/js/components/shows/details.js | 9 +- .../js/components/shows/details/episode.js | 114 ++++---- .../components/shows/details/episodeThumb.js | 14 +- .../js/components/shows/details/fanart.js | 22 +- .../js/components/shows/details/header.js | 60 ++-- .../js/components/shows/details/season.js | 47 ++-- .../js/components/shows/details/seasons.js | 46 +-- .../shows/details/subtitlesButton.js | 42 +-- .../shows/details/torrentsButton.js | 43 +-- frontend/js/components/shows/list.js | 38 +-- frontend/js/components/torrents/list.js | 60 ++-- frontend/js/components/torrents/search.js | 119 ++++---- frontend/js/components/users/activation.js | 9 +- frontend/js/components/users/edit.js | 15 +- frontend/js/components/users/login.js | 10 +- frontend/js/components/users/logout.js | 11 +- frontend/js/components/users/profile.js | 10 +- frontend/js/components/users/signup.js | 10 +- frontend/js/components/users/tokens.js | 41 ++- frontend/js/components/websocket.js | 6 +- frontend/js/reducers/admins.js | 55 ++-- frontend/js/reducers/alerts.js | 56 ++-- frontend/js/reducers/index.js | 28 +- frontend/js/reducers/movies.js | 182 ++++++------ frontend/js/reducers/notifications.js | 36 ++- frontend/js/reducers/polochon.js | 52 ++-- frontend/js/reducers/show.js | 210 +++++++------- frontend/js/reducers/shows.js | 138 ++++----- frontend/js/reducers/torrents.js | 52 ++-- frontend/js/reducers/users.js | 166 +++++------ frontend/js/utils.js | 53 ++-- frontend/package-lock.json | 8 +- frontend/package.json | 2 +- 66 files changed, 1330 insertions(+), 1678 deletions(-) delete mode 100644 frontend/js/components/admins/users.js create mode 100644 frontend/js/components/list/keyboard.js diff --git a/frontend/.eslintrc b/frontend/.eslintrc index 8ce7f27..068cc77 100644 --- a/frontend/.eslintrc +++ b/frontend/.eslintrc @@ -20,6 +20,7 @@ } }, "env": { + "es6": true, "browser": true, "mocha": true, "node": true diff --git a/frontend/js/actions/shows.js b/frontend/js/actions/shows.js index 70ddd4b..888e700 100644 --- a/frontend/js/actions/shows.js +++ b/frontend/js/actions/shows.js @@ -106,15 +106,6 @@ export function selectShow(imdbId) { }; } -export function updateFilter(filter) { - return { - type: "SHOWS_UPDATE_FILTER", - payload: { - filter, - }, - }; -} - export function updateLastShowsFetchUrl(url) { return { type: "UPDATE_LAST_SHOWS_FETCH_URL", diff --git a/frontend/js/app.js b/frontend/js/app.js index 6f6b4bd..44a2127 100644 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -26,25 +26,25 @@ import store, { history } from "./store"; // Components import { AdminPanel } from "./components/admins/panel"; 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 NavBar from "./components/navbar"; -import WsHandler from "./components/websocket"; +import { AppNavBar } from "./components/navbar"; +import { WsHandler } from "./components/websocket"; import { ShowDetails } from "./components/shows/details"; import ShowList from "./components/shows/list"; -import TorrentList from "./components/torrents/list"; -import TorrentSearch from "./components/torrents/search"; -import UserActivation from "./components/users/activation"; -import UserLoginForm from "./components/users/login"; -import UserLogout from "./components/users/logout"; -import UserProfile from "./components/users/profile"; -import UserSignUp from "./components/users/signup"; -import UserTokens from "./components/users/tokens"; +import { TorrentList } from "./components/torrents/list"; +import { TorrentSearch } from "./components/torrents/search"; +import { UserActivation } from "./components/users/activation"; +import { UserLoginForm } from "./components/users/login"; +import { UserLogout } from "./components/users/logout"; +import { UserProfile } from "./components/users/profile"; +import { UserSignUp } from "./components/users/signup"; +import { UserTokens } from "./components/users/tokens"; const App = () => (
- + diff --git a/frontend/js/auth.js b/frontend/js/auth.js index 846e758..4b20fb6 100644 --- a/frontend/js/auth.js +++ b/frontend/js/auth.js @@ -8,11 +8,9 @@ import { setUserToken } from "./actions/users"; export const ProtectedRoute = ({ component: Component, ...otherProps }) => { const dispatch = useDispatch(); - const isLogged = useSelector((state) => state.userStore.get("isLogged")); - const isActivated = useSelector((state) => - state.userStore.get("isActivated") - ); - const isTokenSet = useSelector((state) => state.userStore.get("isTokenSet")); + const isLogged = useSelector((state) => state.user.isLogged); + const isActivated = useSelector((state) => state.user.isActivated); + const isTokenSet = useSelector((state) => state.user.isTokenSet); const isAuthenticated = () => { if (isTokenSet) { @@ -52,7 +50,7 @@ ProtectedRoute.propTypes = { }; export const AdminRoute = ({ component: Component, ...otherProps }) => { - const isAdmin = useSelector((state) => state.userStore.get("isAdmin")); + const isAdmin = useSelector((state) => state.user.isAdmin); return ( { const dispatch = useDispatch(); - const loading = useSelector((state) => - state.adminStore.get("fetchingModules") - ); - const modules = useSelector((state) => state.adminStore.get("modules")); + const loading = useSelector((state) => state.admin.fetchingModules); + const modules = useSelector((state) => state.admin.modules); useEffect(() => { dispatch(getAdminModules()); diff --git a/frontend/js/components/admins/stat.js b/frontend/js/components/admins/stat.js index 485018b..cf5afe8 100644 --- a/frontend/js/components/admins/stat.js +++ b/frontend/js/components/admins/stat.js @@ -25,8 +25,14 @@ export const Stat = ({ name, count, torrentCount, torrentCountById }) => (
); Stat.propTypes = { - name: PropTypes.string, - count: PropTypes.number, - torrentCount: PropTypes.number, - torrentCountById: PropTypes.number, + name: PropTypes.string.isRequired, + count: PropTypes.number.isRequired, + torrentCount: PropTypes.number.isRequired, + torrentCountById: PropTypes.number.isRequired, +}; +Stat.defaultProps = { + name: "", + count: 0, + torrentCount: 0, + torrentCountById: 0, }; diff --git a/frontend/js/components/admins/stats.js b/frontend/js/components/admins/stats.js index cefaf68..031a5ac 100644 --- a/frontend/js/components/admins/stats.js +++ b/frontend/js/components/admins/stats.js @@ -7,7 +7,7 @@ import { getStats } from "../../actions/admins"; export const Stats = () => { const dispatch = useDispatch(); - const stats = useSelector((state) => state.adminStore.get("stats")); + const stats = useSelector((state) => state.admin.stats); useEffect(() => { dispatch(getStats()); @@ -17,21 +17,21 @@ export const Stats = () => {
); diff --git a/frontend/js/components/admins/torrentsStat.js b/frontend/js/components/admins/torrentsStat.js index f68ef58..aca2a9d 100644 --- a/frontend/js/components/admins/torrentsStat.js +++ b/frontend/js/components/admins/torrentsStat.js @@ -15,7 +15,12 @@ export const TorrentsStat = ({ count, torrentCount, torrentCountById }) => { ); }; TorrentsStat.propTypes = { - count: PropTypes.number, - torrentCount: PropTypes.number, - torrentCountById: PropTypes.number, + count: PropTypes.number.isRequired, + torrentCount: PropTypes.number.isRequired, + torrentCountById: PropTypes.number.isRequired, +}; +TorrentsStat.defaultProps = { + count: 1, + torrentCount: 0, + torrentCountById: 0, }; diff --git a/frontend/js/components/admins/user.js b/frontend/js/components/admins/user.js index b7454db..461f504 100644 --- a/frontend/js/components/admins/user.js +++ b/frontend/js/components/admins/user.js @@ -1,65 +1,43 @@ import React from "react"; import PropTypes from "prop-types"; +import { useSelector } from "react-redux"; import { UserEdit } from "./userEdit"; -export const User = ({ - id, - admin, - activated, - name, - polochonActivated, - polochonUrl, - polochonName, - polochonId, - token, -}) => { +export const User = ({ id }) => { + const user = useSelector((state) => state.admin.users.get(id)); + const polochon = user.polochon; + return ( - {id} - {name} + {user.id} + {user.name} - + - {polochonName !== "" ? polochonName : "-"} - {polochonUrl !== "" && ({polochonUrl})} + {polochon ? polochon.name : "-"} + {polochon && ({polochon.url})} - {token} + {user.token} - + ); }; User.propTypes = { - id: PropTypes.string, - name: PropTypes.string, - polochonId: PropTypes.string, - polochonUrl: PropTypes.string, - polochonName: PropTypes.string, - token: PropTypes.string, - admin: PropTypes.bool, - activated: PropTypes.bool, - polochonActivated: PropTypes.bool, + id: PropTypes.string.isRequired, }; diff --git a/frontend/js/components/admins/userEdit.js b/frontend/js/components/admins/userEdit.js index cd6ed26..e1db1de 100644 --- a/frontend/js/components/admins/userEdit.js +++ b/frontend/js/components/admins/userEdit.js @@ -10,25 +10,20 @@ import { PolochonSelect } from "../polochons/select"; import { FormModal } from "../forms/modal"; import { FormInput } from "../forms/input"; -export const UserEdit = ({ - id, - name, - admin: initAdmin, - activated: initActivated, - polochonToken, - polochonId: initPolochonId, - polochonActivated: initPolochonActivated, -}) => { +export const UserEdit = ({ id }) => { const dispatch = useDispatch(); - const publicPolochons = useSelector((state) => state.polochon.get("public")); + const user = useSelector((state) => state.admin.users.get(id)); + const polochon = user.polochon; const [modal, setModal] = useState(false); - const [admin, setAdmin] = useState(initAdmin); - const [activated, setActivated] = useState(initActivated); - const [token, setToken] = useState(polochonToken); - const [polochonId, setPolochonId] = useState(initPolochonId); + const [admin, setAdmin] = useState(user.admin); + const [activated, setActivated] = useState(user.activated); + const [token, setToken] = useState(user.token); + const [polochonId, setPolochonId] = useState( + polochon ? polochon.id : undefined + ); const [polochonActivated, setPolochonActivated] = useState( - initPolochonActivated + user.polochon_activated ); const [password, setPassword] = useState(""); const [confirmDelete, setConfirmDelete] = useState(false); @@ -106,11 +101,7 @@ export const UserEdit = ({ updateValue={setPassword} /> - + { const dispatch = useDispatch(); - const users = useSelector((state) => state.adminStore.get("users")); + const userIds = useSelector((state) => Array.from(state.admin.users.keys())); useEffect(() => { dispatch(getUsers()); dispatch(getPolochons()); }, [dispatch]); + if (userIds.length === 0) { + return null; + } + return (
@@ -31,19 +35,8 @@ export const UserList = () => { - {users.map((el, index) => ( - + {userIds.map((id) => ( + ))}
diff --git a/frontend/js/components/admins/users.js b/frontend/js/components/admins/users.js deleted file mode 100644 index f09c6a0..0000000 --- a/frontend/js/components/admins/users.js +++ /dev/null @@ -1,172 +0,0 @@ -import React, { useState } from "react"; -import PropTypes from "prop-types"; -import { List } from "immutable"; - -import { Button, Modal } from "react-bootstrap"; -import Toggle from "react-bootstrap-toggle"; - -export const UserList = (props) => ( -
- - - - - - - - - - - - - - {props.users.map((el, index) => ( - - ))} - -
#NameActivatedAdminPolochon URLPolochon tokenActions
-
-); -UserList.propTypes = { - users: PropTypes.PropTypes.instanceOf(List), - updateUser: PropTypes.func, -}; - -const User = function (props) { - const polochonConfig = props.data.get("RawConfig").get("polochon"); - const polochonUrl = polochonConfig ? polochonConfig.get("url") : "-"; - const polochonToken = polochonConfig ? polochonConfig.get("token") : "-"; - return ( - - {props.data.get("id")} - {props.data.get("Name")} - - - - - - - {polochonUrl} - {polochonToken} - - - - - ); -}; -User.propTypes = { - data: PropTypes.object, - updateUser: PropTypes.func, -}; - -const UserAdminStatus = (props) => ( - -); -UserAdminStatus.propTypes = { admin: PropTypes.bool.isRequired }; - -const UserActivationStatus = (props) => ( - -); -UserActivationStatus.propTypes = { activated: PropTypes.bool.isRequired }; - -function UserEdit(props) { - const [modal, setModal] = useState(false); - const [admin, setAdmin] = useState(props.data.get("Admin")); - const [activated, setActivated] = useState(props.data.get("Activated")); - const [url, setUrl] = useState(props.polochonUrl); - const [token, setToken] = useState(props.polochonToken); - - const handleSubmit = function (e) { - if (e) { - e.preventDefault(); - } - props.updateUser({ - userId: props.data.get("id"), - admin: admin, - activated: activated, - polochonUrl: url, - polochonToken: token, - }); - setModal(false); - }; - - return ( - - setModal(true)} - > - setModal(false)}> - - - -   Edit user - {props.data.get("Name")} - - - -
handleSubmit(ev)}> -
- - setActivated(!activated)} - /> -
-
- - setAdmin(!admin)} - /> -
-
- - setUrl(e.target.value)} - /> -
-
- - setToken(e.target.value)} - /> -
-
-
- - - - -
-
- ); -} -UserEdit.propTypes = { - data: PropTypes.object, - updateUser: PropTypes.func, - polochonUrl: PropTypes.string, - polochonToken: PropTypes.string, -}; diff --git a/frontend/js/components/alerts/alert.js b/frontend/js/components/alerts/alert.js index dbc33d9..a15e964 100644 --- a/frontend/js/components/alerts/alert.js +++ b/frontend/js/components/alerts/alert.js @@ -4,24 +4,20 @@ import { useSelector, useDispatch } from "react-redux"; import { dismissAlert } from "../../actions/alerts"; -const Alert = () => { +export const Alert = () => { const dispatch = useDispatch(); - const show = useSelector((state) => state.alerts.get("show")); - const title = useSelector((state) => state.alerts.get("message")); - const type = useSelector((state) => state.alerts.get("type")); + const show = useSelector((state) => state.alerts.show); + const title = useSelector((state) => state.alerts.message); + const type = useSelector((state) => state.alerts.type); + + const dismiss = () => { + dispatch(dismissAlert()); + }; if (!show) { return null; } - return ( - dispatch(dismissAlert())} - /> - ); + return ; }; - -export default Alert; diff --git a/frontend/js/components/buttons/download.js b/frontend/js/components/buttons/download.js index ee5f2d4..f3025c5 100644 --- a/frontend/js/components/buttons/download.js +++ b/frontend/js/components/buttons/download.js @@ -1,6 +1,5 @@ import React, { useState } from "react"; import PropTypes from "prop-types"; -import { List } from "immutable"; import Modal from "react-bootstrap/Modal"; @@ -19,7 +18,7 @@ export const DownloadAndStream = ({ url, name, subtitles }) => { DownloadAndStream.propTypes = { url: PropTypes.string, name: PropTypes.string, - subtitles: PropTypes.instanceOf(List), + subtitles: PropTypes.array, }; const DownloadButton = ({ url }) => ( @@ -68,35 +67,30 @@ const StreamButton = ({ name, url, subtitles }) => { StreamButton.propTypes = { name: PropTypes.string.isRequired, url: PropTypes.string.isRequired, - subtitles: PropTypes.instanceOf(List), + subtitles: PropTypes.array, }; const Player = ({ url, subtitles }) => { - const hasSubtitles = !(subtitles === null || subtitles.size === 0); + const subs = subtitles || []; + return (
); }; Player.propTypes = { - subtitles: PropTypes.instanceOf(List), + subtitles: PropTypes.array, url: PropTypes.string.isRequired, }; -Player.defaultProps = { - subtitles: List(), -}; diff --git a/frontend/js/components/buttons/subtitles.js b/frontend/js/components/buttons/subtitles.js index 7cd80b7..91ab89e 100644 --- a/frontend/js/components/buttons/subtitles.js +++ b/frontend/js/components/buttons/subtitles.js @@ -1,6 +1,5 @@ import React, { useState } from "react"; import PropTypes from "prop-types"; -import { List } from "immutable"; import Dropdown from "react-bootstrap/Dropdown"; @@ -30,7 +29,7 @@ export const SubtitlesButton = ({ } }; - const count = subtitles && subtitles.size !== 0 ? subtitles.size : 0; + const count = subtitles && subtitles.length !== 0 ? subtitles.length : 0; return ( @@ -54,9 +53,9 @@ export const SubtitlesButton = ({ )} {count > 0 && - subtitles.toIndexedSeq().map((subtitle, index) => ( - - {subtitle.get("language").split("_")[1]} + subtitles.map((subtitle, index) => ( + + {subtitle.language.split("_")[1]} ))} @@ -65,7 +64,7 @@ export const SubtitlesButton = ({ ); }; SubtitlesButton.propTypes = { - subtitles: PropTypes.instanceOf(List), + subtitles: PropTypes.array, inLibrary: PropTypes.bool.isRequired, searching: PropTypes.bool.isRequired, search: PropTypes.func.isRequired, diff --git a/frontend/js/components/buttons/torrents.js b/frontend/js/components/buttons/torrents.js index 07da703..5a9b7e1 100644 --- a/frontend/js/components/buttons/torrents.js +++ b/frontend/js/components/buttons/torrents.js @@ -1,6 +1,5 @@ import React, { useState } from "react"; import PropTypes from "prop-types"; -import { List } from "immutable"; import { useDispatch } from "react-redux"; import { prettySize } from "../../utils"; @@ -8,49 +7,60 @@ import { addTorrent } from "../../actions/torrents"; import Dropdown from "react-bootstrap/Dropdown"; -function buildMenuItems(torrents) { - if (!torrents || torrents.size === 0) { +const buildMenuItems = (torrents) => { + if (!torrents || torrents.length === 0) { return []; } - const t = torrents.groupBy((el) => el.get("source")); - // Build the array of entries let entries = []; - let dividerCount = t.size - 1; - for (let [source, torrentList] of t.entrySeq()) { + + let dividerCount = torrents.size - 1; + torrents.forEach((torrentsBySource, source) => { // Push the title entries.push({ type: "header", value: source, }); - // Push the torrents - for (let torrent of torrentList) { + torrentsBySource.forEach((torrent) => { entries.push({ type: "entry", - quality: torrent.get("quality"), - url: torrent.get("url"), - size: torrent.get("size"), + quality: torrent.quality, + url: torrent.url, + size: torrent.size, }); - } + }); // Push the divider if (dividerCount > 0) { dividerCount--; entries.push({ type: "divider" }); } - } + }); return entries; -} +}; + +const countEntries = (torrents) => { + if (!torrents || torrents.length === 0) { + return 0; + } + + let count = 0; + torrents.forEach((source) => { + count = count + source.size; + }); + + return count; +}; export const TorrentsButton = ({ torrents, search, searching, url }) => { const dispatch = useDispatch(); const [show, setShow] = useState(false); const entries = buildMenuItems(torrents); - const count = torrents && torrents.size !== 0 ? torrents.size : 0; + const count = countEntries(torrents); const onSelect = (eventKey) => { // Close the dropdown if the eventkey is not specified @@ -115,11 +125,8 @@ export const TorrentsButton = ({ torrents, search, searching, url }) => { ); }; TorrentsButton.propTypes = { - torrents: PropTypes.instanceOf(List), + torrents: PropTypes.object, searching: PropTypes.bool, search: PropTypes.func.isRequired, url: PropTypes.string, }; -TorrentsButton.defaultProps = { - torrents: List(), -}; diff --git a/frontend/js/components/details/genres.js b/frontend/js/components/details/genres.js index d1022a6..c2b94be 100644 --- a/frontend/js/components/details/genres.js +++ b/frontend/js/components/details/genres.js @@ -1,15 +1,13 @@ import React from "react"; import PropTypes from "prop-types"; -import { List } from "immutable"; -export const Genres = ({ genres }) => { - if (genres.size === 0) { +export const Genres = ({ genres = [] }) => { + if (!genres || genres.length === 0) { return null; } // Uppercase first genres const prettyGenres = genres - .toJS() .map((word) => word[0].toUpperCase() + word.substr(1)) .join(", "); @@ -20,5 +18,4 @@ export const Genres = ({ genres }) => { ); }; -Genres.propTypes = { genres: PropTypes.instanceOf(List) }; -Genres.defaultProps = { genres: List() }; +Genres.propTypes = { genres: PropTypes.array }; diff --git a/frontend/js/components/list/details.js b/frontend/js/components/list/details.js index 7b9b6b3..885b4ca 100644 --- a/frontend/js/components/list/details.js +++ b/frontend/js/components/list/details.js @@ -1,8 +1,5 @@ import React from "react"; import PropTypes from "prop-types"; -import { Map } from "immutable"; - -import { inLibrary, isWishlisted } from "../../utils"; import { DownloadAndStream } from "../buttons/download"; import { ImdbBadge } from "../buttons/imdb"; @@ -17,9 +14,10 @@ import { Runtime } from "../details/runtime"; import { Title } from "../details/title"; const ListDetails = (props) => { - if (props.data === undefined) { + if (!props.data || Object.keys(props.data).length === 0) { return null; } + if (props.loading) { return null; } @@ -27,46 +25,44 @@ const ListDetails = (props) => { return (
- <ReleaseDate date={props.data.get("year")} /> - <Runtime runtime={props.data.get("runtime")} /> - <Genres genres={props.data.get("genres")} /> - <Rating - rating={props.data.get("rating")} - votes={props.data.get("votes")} + wishlisted={props.wishlisted} /> + <ReleaseDate date={props.data.year} /> + <Runtime runtime={props.data.runtime} /> + <Genres genres={props.data.genres} /> + <Rating rating={props.data.rating} votes={props.data.votes} /> <div> - <ImdbBadge imdbId={props.data.get("imdb_id")} /> + <ImdbBadge imdbId={props.data.imdb_id} /> <DownloadAndStream - url={props.data.get("polochon_url")} - name={props.data.get("title")} - subtitles={props.data.get("subtitles")} + url={props.data.polochon_url} + name={props.data.title} + subtitles={props.data.subtitles} /> </div> <TrackingLabel - wishlisted={props.data.get("wishlisted")} - inLibrary={inLibrary(props.data)} - trackedSeason={props.data.get("tracked_season")} - trackedEpisode={props.data.get("tracked_episode")} + wishlisted={props.data.wishlisted} + inLibrary={props.data.polochon_url !== ""} + trackedSeason={props.data.tracked_season} + trackedEpisode={props.data.tracked_episode} /> <PolochonMetadata - quality={props.data.get("quality")} - releaseGroup={props.data.get("release_group")} - container={props.data.get("container")} - audioCodec={props.data.get("audio_codec")} - videoCodec={props.data.get("video_codec")} + quality={props.data.quality} + releaseGroup={props.data.release_group} + container={props.data.container} + audioCodec={props.data.audio_codec} + videoCodec={props.data.video_codec} /> - <Plot plot={props.data.get("plot")} /> + <Plot plot={props.data.plot} /> {props.children} </div> ); }; ListDetails.propTypes = { - data: PropTypes.instanceOf(Map), + data: PropTypes.object, wishlist: PropTypes.func, + wishlisted: PropTypes.bool, loading: PropTypes.bool, children: PropTypes.object, }; diff --git a/frontend/js/components/list/explorerOptions.js b/frontend/js/components/list/explorerOptions.js index 7ce08dc..688c499 100644 --- a/frontend/js/components/list/explorerOptions.js +++ b/frontend/js/components/list/explorerOptions.js @@ -31,7 +31,7 @@ const ExplorerOptions = ({ const handleSourceChange = (event) => { let source = event.target.value; - let category = options.get(event.target.value).first(); + let category = options[event.target.value][0]; history.push(`/${type}/explore/${source}/${category}`); }; @@ -50,13 +50,13 @@ const ExplorerOptions = ({ }; // Options are not yet fetched - if (options.size === 0) { + if (Object.keys(options).length === 0) { return null; } let source = params.source; let category = params.category; - let categories = options.get(params.source); + let categories = options[params.source]; return ( <div className="row"> @@ -72,7 +72,7 @@ const ExplorerOptions = ({ onChange={handleSourceChange} value={source} > - {options.keySeq().map(function (source) { + {Object.keys(options).map(function (source) { return ( <option key={source} value={source}> {prettyName(source)} diff --git a/frontend/js/components/list/filter.js b/frontend/js/components/list/filter.js index f1ffb5e..fbfa35f 100644 --- a/frontend/js/components/list/filter.js +++ b/frontend/js/components/list/filter.js @@ -8,6 +8,8 @@ const ListFilter = ({ placeHolder, updateFilter }) => { // Start filtering at 3 chars if (filter.length >= 3) { updateFilter(filter); + } else { + updateFilter(""); } }, [filter, updateFilter]); diff --git a/frontend/js/components/list/keyboard.js b/frontend/js/components/list/keyboard.js new file mode 100644 index 0000000..132ad52 --- /dev/null +++ b/frontend/js/components/list/keyboard.js @@ -0,0 +1,106 @@ +import React, { useEffect, useCallback, useRef } from "react"; +import PropTypes from "prop-types"; + +export const KeyboardNavigation = ({ + onKeyEnter, + selectPoster, + children, + list, + selected, +}) => { + const containerRef = useRef(null); + + useEffect(() => { + document.onkeypress = move; + return () => { + document.onkeypress = null; + }; + }, [move, list, selected]); + + const move = useCallback( + (event) => { + // Only run the function if nothing else if actively focused + if (document.activeElement.tagName.toLowerCase() !== "body") { + return; + } + + // Compute the grid dimentions + if (containerRef === null) { + return; + } + const containerWidth = containerRef.current.getBoundingClientRect().width; + const posterContainer = containerRef.current.getElementsByClassName( + "img-thumbnail" + ); + + let posterWidth = 0; + if (posterContainer !== null && posterContainer.item(0) !== null) { + const poster = posterContainer.item(0); + posterWidth = + poster.getBoundingClientRect().width + + poster.getBoundingClientRect().left; + } + let postersPerRow = + posterWidth >= containerWidth + ? 1 + : Math.floor(containerWidth / posterWidth); + + let diff = 0; + switch (event.key) { + case "Enter": + onKeyEnter(selected); + return; + case "l": + diff = 1; + break; + case "h": + diff = -1; + break; + case "k": + diff = -1 * postersPerRow; + break; + case "j": + diff = postersPerRow; + break; + default: + return; + } + + // Get the index of the currently selected item + const idx = list.findIndex((e) => e.imdbId === selected); + + var newIdx = idx + diff; + + // Handle edge cases + if (newIdx > list.length - 1) { + newIdx = list.length - 1; + } else if (newIdx < 0) { + newIdx = 0; + } + + // Get the imdbID of the newly selected item + var selectedImdb = list[newIdx].imdbId; + + // Select the movie + selectPoster(selectedImdb); + containerRef.current + .getElementsByClassName("img-thumbnail") + .item(newIdx) + .scrollIntoView({ + behavior: "smooth", + block: "center", + inline: "nearest", + }); + }, + [list, onKeyEnter, selectPoster, selected] + ); + + return <div ref={containerRef}>{children}</div>; +}; +KeyboardNavigation.propTypes = { + list: PropTypes.array, + selected: PropTypes.string, + onKeyEnter: PropTypes.func, + selectPoster: PropTypes.func, + children: PropTypes.object, +}; diff --git a/frontend/js/components/list/posters.js b/frontend/js/components/list/posters.js index d70feda..692e2c4 100644 --- a/frontend/js/components/list/posters.js +++ b/frontend/js/components/list/posters.js @@ -1,94 +1,115 @@ -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect } from "react"; import PropTypes from "prop-types"; -import { OrderedMap, Map } from "immutable"; import fuzzy from "fuzzy"; import InfiniteScroll from "react-infinite-scroll-component"; import ListFilter from "./filter"; import ExplorerOptions from "./explorerOptions"; import Poster from "./poster"; +import { KeyboardNavigation } from "./keyboard"; import Loader from "../loader/loader"; -const ListPosters = (props) => { - if (props.loading) { +const ListPosters = ({ + data, + exploreFetchOptions, + exploreOptions, + loading, + onClick, + onDoubleClick, + onKeyEnter, + params, + placeHolder, + selectedImdbId, + type, +}) => { + const [list, setList] = useState([]); + const [filteredList, setFilteredList] = useState([]); + const [filter, setFilter] = useState(""); + + useEffect(() => { + let l = []; + data.forEach((e) => { + l.push({ + imdbId: e.imdb_id, + posterUrl: e.poster_url, + title: e.title, + }); + }); + setList(l); + }, [data]); + + useEffect(() => { + if (filter !== "" && filter.length >= 3) { + setFilteredList(list.filter((e) => fuzzy.test(filter, e.title))); + } else { + setFilteredList(list); + } + }, [filter, list]); + + if (loading) { return <Loader />; } - let elmts = props.data; - const listSize = elmts !== undefined ? elmts.size : 0; - - // Filter the list of elements - if (props.filter !== "") { - elmts = elmts.filter((v) => fuzzy.test(props.filter, v.get("title"))); - } else { - elmts = elmts.slice(0, listSize.items); - } - // Chose when to display filter / explore options let displayFilter = true; if ( - (props.params && - props.params.category && - props.params.category !== "" && - props.params.source && - props.params.source !== "") || - listSize === 0 + (params && + params.category && + params.category !== "" && + params.source && + params.source !== "") || + list.length === 0 ) { displayFilter = false; } let displayExplorerOptions = false; - if (listSize !== 0) { + if (list.length !== 0) { displayExplorerOptions = !displayFilter; } return ( <div className="col-4 col-md-8 px-1"> {displayFilter && ( - <ListFilter - updateFilter={props.updateFilter} - placeHolder={props.placeHolder} - /> + <ListFilter updateFilter={setFilter} placeHolder={placeHolder} /> )} <ExplorerOptions - type={props.type} + type={type} display={displayExplorerOptions} - params={props.params} - fetch={props.exploreFetchOptions} - options={props.exploreOptions} + params={params} + fetch={exploreFetchOptions} + options={exploreOptions} /> <Posters - elmts={elmts} - loading={props.loading} - selectedImdbId={props.selectedImdbId} - selectPoster={props.onClick} - onDoubleClick={props.onDoubleClick} - onKeyEnter={props.onKeyEnter} + list={filteredList} + loading={loading} + selectedImdbId={selectedImdbId} + selectPoster={onClick} + onDoubleClick={onDoubleClick} + onKeyEnter={onKeyEnter} /> </div> ); }; ListPosters.propTypes = { - data: PropTypes.instanceOf(OrderedMap), + data: PropTypes.object, onClick: PropTypes.func, onDoubleClick: PropTypes.func, onKeyEnter: PropTypes.func, selectedImdbId: PropTypes.string, loading: PropTypes.bool.isRequired, params: PropTypes.object.isRequired, - exploreOptions: PropTypes.instanceOf(Map), + exploreOptions: PropTypes.object, type: PropTypes.string.isRequired, placeHolder: PropTypes.string.isRequired, - updateFilter: PropTypes.func.isRequired, - filter: PropTypes.string, exploreFetchOptions: PropTypes.func, }; export default ListPosters; const Posters = ({ - elmts, + list, onKeyEnter, selectedImdbId, selectPoster, @@ -96,120 +117,20 @@ const Posters = ({ }) => { const addMoreCount = 20; const [size, setSize] = useState(0); - const [postersPerRow, setPostersPerRow] = useState(0); - const [posterHeight, setPosterHeight] = useState(0); - const [initialLoading, setInitialLoading] = useState(true); - const loadMore = useCallback(() => { - if (size === elmts.size) { - return; - } - - const newSize = - size + addMoreCount >= elmts.size ? elmts.size : size + addMoreCount; - - setSize(newSize); - }, [size, elmts.size]); - - useEffect(() => { - if (initialLoading) { - loadMore(); - setInitialLoading(false); - } - }, [loadMore, initialLoading, setInitialLoading]); - - const move = useCallback( - (event) => { - // Only run the function if nothing else if actively focused - if (document.activeElement.tagName.toLowerCase() !== "body") { - return; - } - - let diff = 0; - let moveFocus = 0; - switch (event.key) { - case "Enter": - onKeyEnter(selectedImdbId); - return; - case "l": - diff = 1; - break; - case "h": - diff = -1; - break; - case "k": - diff = -1 * postersPerRow; - moveFocus = -1 * posterHeight; - break; - case "j": - diff = postersPerRow; - moveFocus = posterHeight; - break; - default: - return; - } - - // Get the index of the currently selected item - const idx = elmts.keySeq().findIndex((k) => k === selectedImdbId); - - var newIdx = idx + diff; - - // Handle edge cases - if (newIdx > elmts.size - 1) { - newIdx = elmts.size - 1; - } else if (newIdx < 0) { - newIdx = 0; - } - - // Get the imdbID of the newly selected item - var selectedImdb = Object.keys(elmts.toJS())[newIdx]; - - // Select the movie - selectPoster(selectedImdb); - if (moveFocus !== 0) { - window.scrollBy(0, moveFocus); - } - }, - [ - elmts, - onKeyEnter, - posterHeight, - postersPerRow, - selectPoster, - selectedImdbId, - ] - ); - - const posterCount = (node) => { - if (node === null) { - return; - } - const parentWidth = node.getBoundingClientRect().width; - const childContainer = node.getElementsByClassName("img-thumbnail"); - let childWidth = 0; - let posterHeight = 0; - if (childContainer !== null && childContainer.item(0) !== null) { - const child = childContainer.item(0); - childWidth = - child.getBoundingClientRect().width + - child.getBoundingClientRect().left; - posterHeight = child.getBoundingClientRect().height; - } - - let numberPerRow = - childWidth >= parentWidth ? 1 : Math.floor(parentWidth / childWidth); - setPostersPerRow(numberPerRow); - setPosterHeight(posterHeight); + const updateSize = (newSize, maxSize) => { + setSize(Math.min(newSize, maxSize)); }; useEffect(() => { - document.onkeypress = move; - return () => { - document.onkeypress = null; - }; - }, [move]); + updateSize(size + addMoreCount, list.length); + }, [list]); // eslint-disable-line react-hooks/exhaustive-deps - if (elmts.size === 0) { + const loadMore = () => { + updateSize(size + addMoreCount, list.length); + }; + + if (list.length === 0) { return ( <div className="jumbotron"> <h2>No result</h2> @@ -218,39 +139,38 @@ const Posters = ({ } return ( - <div ref={posterCount}> + <KeyboardNavigation + onKeyEnter={onKeyEnter} + selectPoster={selectPoster} + list={list} + selected={selectedImdbId} + > <InfiniteScroll className="poster-list d-flex flex-column flex-sm-row flex-sm-wrap justify-content-around" dataLength={size} next={loadMore} - hasMore={size !== elmts.size} + hasMore={size !== list.length} loader={<Loader />} > - {elmts - .slice(0, size) - .toIndexedSeq() - .map(function (el, index) { - const imdbId = el.get("imdb_id"); - const selected = imdbId === selectedImdbId ? true : false; - - return ( - <Poster - url={el.get("poster_url")} - key={index} - selected={selected} - onClick={() => selectPoster(imdbId)} - onDoubleClick={() => onDoubleClick(imdbId)} - /> - ); - }, this)} + {list.slice(0, size).map((el, index) => { + const imdbId = el.imdbId; + return ( + <Poster + url={el.posterUrl} + key={index} + selected={imdbId === selectedImdbId} + onClick={() => selectPoster(imdbId)} + onDoubleClick={() => onDoubleClick(imdbId)} + /> + ); + }, this)} </InfiniteScroll> - </div> + </KeyboardNavigation> ); }; Posters.propTypes = { - elmts: PropTypes.instanceOf(OrderedMap), + list: PropTypes.array.isRequired, selectedImdbId: PropTypes.string, - loading: PropTypes.bool.isRequired, onDoubleClick: PropTypes.func, onKeyEnter: PropTypes.func, selectPoster: PropTypes.func, diff --git a/frontend/js/components/modules/modules.js b/frontend/js/components/modules/modules.js index 0a383ba..8b15afa 100644 --- a/frontend/js/components/modules/modules.js +++ b/frontend/js/components/modules/modules.js @@ -1,65 +1,57 @@ import React from "react"; import Loader from "../loader/loader"; import PropTypes from "prop-types"; -import { Map, List } from "immutable"; // TODO: udpate this import { OverlayTrigger, Tooltip } from "react-bootstrap"; -const Modules = (props) => { - if (props.isLoading) { +const Modules = ({ isLoading, modules }) => { + if (isLoading) { return <Loader />; } return ( <div className="row"> - {props.modules && - props.modules - .keySeq() - .map((value, key) => ( - <ModulesByVideoType - key={key} - videoType={value} - data={props.modules.get(value)} - /> - ))} + {Object.keys(modules).map((type) => ( + <ModulesByVideoType key={type} type={type} modules={modules[type]} /> + ))} </div> ); }; Modules.propTypes = { isLoading: PropTypes.bool.isRequired, - modules: PropTypes.instanceOf(Map), + modules: PropTypes.object.isRequired, }; export default Modules; const capitalize = (string) => string.charAt(0).toUpperCase() + string.slice(1); -const ModulesByVideoType = (props) => ( +const ModulesByVideoType = ({ type, modules }) => ( <div className="col-12 col-md-6"> <div className="card mb-3"> <div className="card-header"> - <h3>{`${capitalize(props.videoType)} modules`}</h3> + <h3>{`${capitalize(type)} modules`}</h3> </div> <div className="card-body"> - {props.data.keySeq().map((value, key) => ( - <ModuleByType key={key} type={value} data={props.data.get(value)} /> + {Object.keys(modules).map((moduleType, i) => ( + <ModuleByType key={i} type={type} modules={modules[moduleType]} /> ))} </div> </div> </div> ); ModulesByVideoType.propTypes = { - videoType: PropTypes.string.isRequired, - data: PropTypes.instanceOf(Map), + type: PropTypes.string.isRequired, + modules: PropTypes.object.isRequired, }; -const ModuleByType = (props) => ( +const ModuleByType = ({ type, modules }) => ( <div> - <h4>{capitalize(props.type)}</h4> + <h4>{capitalize(type)}</h4> <table className="table"> <tbody> - {props.data.map(function (value, key) { - return <Module key={key} type={key} data={value} />; + {modules.map((module, type) => { + return <Module key={type} type={type} module={module} />; })} </tbody> </table> @@ -67,14 +59,13 @@ const ModuleByType = (props) => ( ); ModuleByType.propTypes = { type: PropTypes.string.isRequired, - data: PropTypes.instanceOf(List), + modules: PropTypes.array.isRequired, }; -const Module = (props) => { +const Module = ({ module }) => { let iconClass, prettyStatus, badgeClass; - const name = props.data.get("name"); - switch (props.data.get("status")) { + switch (module.status) { case "ok": iconClass = "fa fa-check-circle"; badgeClass = "badge badge-pill badge-success"; @@ -97,19 +88,17 @@ const Module = (props) => { } const tooltip = ( - <Tooltip id={`tooltip-status-${name}`}> + <Tooltip id={`tooltip-status-${module.name}}`}> <p> <span className={badgeClass}>Status: {prettyStatus}</span> </p> - {props.data.get("error") !== "" && ( - <p>Error: {props.data.get("error")}</p> - )} + {module.error !== "" && <p>Error: {module.error}</p>} </Tooltip> ); return ( <tr> - <th>{name}</th> + <th>{module.name}</th> <td> <OverlayTrigger placement="right" overlay={tooltip}> <span className={badgeClass}> @@ -121,5 +110,5 @@ const Module = (props) => { ); }; Module.propTypes = { - data: PropTypes.instanceOf(Map), + module: PropTypes.object.isRequired, }; diff --git a/frontend/js/components/movies/list.js b/frontend/js/components/movies/list.js index 70b20f5..ad4c619 100644 --- a/frontend/js/components/movies/list.js +++ b/frontend/js/components/movies/list.js @@ -1,11 +1,9 @@ import React, { useEffect, useCallback } from "react"; import PropTypes from "prop-types"; import { useSelector, useDispatch } from "react-redux"; -import { Map } from "immutable"; import { selectMovie, - updateFilter, movieWishlistToggle, fetchMovies, getMovieExploreOptions, @@ -14,8 +12,6 @@ import { import ListDetails from "../list/details"; import ListPosters from "../list/posters"; -import { inLibrary, isWishlisted } from "../../utils"; - import { ShowMore } from "../buttons/showMore"; import { MovieSubtitlesButton } from "./subtitlesButton"; @@ -51,25 +47,17 @@ const MovieList = ({ match }) => { } }, [dispatch, match]); - const loading = useSelector((state) => state.movieStore.get("loading")); - const movies = useSelector((state) => state.movieStore.get("movies")); - const filter = useSelector((state) => state.movieStore.get("filter")); - const selectedImdbId = useSelector((state) => - state.movieStore.get("selectedImdbId") - ); - const exploreOptions = useSelector((state) => - state.movieStore.get("exploreOptions") - ); + const loading = useSelector((state) => state.movies.loading); + const movies = useSelector((state) => state.movies.movies); + const selectedImdbId = useSelector((state) => state.movies.selectedImdbId); + const exploreOptions = useSelector((state) => state.movies.exploreOptions); - let selectedMovie = Map(); + let selectedMovie = {}; if (movies !== undefined && movies.has(selectedImdbId)) { selectedMovie = movies.get(selectedImdbId); } const selectFunc = useCallback((id) => dispatch(selectMovie(id)), [dispatch]); - const filterFunc = useCallback((filter) => dispatch(updateFilter(filter)), [ - dispatch, - ]); const exploreFetchOptions = useCallback( () => dispatch(getMovieExploreOptions()), [dispatch] @@ -84,8 +72,6 @@ const MovieList = ({ match }) => { exploreFetchOptions={exploreFetchOptions} exploreOptions={exploreOptions} selectedImdbId={selectedImdbId} - updateFilter={filterFunc} - filter={filter} onClick={selectFunc} onDoubleClick={() => {}} onKeyEnter={() => {}} @@ -95,31 +81,19 @@ const MovieList = ({ match }) => { <ListDetails data={selectedMovie} loading={loading} + wishlisted={selectedMovie.wishlisted} wishlist={() => dispatch( - movieWishlistToggle( - selectedMovie.get("imdb_id"), - isWishlisted(selectedMovie) - ) + movieWishlistToggle(selectedMovie.imdb_id, selectedMovie.wishlisted) ) } > <ShowMore - id={selectedMovie.get("imdb_id")} - inLibrary={inLibrary(selectedMovie)} + id={selectedMovie.imdb_id} + inLibrary={selectedMovie.polochon_url !== ""} > - <MovieTorrentsButton - torrents={selectedMovie.get("torrents")} - imdbId={selectedMovie.get("imdb_id")} - title={selectedMovie.get("title")} - searching={selectedMovie.get("fetchingDetails", false)} - /> - <MovieSubtitlesButton - subtitles={selectedMovie.get("subtitles")} - inLibrary={inLibrary(selectedMovie)} - imdbId={selectedMovie.get("imdb_id")} - searching={selectedMovie.get("fetchingSubtitles", false)} - /> + <MovieTorrentsButton /> + <MovieSubtitlesButton /> </ShowMore> </ListDetails> </div> diff --git a/frontend/js/components/movies/subtitlesButton.js b/frontend/js/components/movies/subtitlesButton.js index 2f1bd89..f931078 100644 --- a/frontend/js/components/movies/subtitlesButton.js +++ b/frontend/js/components/movies/subtitlesButton.js @@ -1,20 +1,24 @@ import React from "react"; -import PropTypes from "prop-types"; -import { List } from "immutable"; -import { useDispatch } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { searchMovieSubtitles } from "../../actions/subtitles"; import { SubtitlesButton } from "../buttons/subtitles"; -export const MovieSubtitlesButton = ({ - inLibrary, - imdbId, - searching, - subtitles, -}) => { +export const MovieSubtitlesButton = () => { const dispatch = useDispatch(); + const imdbId = useSelector((state) => state.movies.selectedImdbId); + const inLibrary = useSelector( + (state) => state.movies.movies.get(imdbId).polochon_url !== "" + ); + const subtitles = useSelector( + (state) => state.movies.movies.get(imdbId).subtitles + ); + const searching = useSelector( + (state) => state.movies.movies.get(imdbId).fetchingSubtitles + ); + return ( <SubtitlesButton inLibrary={inLibrary} @@ -24,9 +28,3 @@ export const MovieSubtitlesButton = ({ /> ); }; -MovieSubtitlesButton.propTypes = { - searching: PropTypes.bool, - inLibrary: PropTypes.bool, - imdbId: PropTypes.string, - subtitles: PropTypes.instanceOf(List), -}; diff --git a/frontend/js/components/movies/torrentsButton.js b/frontend/js/components/movies/torrentsButton.js index 5905ae0..dc9e6e1 100644 --- a/frontend/js/components/movies/torrentsButton.js +++ b/frontend/js/components/movies/torrentsButton.js @@ -1,13 +1,22 @@ import React from "react"; -import PropTypes from "prop-types"; -import { List } from "immutable"; -import { useDispatch } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { getMovieDetails } from "../../actions/movies"; + import { TorrentsButton } from "../buttons/torrents"; -export const MovieTorrentsButton = ({ torrents, imdbId, title, searching }) => { +export const MovieTorrentsButton = () => { const dispatch = useDispatch(); + + const imdbId = useSelector((state) => state.movies.selectedImdbId); + const title = useSelector((state) => state.movies.movies.get(imdbId).title); + const torrents = useSelector( + (state) => state.movies.movies.get(imdbId).torrents + ); + const searching = useSelector( + (state) => state.movies.movies.get(imdbId).fetchingDetails + ); + return ( <TorrentsButton torrents={torrents} @@ -17,10 +26,3 @@ export const MovieTorrentsButton = ({ torrents, imdbId, title, searching }) => { /> ); }; -MovieTorrentsButton.propTypes = { - torrents: PropTypes.instanceOf(List), - imdbId: PropTypes.string, - title: PropTypes.string, - searching: PropTypes.bool, - getMovieDetails: PropTypes.func, -}; diff --git a/frontend/js/components/navbar.js b/frontend/js/components/navbar.js index 6575f6b..c0f0e5d 100644 --- a/frontend/js/components/navbar.js +++ b/frontend/js/components/navbar.js @@ -8,14 +8,11 @@ import Nav from "react-bootstrap/Nav"; import Navbar from "react-bootstrap/Navbar"; import NavDropdown from "react-bootstrap/NavDropdown"; -const AppNavBar = () => { +export const AppNavBar = () => { const [expanded, setExpanded] = useState(false); - const username = useSelector((state) => state.userStore.get("username")); - const isAdmin = useSelector((state) => state.userStore.get("isAdmin")); - const torrentCount = useSelector( - (state) => state.torrentStore.get("torrents").size - ); + const username = useSelector((state) => state.user.username); + const isAdmin = useSelector((state) => state.user.isAdmin); return ( <Navbar @@ -36,7 +33,7 @@ const AppNavBar = () => { <MoviesDropdown /> <ShowsDropdown /> <WishlistDropdown /> - <TorrentsDropdown torrentsCount={torrentCount} /> + <TorrentsDropdown /> </Nav> <Nav> <Route @@ -70,7 +67,6 @@ const AppNavBar = () => { AppNavBar.propTypes = { history: PropTypes.object, }; -export default AppNavBar; const Search = ({ path, placeholder, setExpanded, history }) => { const [search, setSearch] = useState(""); @@ -159,8 +155,8 @@ const WishlistDropdown = () => ( </NavDropdown> ); -const TorrentsDropdown = (props) => { - const title = <TorrentsDropdownTitle torrentsCount={props.torrentsCount} />; +const TorrentsDropdown = () => { + const title = <TorrentsDropdownTitle />; return ( <NavDropdown title={title} id="navbar-wishlit-dropdown"> <LinkContainer to="/torrents/list"> @@ -172,26 +168,17 @@ const TorrentsDropdown = (props) => { </NavDropdown> ); }; -TorrentsDropdown.propTypes = { torrentsCount: PropTypes.number.isRequired }; -const TorrentsDropdownTitle = (props) => { - if (props.torrentsCount === 0) { +const TorrentsDropdownTitle = () => { + const count = useSelector((state) => state.torrents.torrents.length); + if (count === 0) { return <span>Torrents</span>; } return ( <span> - {" "} Torrents - <span> -  {" "} - <span className="badge badge-info badge-pill"> - {props.torrentsCount} - </span> - </span> + <span className="badge badge-info badge-pill ml-1">{count}</span> </span> ); }; -TorrentsDropdownTitle.propTypes = { - torrentsCount: PropTypes.number.isRequired, -}; diff --git a/frontend/js/components/notifications/notification.js b/frontend/js/components/notifications/notification.js index 0872ce5..c19c8e7 100644 --- a/frontend/js/components/notifications/notification.js +++ b/frontend/js/components/notifications/notification.js @@ -1,58 +1,44 @@ import React, { useState } from "react"; import PropTypes from "prop-types"; -import { useDispatch } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { Toast } from "react-bootstrap"; import { removeNotification } from "../../actions/notifications"; -export const Notification = ({ - id, - icon, - title, - message, - imageUrl, - autohide, - delay, -}) => { +export const Notification = ({ id }) => { const dispatch = useDispatch(); + const notification = useSelector((state) => state.notifications.get(id)); + const [show, setShow] = useState(true); const hide = () => { setShow(false); - setTimeout(() => dispatch(removeNotification(id)), 200); + dispatch(removeNotification(id)); }; return ( - <Toast show={show} onClose={hide} autohide={autohide} delay={delay}> + <Toast + show={show} + onClose={hide} + autohide={notification.autohide || true} + delay={notification.delay || 10000} + > <Toast.Header> - {icon !== "" && <i className={`fa fa-${icon} mr-2`} />} - <strong className="mr-auto">{title}</strong> + {notification.icon && ( + <i className={`fa fa-${notification.icon} mr-2`} /> + )} + <strong className="mr-auto">{notification.title}</strong> </Toast.Header> <Toast.Body> - {message !== "" && <span>{message}</span>} - {imageUrl !== "" && ( - <img src={imageUrl} className="img-fluid mt-2 mr-auto" /> + {notification.message !== "" && <span>{notification.message}</span>} + {notification.imageUrl !== "" && ( + <img src={notification.imageUrl} className="img-fluid mt-2 mr-auto" /> )} </Toast.Body> </Toast> ); }; Notification.propTypes = { - id: PropTypes.string, - icon: PropTypes.string, - title: PropTypes.string, - message: PropTypes.string, - imageUrl: PropTypes.string, - autohide: PropTypes.bool, - delay: PropTypes.number, -}; - -Notification.defaultProps = { - autohide: false, - delay: 5000, - icon: "", - imageUrl: "", - title: "Info", - message: "", + id: PropTypes.string.isRequired, }; diff --git a/frontend/js/components/notifications/notifications.js b/frontend/js/components/notifications/notifications.js index 285b234..15a43a9 100644 --- a/frontend/js/components/notifications/notifications.js +++ b/frontend/js/components/notifications/notifications.js @@ -1,29 +1,18 @@ import React from "react"; -import PropTypes from "prop-types"; -import { List } from "immutable"; import { useSelector } from "react-redux"; import { Notification } from "./notification"; export const Notifications = () => { - const notifications = useSelector((state) => state.notifications); + const notificationIds = useSelector((state) => + Array.from(state.notifications.keys()) + ); + return ( <div className="notifications"> - {notifications.map((el) => ( - <Notification - key={el.get("id")} - id={el.get("id")} - icon={el.get("icon", "video-camera")} - title={el.get("title", "Notification")} - message={el.get("message")} - autohide={el.get("autohide", true)} - delay={el.get("delay", 10000)} - imageUrl={el.get("imageUrl")} - /> + {notificationIds.map((id) => ( + <Notification key={id} id={id} /> ))} </div> ); }; -Notifications.propTypes = { - notifications: PropTypes.instanceOf(List), -}; diff --git a/frontend/js/components/polochons/list.js b/frontend/js/components/polochons/list.js index b1c3010..f72c2fe 100644 --- a/frontend/js/components/polochons/list.js +++ b/frontend/js/components/polochons/list.js @@ -7,7 +7,7 @@ import { Polochon } from "./polochon"; import { PolochonAdd } from "./add"; export const PolochonList = () => { - const list = useSelector((state) => state.polochon.get("managed")); + const list = useSelector((state) => state.polochon.managed); const dispatch = useDispatch(); useEffect(() => { @@ -20,15 +20,15 @@ export const PolochonList = () => { <h2>My polochons</h2> <hr /> <span> - {list.map((el, index) => ( + {list.map((polochon, index) => ( <Polochon key={index} - id={el.get("id")} - name={el.get("name")} - token={el.get("token")} - url={el.get("url")} - authToken={el.get("auth_token")} - users={el.get("users")} + id={polochon.id} + name={polochon.name} + token={polochon.token} + url={polochon.url} + authToken={polochon.auth_token} + users={polochon.users} /> ))} </span> diff --git a/frontend/js/components/polochons/polochon.js b/frontend/js/components/polochons/polochon.js index 3bbc37d..d188576 100644 --- a/frontend/js/components/polochons/polochon.js +++ b/frontend/js/components/polochons/polochon.js @@ -1,6 +1,5 @@ import React, { useState } from "react"; import PropTypes from "prop-types"; -import { List } from "immutable"; import { useDispatch } from "react-redux"; import { PolochonUsers } from "./users"; @@ -57,5 +56,5 @@ Polochon.propTypes = { token: PropTypes.string, url: PropTypes.string, authToken: PropTypes.string, - users: PropTypes.instanceOf(List), + users: PropTypes.array.isRequired, }; diff --git a/frontend/js/components/polochons/select.js b/frontend/js/components/polochons/select.js index 05b6971..d2535ca 100644 --- a/frontend/js/components/polochons/select.js +++ b/frontend/js/components/polochons/select.js @@ -1,8 +1,10 @@ import React from "react"; import PropTypes from "prop-types"; -import { List } from "immutable"; +import { useSelector } from "react-redux"; + +export const PolochonSelect = ({ value, changeValue }) => { + const publicPolochons = useSelector((state) => state.polochon.public); -export const PolochonSelect = ({ value, changeValue, polochonList }) => { return ( <select className="form-control" @@ -11,9 +13,9 @@ export const PolochonSelect = ({ value, changeValue, polochonList }) => { changeValue(e.target.options[e.target.selectedIndex].value) } > - {polochonList.map((el, index) => ( - <option value={el.get("id")} key={index}> - {el.get("name")} ({el.get("url")}) + {publicPolochons.map((polochon) => ( + <option value={polochon.id} key={polochon.id}> + {polochon.name} ({polochon.url}) </option> ))} </select> @@ -21,6 +23,5 @@ export const PolochonSelect = ({ value, changeValue, polochonList }) => { }; PolochonSelect.propTypes = { value: PropTypes.string, - changeValue: PropTypes.func, - polochonList: PropTypes.instanceOf(List), + changeValue: PropTypes.func.isRequired, }; diff --git a/frontend/js/components/polochons/users.js b/frontend/js/components/polochons/users.js index 79e0197..ce767f3 100644 --- a/frontend/js/components/polochons/users.js +++ b/frontend/js/components/polochons/users.js @@ -1,6 +1,5 @@ import React from "react"; import PropTypes from "prop-types"; -import { List } from "immutable"; import { PolochonUser } from "./user"; @@ -23,10 +22,10 @@ export const PolochonUsers = ({ id, users }) => { <PolochonUser key={index} polochonId={id} - id={user.get("id")} - name={user.get("name")} - initialToken={user.get("token")} - initialActivated={user.get("polochon_activated")} + id={user.id} + name={user.name} + initialToken={user.token} + initialActivated={user.polochon_activated} /> ))} </tbody> @@ -34,6 +33,6 @@ export const PolochonUsers = ({ id, users }) => { ); }; PolochonUsers.propTypes = { - id: PropTypes.string, - users: PropTypes.instanceOf(List), + id: PropTypes.string.isRequired, + users: PropTypes.array.isRequired, }; diff --git a/frontend/js/components/shows/details.js b/frontend/js/components/shows/details.js index 2bd4833..a4d59c6 100644 --- a/frontend/js/components/shows/details.js +++ b/frontend/js/components/shows/details.js @@ -12,8 +12,7 @@ import { fetchShowDetails } from "../../actions/shows"; export const ShowDetails = ({ match }) => { const dispatch = useDispatch(); - const loading = useSelector((state) => state.showStore.get("loading")); - const show = useSelector((state) => state.showStore.get("show")); + const loading = useSelector((state) => state.show.loading); useEffect(() => { dispatch(fetchShowDetails(match.params.imdbId)); @@ -25,10 +24,10 @@ export const ShowDetails = ({ match }) => { return ( <React.Fragment> - <Fanart url={show.get("fanart_url")} /> + <Fanart /> <div className="row no-gutters"> - <Header data={show} /> - <SeasonsList data={show} /> + <Header /> + <SeasonsList /> </div> </React.Fragment> ); diff --git a/frontend/js/components/shows/details/episode.js b/frontend/js/components/shows/details/episode.js index ce2c3e8..ac83b5e 100644 --- a/frontend/js/components/shows/details/episode.js +++ b/frontend/js/components/shows/details/episode.js @@ -1,15 +1,10 @@ import React from "react"; import PropTypes from "prop-types"; -import { Map } from "immutable"; import { useDispatch, useSelector } from "react-redux"; import { showWishlistToggle } from "../../../actions/shows"; -import { - inLibrary, - isEpisodeWishlisted, - prettyEpisodeName, -} from "../../../utils"; +import { prettyEpisodeName } from "../../../utils"; import { Plot } from "../../details/plot"; import { PolochonMetadata } from "../../details/polochon"; @@ -24,88 +19,75 @@ import { EpisodeSubtitlesButton } from "./subtitlesButton"; import { EpisodeThumb } from "./episodeThumb"; import { EpisodeTorrentsButton } from "./torrentsButton"; -export const Episode = (props) => { +export const Episode = ({ season, episode }) => { const dispatch = useDispatch(); - const trackedSeason = useSelector((state) => - state.showStore.getIn(["show", "tracked_season"], null) - ); - const trackedEpisode = useSelector((state) => - state.showStore.getIn(["show", "tracked_episode"], null) + const imdbId = useSelector((state) => state.show.show.imdb_id); + const showTitle = useSelector((state) => state.show.show.title); + + const data = useSelector((state) => + state.show.show.seasons.get(season).get(episode) ); + const isWishlisted = useSelector((state) => { + const trackedSeason = state.show.show.tracked_season; + const trackedEpisode = state.show.show.tracked_episode; + if (trackedSeason == null || trackedEpisode == null) { + return false; + } + + if (trackedSeason == 0 || trackedEpisode == 0) { + return true; + } + + if (season < trackedSeason) { + return false; + } else if (season > trackedSeason) { + return true; + } else { + return episode >= trackedEpisode; + } + }); + return ( <div className="d-flex flex-column flex-lg-row mb-3 pb-3 border-bottom border-light"> - <EpisodeThumb url={props.data.get("thumb")} /> + <EpisodeThumb season={season} episode={episode} /> <div className="d-flex flex-column"> <Title - title={`${props.data.get("episode")}. ${props.data.get("title")}`} - wishlisted={isEpisodeWishlisted( - props.data, - trackedSeason, - trackedEpisode - )} + title={`${episode}. ${data.title}`} + wishlisted={isWishlisted} wishlist={() => - dispatch( - showWishlistToggle( - isEpisodeWishlisted(props.data), - props.data.get("show_imdb_id"), - props.data.get("season"), - props.data.get("episode") - ) - ) + dispatch(showWishlistToggle(isWishlisted, imdbId, season, episode)) } /> - <ReleaseDate date={props.data.get("aired")} /> - <Runtime runtime={props.data.get("runtime")} /> - <Plot plot={props.data.get("plot")} /> + <ReleaseDate date={data.aired} /> + <Runtime runtime={data.runtime} /> + <Plot plot={data.plot} /> <DownloadAndStream - name={prettyEpisodeName( - props.showName, - props.data.get("season"), - props.data.get("episode") - )} - url={props.data.get("polochon_url")} - subtitles={props.data.get("subtitles")} + name={prettyEpisodeName(showTitle, season, episode)} + url={data.polochon_url} + subtitles={data.subtitles} /> <PolochonMetadata - quality={props.data.get("quality")} - releaseGroup={props.data.get("release_group")} - container={props.data.get("container")} - audioCodec={props.data.get("audio_codec")} - videoCodec={props.data.get("video_codec")} + quality={data.quality} + releaseGroup={data.release_group} + container={data.container} + audioCodec={data.audio_codec} + videoCodec={data.video_codec} light /> <ShowMore - id={prettyEpisodeName( - props.showName, - props.data.get("season"), - props.data.get("episode") - )} - inLibrary={inLibrary(props.data)} + id={prettyEpisodeName(showTitle, season, episode)} + inLibrary={data.polochon_url !== ""} > - <EpisodeTorrentsButton - torrents={props.data.get("torrents")} - showName={props.showName} - imdbId={props.data.get("show_imdb_id")} - season={props.data.get("season")} - episode={props.data.get("episode")} - searching={props.data.get("fetching")} - /> - <EpisodeSubtitlesButton - subtitles={props.data.get("subtitles")} - inLibrary={inLibrary(props.data)} - searching={props.data.get("fetchingSubtitles", false)} - imdbId={props.data.get("show_imdb_id")} - season={props.data.get("season")} - episode={props.data.get("episode")} - /> + <EpisodeTorrentsButton season={season} episode={episode} /> + <EpisodeSubtitlesButton season={season} episode={episode} /> </ShowMore> </div> </div> ); }; Episode.propTypes = { - data: PropTypes.instanceOf(Map).isRequired, - showName: PropTypes.string.isRequired, + season: PropTypes.number.isRequired, + episode: PropTypes.number.isRequired, }; diff --git a/frontend/js/components/shows/details/episodeThumb.js b/frontend/js/components/shows/details/episodeThumb.js index 26a5e71..a5b8a95 100644 --- a/frontend/js/components/shows/details/episodeThumb.js +++ b/frontend/js/components/shows/details/episodeThumb.js @@ -1,15 +1,23 @@ import React from "react"; import PropTypes from "prop-types"; +import { useSelector } from "react-redux"; + +export const EpisodeThumb = ({ season, episode }) => { + const url = useSelector( + (state) => state.show.show.seasons.get(season).get(episode).thumb + ); -export const EpisodeThumb = ({ url }) => { if (url === "") { return null; } + return ( <div className="mr-0 mr-lg-2 mb-2 mb-lg-0 episode-thumb"> <img src={url} /> </div> ); }; -EpisodeThumb.propTypes = { url: PropTypes.string }; -EpisodeThumb.defaultProps = { url: "" }; +EpisodeThumb.propTypes = { + season: PropTypes.number.isRequired, + episode: PropTypes.number.isRequired, +}; diff --git a/frontend/js/components/shows/details/fanart.js b/frontend/js/components/shows/details/fanart.js index 2aca94e..593da65 100644 --- a/frontend/js/components/shows/details/fanart.js +++ b/frontend/js/components/shows/details/fanart.js @@ -1,14 +1,14 @@ import React from "react"; -import PropTypes from "prop-types"; +import { useSelector } from "react-redux"; -export const Fanart = ({ url }) => ( - <div className="show-fanart mx-n3 mt-n1"> - <img - className="img w-100 object-fit-cover object-position-top mh-40vh" - src={url} - /> - </div> -); -Fanart.propTypes = { - url: PropTypes.string, +export const Fanart = () => { + const url = useSelector((state) => state.show.show.fanart_url); + return ( + <div className="show-fanart mx-n3 mt-n1"> + <img + className="img w-100 object-fit-cover object-position-top mh-40vh" + src={url} + /> + </div> + ); }; diff --git a/frontend/js/components/shows/details/header.js b/frontend/js/components/shows/details/header.js index ece6bf0..b5647ae 100644 --- a/frontend/js/components/shows/details/header.js +++ b/frontend/js/components/shows/details/header.js @@ -1,11 +1,5 @@ import React from "react"; -import PropTypes from "prop-types"; -import { Map } from "immutable"; -import { useDispatch } from "react-redux"; - -import { isWishlisted } from "../../../utils"; - -import { showWishlistToggle } from "../../../actions/shows"; +import { useDispatch, useSelector } from "react-redux"; import { Plot } from "../../details/plot"; import { Rating } from "../../details/rating"; @@ -15,51 +9,62 @@ import { TrackingLabel } from "../../details/tracking"; import { ImdbBadge } from "../../buttons/imdb"; -export const Header = (props) => { +import { showWishlistToggle } from "../../../actions/shows"; + +export const Header = () => { const dispatch = useDispatch(); + const posterUrl = useSelector((state) => state.show.show.poster_url); + const title = useSelector((state) => state.show.show.title); + const imdbId = useSelector((state) => state.show.show.imdb_id); + const year = useSelector((state) => state.show.show.year); + const rating = useSelector((state) => state.show.show.rating); + const plot = useSelector((state) => state.show.show.plot); + + const trackedSeason = useSelector((state) => state.show.show.tracked_season); + const trackedEpisode = useSelector( + (state) => state.show.show.tracked_episode + ); + const wishlisted = useSelector( + (state) => + state.show.show.tracked_season !== null && + state.show.show.tracked_episode !== null + ); + return ( <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 justify-content-center"> - <img - className="overflow-hidden object-fit-cover" - src={props.data.get("poster_url")} - /> + <img className="overflow-hidden object-fit-cover" src={posterUrl} /> </div> <div> <div className="card-body"> <p className="card-title"> <Title - title={props.data.get("title")} - wishlisted={isWishlisted(props.data)} + title={title} + wishlisted={wishlisted} wishlist={() => - dispatch( - showWishlistToggle( - isWishlisted(props.data), - props.data.get("imdb_id") - ) - ) + dispatch(showWishlistToggle(wishlisted, imdbId)) } /> </p> <p className="card-text"> - <ReleaseDate date={props.data.get("year")} /> + <ReleaseDate date={year} /> </p> <p className="card-text"> - <Rating rating={props.data.get("rating")} /> + <Rating rating={rating} /> </p> <p className="card-text"> - <ImdbBadge imdbId={props.data.get("imdb_id")} /> + <ImdbBadge imdbId={imdbId} /> </p> <p className="card-text"> <TrackingLabel inLibrary={false} - trackedSeason={props.data.get("tracked_season")} - trackedEpisode={props.data.get("tracked_episode")} + trackedSeason={trackedSeason} + trackedEpisode={trackedEpisode} /> </p> <p className="card-text"> - <Plot plot={props.data.get("plot")} /> + <Plot plot={plot} /> </p> </div> </div> @@ -67,6 +72,3 @@ export const Header = (props) => { </div> ); }; -Header.propTypes = { - data: PropTypes.instanceOf(Map), -}; diff --git a/frontend/js/components/shows/details/season.js b/frontend/js/components/shows/details/season.js index e1c3e36..a05898c 100644 --- a/frontend/js/components/shows/details/season.js +++ b/frontend/js/components/shows/details/season.js @@ -1,20 +1,28 @@ import React, { useState } from "react"; import PropTypes from "prop-types"; -import { Map } from "immutable"; +import { useSelector } from "react-redux"; import { Episode } from "./episode"; -export const Season = (props) => { +export const Season = ({ season }) => { const [show, setShow] = useState(false); + const episodeNumbers = useSelector((state) => + Array.from(state.show.show.seasons.get(season).keys()) + ); + + const showToggle = () => { + setShow(!show); + }; + return ( <div className="card mb-3 show-season"> - <div className="card-header clickable" onClick={() => setShow(!show)}> + <div className="card-header clickable" onClick={showToggle}> <h5 className="m-0"> - Season {props.season} + Season {season} <small className="text-primary"> {" "} - — ({props.data.toList().size} episodes) + — ({episodeNumbers.length} episodes) </small> <i className={`float-right fa fa-chevron-${show ? "down" : "left"}`} @@ -22,30 +30,17 @@ export const Season = (props) => { </h5> </div> <div className={`card-body ${show ? "d-flex flex-column" : "d-none"}`}> - {props.data.toList().map(function (episode) { - let key = `${episode.get("season")}-${episode.get("episode")}`; - return ( - <Episode - key={key} - data={episode} - showName={props.showName} - addTorrent={props.addTorrent} - addToWishlist={props.addToWishlist} - getEpisodeDetails={props.getEpisodeDetails} - refreshSubtitles={props.refreshSubtitles} - /> - ); - })} + {episodeNumbers.map((episode) => ( + <Episode + key={`${season}-${episode}`} + season={season} + episode={episode} + /> + ))} </div> </div> ); }; Season.propTypes = { - data: PropTypes.instanceOf(Map), - season: PropTypes.number, - showName: PropTypes.string, - addToWishlist: PropTypes.func, - addTorrent: PropTypes.func, - refreshSubtitles: PropTypes.func, - getEpisodeDetails: PropTypes.func, + season: PropTypes.number.isRequired, }; diff --git a/frontend/js/components/shows/details/seasons.js b/frontend/js/components/shows/details/seasons.js index 88b0318..ece930f 100644 --- a/frontend/js/components/shows/details/seasons.js +++ b/frontend/js/components/shows/details/seasons.js @@ -1,37 +1,19 @@ import React from "react"; -import PropTypes from "prop-types"; -import { Map } from "immutable"; +import { useSelector } from "react-redux"; import { Season } from "./season"; -export const SeasonsList = (props) => ( - <div className="col col-12 col-md-10 offset-md-1"> - {props.data - .get("seasons") - .entrySeq() - .map(function ([season, data]) { - if (season === 0) { - return null; - } - return ( - <Season - key={`season-list-key-${season}`} - data={data} - season={season} - showName={props.data.get("title")} - addTorrent={props.addTorrent} - addToWishlist={props.addToWishlist} - getEpisodeDetails={props.getEpisodeDetails} - refreshSubtitles={props.refreshSubtitles} - /> - ); - })} - </div> -); -SeasonsList.propTypes = { - data: PropTypes.instanceOf(Map), - addToWishlist: PropTypes.func, - addTorrent: PropTypes.func, - refreshSubtitles: PropTypes.func, - getEpisodeDetails: PropTypes.func, +export const SeasonsList = () => { + const seasonNumbers = useSelector((state) => { + const keys = state.show.show.seasons ? state.show.show.seasons.keys() : []; + return Array.from(keys); + }); + + return ( + <div className="col col-12 col-md-10 offset-md-1"> + {seasonNumbers.map((season) => ( + <Season key={`season-list-key-${season}`} season={season} /> + ))} + </div> + ); }; diff --git a/frontend/js/components/shows/details/subtitlesButton.js b/frontend/js/components/shows/details/subtitlesButton.js index 70a8b31..80a761e 100644 --- a/frontend/js/components/shows/details/subtitlesButton.js +++ b/frontend/js/components/shows/details/subtitlesButton.js @@ -1,37 +1,45 @@ import React from "react"; import PropTypes from "prop-types"; -import { List } from "immutable"; -import { useDispatch } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { searchEpisodeSubtitles } from "../../../actions/subtitles"; import { SubtitlesButton } from "../../buttons/subtitles"; -export const EpisodeSubtitlesButton = ({ - inLibrary, - imdbId, - season, - episode, - searching, - subtitles, -}) => { +export const EpisodeSubtitlesButton = ({ season, episode }) => { const dispatch = useDispatch(); + const imdbId = useSelector((state) => state.show.show.imdb_id); + const searching = useSelector((state) => + state.show.show.seasons.get(season).get(episode).fetchingSubtitles + ? state.show.show.seasons.get(season).get(episode).fetchingSubtitles + : false + ); + const inLibrary = useSelector( + (state) => + state.show.show.seasons.get(season).get(episode).polochon_url !== "" + ); + const subtitles = useSelector((state) => + state.show.show.seasons.get(season).get(episode).subtitles + ? state.show.show.seasons.get(season).get(episode).subtitles + : [] + ); + + const search = () => { + dispatch(searchEpisodeSubtitles(imdbId, season, episode)); + }; + return ( <SubtitlesButton subtitles={subtitles} inLibrary={inLibrary} searching={searching} - search={() => dispatch(searchEpisodeSubtitles(imdbId, season, episode))} + search={search} /> ); }; EpisodeSubtitlesButton.propTypes = { - inLibrary: PropTypes.bool, - searching: PropTypes.bool, - imdbId: PropTypes.string, - season: PropTypes.number, - episode: PropTypes.number, - subtitles: PropTypes.instanceOf(List), + season: PropTypes.number.isRequired, + episode: PropTypes.number.isRequired, }; diff --git a/frontend/js/components/shows/details/torrentsButton.js b/frontend/js/components/shows/details/torrentsButton.js index f1125ab..b739ac1 100644 --- a/frontend/js/components/shows/details/torrentsButton.js +++ b/frontend/js/components/shows/details/torrentsButton.js @@ -1,39 +1,46 @@ import React from "react"; import PropTypes from "prop-types"; -import { useDispatch } from "react-redux"; -import { List } from "immutable"; +import { useDispatch, useSelector } from "react-redux"; import { getEpisodeDetails } from "../../../actions/shows"; import { TorrentsButton } from "../../buttons/torrents"; import { prettyEpisodeName } from "../../../utils"; -export const EpisodeTorrentsButton = ({ - torrents, - imdbId, - season, - episode, - showName, - searching, -}) => { +export const EpisodeTorrentsButton = ({ season, episode }) => { const dispatch = useDispatch(); + const imdbId = useSelector((state) => state.show.show.imdb_id); + const search = () => { + dispatch(getEpisodeDetails(imdbId, season, episode)); + }; + const searching = useSelector((state) => + state.show.show.seasons.get(season).get(episode).fetching + ? state.show.show.seasons.get(season).get(episode).fetching + : false + ); + const torrents = useSelector((state) => + state.show.show.seasons.get(season).get(episode).torrents + ? state.show.show.seasons.get(season).get(episode).torrents + : [] + ); + + const url = useSelector( + (state) => + "#/torrents/search/shows/" + + encodeURI(prettyEpisodeName(state.show.show.title, season, episode)) + ); + return ( <TorrentsButton torrents={torrents} searching={searching} - search={() => dispatch(getEpisodeDetails(imdbId, season, episode))} - url={`#/torrents/search/shows/${encodeURI( - prettyEpisodeName(showName, season, episode) - )}`} + search={search} + url={url} /> ); }; EpisodeTorrentsButton.propTypes = { - torrents: PropTypes.instanceOf(List), - showName: PropTypes.string.isRequired, - imdbId: PropTypes.string.isRequired, episode: PropTypes.number.isRequired, season: PropTypes.number.isRequired, - searching: PropTypes.bool.isRequired, }; diff --git a/frontend/js/components/shows/list.js b/frontend/js/components/shows/list.js index 85703a6..45e480a 100644 --- a/frontend/js/components/shows/list.js +++ b/frontend/js/components/shows/list.js @@ -1,18 +1,14 @@ import React, { useEffect, useCallback } from "react"; import PropTypes from "prop-types"; -import { Map } from "immutable"; import { useSelector, useDispatch } from "react-redux"; import { fetchShows, selectShow, showWishlistToggle, - updateFilter, getShowExploreOptions, } from "../../actions/shows"; -import { isWishlisted } from "../../utils"; - import ListDetails from "../list/details"; import ListPosters from "../list/posters"; @@ -46,34 +42,30 @@ const ShowList = ({ match, history }) => { } }, [dispatch, match]); - const loading = useSelector((state) => state.showsStore.get("loading")); - const shows = useSelector((state) => state.showsStore.get("shows")); - const filter = useSelector((state) => state.showsStore.get("filter")); - const selectedImdbId = useSelector((state) => - state.showsStore.get("selectedImdbId") - ); - const exploreOptions = useSelector((state) => - state.showsStore.get("exploreOptions") - ); + const loading = useSelector((state) => state.shows.loading); + const shows = useSelector((state) => state.shows.shows); + const selectedImdbId = useSelector((state) => state.shows.selectedImdbId); + const exploreOptions = useSelector((state) => state.shows.exploreOptions); const showDetails = (imdbId) => { history.push("/shows/details/" + imdbId); }; - let selectedShow = Map(); + let selectedShow = new Map(); if (selectedImdbId !== "") { selectedShow = shows.get(selectedImdbId); } const selectFunc = useCallback((id) => dispatch(selectShow(id)), [dispatch]); - const filterFunc = useCallback((filter) => dispatch(updateFilter(filter)), [ - dispatch, - ]); const exploreFetchOptions = useCallback( () => dispatch(getShowExploreOptions()), [dispatch] ); + const wishlisted = + selectedShow.tracked_season !== null && + selectedShow.tracked_episode !== null; + return ( <div className="row" id="container"> <ListPosters @@ -82,9 +74,7 @@ const ShowList = ({ match, history }) => { placeHolder="Filter shows..." exploreOptions={exploreOptions} exploreFetchOptions={exploreFetchOptions} - updateFilter={filterFunc} selectedImdbId={selectedImdbId} - filter={filter} onClick={selectFunc} onDoubleClick={showDetails} onKeyEnter={showDetails} @@ -94,18 +84,14 @@ const ShowList = ({ match, history }) => { <ListDetails data={selectedShow} loading={loading} + wishlisted={wishlisted} wishlist={() => - dispatch( - showWishlistToggle( - isWishlisted(selectedShow), - selectedShow.get("imdb_id") - ) - ) + dispatch(showWishlistToggle(wishlisted, selectedShow.imdb_id)) } > <span> <button - onClick={() => showDetails(selectedShow.get("imdb_id"))} + onClick={() => showDetails(selectedShow.imdb_id)} className="btn btn-primary btn-sm w-md-100" > <i className="fa fa-external-link mr-1" /> diff --git a/frontend/js/components/torrents/list.js b/frontend/js/components/torrents/list.js index f0e6240..6bfbff8 100644 --- a/frontend/js/components/torrents/list.js +++ b/frontend/js/components/torrents/list.js @@ -1,30 +1,23 @@ import React, { useState } from "react"; import PropTypes from "prop-types"; -import { Map, List } from "immutable"; import { useDispatch, useSelector } from "react-redux"; import { prettySize } from "../../utils"; import { addTorrent, removeTorrent } from "../../actions/torrents"; -const TorrentList = () => { - const torrents = useSelector((state) => state.torrentStore.get("torrents")); - const dispatch = useDispatch(); - +export const TorrentList = () => { return ( <div className="row"> <div className="col-12"> - <AddTorrent addTorrent={(url) => dispatch(addTorrent(url))} /> - <Torrents - torrents={torrents} - removeTorrent={(id) => dispatch(removeTorrent(id))} - /> + <AddTorrent /> + <Torrents /> </div> </div> ); }; -export default TorrentList; -const AddTorrent = (props) => { +const AddTorrent = () => { + const dispatch = useDispatch(); const [url, setUrl] = useState(""); const handleSubmit = (e) => { @@ -32,7 +25,7 @@ const AddTorrent = (props) => { if (url === "") { return; } - props.addTorrent(url); + dispatch(addTorrent(url)); setUrl(""); }; @@ -49,12 +42,11 @@ const AddTorrent = (props) => { </form> ); }; -AddTorrent.propTypes = { - addTorrent: PropTypes.func.isRequired, -}; -const Torrents = (props) => { - if (props.torrents.size === 0) { +const Torrents = () => { + const torrents = useSelector((state) => state.torrents.torrents); + + if (torrents.length === 0) { return ( <div className="jumbotron"> <h2>No torrents</h2> @@ -64,45 +56,38 @@ const Torrents = (props) => { return ( <div className="d-flex flex-wrap"> - {props.torrents.map((el, index) => ( - <Torrent key={index} data={el} removeTorrent={props.removeTorrent} /> + {torrents.map((torrent, index) => ( + <Torrent key={index} torrent={torrent} /> ))} </div> ); }; -Torrents.propTypes = { - removeTorrent: PropTypes.func.isRequired, - torrents: PropTypes.instanceOf(List), -}; -const Torrent = (props) => { - const handleClick = () => { - props.removeTorrent(props.data.get("id")); - }; +const Torrent = ({ torrent }) => { + const dispatch = useDispatch(); - const done = props.data.get("is_finished"); - var progressStyle = done + var progressStyle = torrent.is_finished ? "success" : "info progress-bar-striped progress-bar-animated"; const progressBarClass = "progress-bar bg-" + progressStyle; - var percentDone = props.data.get("percent_done"); + var percentDone = torrent.percent_done; const started = percentDone !== 0; if (started) { percentDone = Number(percentDone).toFixed(1) + "%"; } // Pretty sizes - const downloadedSize = prettySize(props.data.get("downloaded_size")); - const totalSize = prettySize(props.data.get("total_size")); - const downloadRate = prettySize(props.data.get("download_rate")) + "/s"; + const downloadedSize = prettySize(torrent.downloaded_size); + const totalSize = prettySize(torrent.total_size); + const downloadRate = prettySize(torrent.download_rate) + "/s"; return ( <div className="card w-100 mb-3"> <h5 className="card-header"> - <span className="text text-break">{props.data.get("name")}</span> + <span className="text text-break">{torrent.name}</span> <span className="fa fa-trash clickable pull-right" - onClick={() => handleClick()} + onClick={() => dispatch(removeTorrent(torrent.id))} ></span> </h5> <div className="card-body pb-0"> @@ -129,6 +114,5 @@ const Torrent = (props) => { ); }; Torrent.propTypes = { - removeTorrent: PropTypes.func.isRequired, - data: PropTypes.instanceOf(Map), + torrent: PropTypes.object.isRequired, }; diff --git a/frontend/js/components/torrents/search.js b/frontend/js/components/torrents/search.js index 68025d4..301fef9 100644 --- a/frontend/js/components/torrents/search.js +++ b/frontend/js/components/torrents/search.js @@ -2,21 +2,15 @@ import React, { useState, useEffect, useCallback } from "react"; import PropTypes from "prop-types"; import { useDispatch, useSelector } from "react-redux"; import { addTorrent, searchTorrents } from "../../actions/torrents"; -import { Map, List } from "immutable"; import Loader from "../loader/loader"; import { OverlayTrigger, Tooltip } from "react-bootstrap"; import { prettySize } from "../../utils"; -const TorrentSearch = ({ history, match }) => { +export const TorrentSearch = ({ history, match }) => { const dispatch = useDispatch(); - const searching = useSelector((state) => state.torrentStore.get("searching")); - const results = useSelector((state) => - state.torrentStore.get("searchResults") - ); - const [search, setSearch] = useState(match.params.search || ""); const [type, setType] = useState(match.params.type || ""); @@ -74,18 +68,13 @@ const TorrentSearch = ({ history, match }) => { </div> </div> <div className="col-12"> - <TorrentList - searching={searching} - results={results} - addTorrent={(url) => dispatch(addTorrent(url))} - searchFromURL={search} - /> + <TorrentList search={search} /> </div> </div> ); }; TorrentSearch.propTypes = { - searchFromURL: PropTypes.string, + search: PropTypes.string, match: PropTypes.object, history: PropTypes.object, }; @@ -109,16 +98,19 @@ SearchButton.propTypes = { handleClick: PropTypes.func.isRequired, }; -const TorrentList = (props) => { - if (props.searching) { +const TorrentList = ({ search }) => { + const searching = useSelector((state) => state.torrents.searching); + const results = useSelector((state) => state.torrents.searchResults); + + if (searching) { return <Loader />; } - if (props.searchFromURL === "") { + if (search === "") { return null; } - if (props.results.size === 0) { + if (results.size === 0) { return ( <div className="jumbotron"> <h2>No results</h2> @@ -128,63 +120,60 @@ const TorrentList = (props) => { return ( <React.Fragment> - {props.results.map(function (el, index) { - return <Torrent key={index} data={el} addTorrent={props.addTorrent} />; + {results.map(function (el, index) { + return <Torrent key={index} torrent={el} />; })} </React.Fragment> ); }; TorrentList.propTypes = { - searching: PropTypes.bool.isRequired, - results: PropTypes.instanceOf(List), - searchFromURL: PropTypes.string, - addTorrent: PropTypes.func.isRequired, + search: PropTypes.string, }; -const Torrent = (props) => ( - <div className="alert d-flex border-bottom border-secondary align-items-center"> - <TorrentHealth - url={props.data.get("url")} - seeders={props.data.get("seeders")} - leechers={props.data.get("leechers")} - /> - <span className="mx-3 text text-start text-break flex-fill"> - {props.data.get("name")} - </span> - <div> - {props.data.get("size") !== 0 && ( +const Torrent = ({ torrent }) => { + const dispatch = useDispatch(); + + return ( + <div className="alert d-flex border-bottom border-secondary align-items-center"> + <TorrentHealth + url={torrent.url} + seeders={torrent.seeders} + leechers={torrent.leechers} + /> + <span className="mx-3 text text-start text-break flex-fill"> + {torrent.name} + </span> + <div> + {torrent.size !== 0 && ( + <span className="mx-1 badge badge-pill badge-warning"> + {prettySize(torrent.size)} + </span> + )} + <span className="mx-1 badge badge-pill badge-warning"> - {prettySize(props.data.get("size"))} + {torrent.quality} </span> - )} - - <span className="mx-1 badge badge-pill badge-warning"> - {props.data.get("quality")} - </span> - <span className="mx-1 badge badge-pill badge-success"> - {props.data.get("source")} - </span> - <span className="mx-1 badge badge-pill badge-info"> - {props.data.get("upload_user")} - </span> + <span className="mx-1 badge badge-pill badge-success"> + {torrent.source} + </span> + <span className="mx-1 badge badge-pill badge-info"> + {torrent.upload_user} + </span> + </div> + <div className="align-self-end ml-3"> + <i + className="fa fa-cloud-download clickable" + onClick={() => dispatch(addTorrent(torrent.url))} + ></i> + </div> </div> - <div className="align-self-end ml-3"> - <i - className="fa fa-cloud-download clickable" - onClick={() => props.addTorrent(props.data.get("url"))} - ></i> - </div> - </div> -); + ); +}; Torrent.propTypes = { - data: PropTypes.instanceOf(Map), - addTorrent: PropTypes.func.isRequired, + torrent: PropTypes.object.isRequired, }; -const TorrentHealth = (props) => { - const seeders = props.seeders || 0; - const leechers = props.leechers || 1; - +const TorrentHealth = ({ url, seeders = 0, leechers = 1 }) => { let color; let health; let ratio = seeders / leechers; @@ -204,12 +193,12 @@ const TorrentHealth = (props) => { const className = `align-self-start text text-center text-${color}`; const tooltip = ( - <Tooltip id={`tooltip-health-${props.url}`}> + <Tooltip id={`tooltip-health-${url}`}> <p> <span className={className}>Health: {health}</span> </p> <p>Seeders: {seeders}</p> - <p>Leechers: {props.leechers}</p> + <p>Leechers: {leechers}</p> </Tooltip> ); @@ -226,5 +215,3 @@ TorrentHealth.propTypes = { seeders: PropTypes.number, leechers: PropTypes.number, }; - -export default TorrentSearch; diff --git a/frontend/js/components/users/activation.js b/frontend/js/components/users/activation.js index 34387a3..25b03bf 100644 --- a/frontend/js/components/users/activation.js +++ b/frontend/js/components/users/activation.js @@ -2,11 +2,9 @@ import React from "react"; import { useSelector } from "react-redux"; import { Redirect, Link } from "react-router-dom"; -const UserActivation = () => { - const isLogged = useSelector((state) => state.userStore.get("isLogged")); - const isActivated = useSelector((state) => - state.userStore.get("isActivated") - ); +export const UserActivation = () => { + const isLogged = useSelector((state) => state.user.isLogged); + const isActivated = useSelector((state) => state.user.isActivated); if (!isLogged) { return <Redirect to="/users/login" />; @@ -30,4 +28,3 @@ const UserActivation = () => { </div> ); }; -export default UserActivation; diff --git a/frontend/js/components/users/edit.js b/frontend/js/components/users/edit.js index c24e24f..3f0ea7c 100644 --- a/frontend/js/components/users/edit.js +++ b/frontend/js/components/users/edit.js @@ -14,11 +14,10 @@ export const UserEdit = () => { const [password, setPassword] = useState(""); const [passwordConfirm, setPasswordConfirm] = useState(""); - const loading = useSelector((state) => state.userStore.get("loading")); - const publicPolochons = useSelector((state) => state.polochon.get("public")); - const polochonId = useSelector((state) => state.userStore.get("polochonId")); - const polochonActivated = useSelector((state) => - state.userStore.get("polochonActivated") + const loading = useSelector((state) => state.user.loading); + const polochonId = useSelector((state) => state.user.polochonId); + const polochonActivated = useSelector( + (state) => state.user.polochonActivated ); useEffect(() => { @@ -60,11 +59,7 @@ export const UserEdit = () => { </span> )} </label> - <PolochonSelect - value={id} - changeValue={setId} - polochonList={publicPolochons} - /> + <PolochonSelect value={id} changeValue={setId} /> </div> <hr /> diff --git a/frontend/js/components/users/login.js b/frontend/js/components/users/login.js index 69b4162..bc1c54b 100644 --- a/frontend/js/components/users/login.js +++ b/frontend/js/components/users/login.js @@ -4,12 +4,12 @@ import { Redirect, Link } from "react-router-dom"; import { loginUser } from "../../actions/users"; -const UserLoginForm = () => { +export const UserLoginForm = () => { const dispatch = useDispatch(); - const isLogged = useSelector((state) => state.userStore.get("isLogged")); - const isLoading = useSelector((state) => state.userStore.get("loading")); - const error = useSelector((state) => state.userStore.get("error")); + const isLogged = useSelector((state) => state.user.isLogged); + const isLoading = useSelector((state) => state.user.loading); + const error = useSelector((state) => state.user.error); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); @@ -85,5 +85,3 @@ const UserLoginForm = () => { </div> ); }; - -export default UserLoginForm; diff --git a/frontend/js/components/users/logout.js b/frontend/js/components/users/logout.js index 43535f4..9889232 100644 --- a/frontend/js/components/users/logout.js +++ b/frontend/js/components/users/logout.js @@ -4,15 +4,14 @@ import { Redirect } from "react-router-dom"; import { userLogout } from "../../actions/users"; -const UserLogout = () => { +export const UserLogout = () => { const dispatch = useDispatch(); - const isLogged = useSelector((state) => state.userStore.get("isLogged")); + const isLogged = useSelector((state) => state.user.isLogged); if (isLogged) { dispatch(userLogout()); + return null; + } else { + return <Redirect to="/users/login" />; } - - return <Redirect to="/users/login" />; }; - -export default UserLogout; diff --git a/frontend/js/components/users/profile.js b/frontend/js/components/users/profile.js index 9e07b24..98db391 100644 --- a/frontend/js/components/users/profile.js +++ b/frontend/js/components/users/profile.js @@ -7,12 +7,10 @@ import { UserEdit } from "./edit"; import { getUserModules } from "../../actions/users"; import Modules from "../modules/modules"; -const UserProfile = () => { +export const UserProfile = () => { const dispatch = useDispatch(); - const modules = useSelector((state) => state.userStore.get("modules")); - const modulesLoading = useSelector((state) => - state.userStore.get("modulesLoading") - ); + const modules = useSelector((state) => state.user.modules); + const modulesLoading = useSelector((state) => state.user.modulesLoading); useEffect(() => { dispatch(getUserModules()); @@ -26,5 +24,3 @@ const UserProfile = () => { </div> ); }; - -export default UserProfile; diff --git a/frontend/js/components/users/signup.js b/frontend/js/components/users/signup.js index f7b7fb1..0224513 100644 --- a/frontend/js/components/users/signup.js +++ b/frontend/js/components/users/signup.js @@ -4,16 +4,16 @@ import { Redirect } from "react-router-dom"; import { userSignUp } from "../../actions/users"; -const UserSignUp = () => { +export const UserSignUp = () => { const dispatch = useDispatch(); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [passwordConfirm, setPasswordConfirm] = useState(""); - const isLogged = useSelector((state) => state.userStore.get("isLogged")); - const isLoading = useSelector((state) => state.userStore.get("loading")); - const error = useSelector((state) => state.userStore.get("error")); + const isLogged = useSelector((state) => state.user.isLogged); + const isLoading = useSelector((state) => state.user.loading); + const error = useSelector((state) => state.user.error); if (isLogged) { return <Redirect to="/" />; @@ -90,5 +90,3 @@ const UserSignUp = () => { </div> ); }; - -export default UserSignUp; diff --git a/frontend/js/components/users/tokens.js b/frontend/js/components/users/tokens.js index 4c9e999..2d4c415 100644 --- a/frontend/js/components/users/tokens.js +++ b/frontend/js/components/users/tokens.js @@ -3,14 +3,12 @@ import PropTypes from "prop-types"; import { useDispatch, useSelector } from "react-redux"; import { UAParser } from "ua-parser-js"; import moment from "moment"; -import { Map } from "immutable"; import { getUserTokens, deleteUserToken } from "../../actions/users"; -const UserTokens = () => { +export const UserTokens = () => { const dispatch = useDispatch(); - - const tokens = useSelector((state) => state.userStore.get("tokens")); + const tokens = useSelector((state) => state.user.tokens); useEffect(() => { dispatch(getUserTokens()); @@ -19,34 +17,30 @@ const UserTokens = () => { return ( <div className="row"> <div className="col-12"> - {tokens.map((el, index) => ( - <Token - key={index} - data={el} - deleteToken={(token) => dispatch(deleteUserToken(token))} - /> + {tokens.map((token, index) => ( + <Token key={index} token={token} /> ))} </div> </div> ); }; -const Token = (props) => { - const ua = UAParser(props.data.get("user_agent")); +const Token = ({ token }) => { + const ua = UAParser(token.user_agent); return ( <div className="card mt-3"> <div className="card-header"> <h4> <Logo {...ua} /> - <span className="ml-3">{props.data.get("description")}</span> - <Actions {...props} /> + <span className="ml-3">{token.description}</span> + <Actions token={token.token} /> </h4> </div> <div className="card-body row"> <div className="col-12 col-md-6"> - <p>Last IP: {props.data.get("ip")}</p> - <p>Last used: {moment(props.data.get("last_used")).fromNow()}</p> - <p>Created: {moment(props.data.get("created_at")).fromNow()}</p> + <p>Last IP: {token.ip}</p> + <p>Last used: {moment(token.last_used).fromNow()}</p> + <p>Created: {moment(token.created_at).fromNow()}</p> </div> <div className="col-12 col-md-6"> <p> @@ -64,12 +58,14 @@ const Token = (props) => { ); }; Token.propTypes = { - data: PropTypes.instanceOf(Map).isRequired, + token: PropTypes.object.isRequired, }; -const Actions = (props) => { +const Actions = ({ token }) => { + const dispatch = useDispatch(); + const handleClick = () => { - props.deleteToken(props.data.get("token")); + dispatch(deleteUserToken(token)); }; return ( @@ -80,8 +76,7 @@ const Actions = (props) => { ); }; Actions.propTypes = { - data: PropTypes.instanceOf(Map).isRequired, - deleteToken: PropTypes.func.isRequired, + token: PropTypes.string.isRequired, }; const Logo = ({ ua, device, browser }) => { @@ -165,5 +160,3 @@ Browser.propTypes = { name: PropTypes.string, version: PropTypes.string, }; - -export default UserTokens; diff --git a/frontend/js/components/websocket.js b/frontend/js/components/websocket.js index 9082461..6438570 100644 --- a/frontend/js/components/websocket.js +++ b/frontend/js/components/websocket.js @@ -4,10 +4,10 @@ import { setFetchedTorrents } from "../actions/torrents"; import { newMovieEvent } from "../actions/movies"; import { newEpisodeEvent } from "../actions/shows"; -const WsHandler = () => { +export const WsHandler = () => { const dispatch = useDispatch(); - const isLogged = useSelector((state) => state.userStore.get("isLogged")); + const isLogged = useSelector((state) => state.user.isLogged); const [ws, setWs] = useState(null); @@ -107,5 +107,3 @@ const WsHandler = () => { return null; }; - -export default WsHandler; diff --git a/frontend/js/reducers/admins.js b/frontend/js/reducers/admins.js index fefc0d7..3d0d380 100644 --- a/frontend/js/reducers/admins.js +++ b/frontend/js/reducers/admins.js @@ -1,26 +1,39 @@ -import { Map, List, fromJS } from "immutable"; +import { produce } from "immer"; -export const defaultState = Map({ +export const defaultState = { fetchingModules: false, - users: List(), - stats: Map({}), - modules: Map({}), -}); - -const handlers = { - ADMIN_LIST_USERS_FULFILLED: (state, action) => - state.set("users", fromJS(action.payload.response.data)), - ADMIN_GET_STATS_FULFILLED: (state, action) => - state.set("stats", fromJS(action.payload.response.data)), - ADMIN_GET_MODULES_PENDING: (state) => state.set("fetchingModules", true), - ADMIN_GET_MODULES_FULFILLED: (state, action) => - state.merge( - fromJS({ - modules: action.payload.response.data, - fetchingModules: false, - }) - ), + users: new Map(), + stats: {}, + modules: {}, }; export default (state = defaultState, action) => - handlers[action.type] ? handlers[action.type](state, action) : state; + produce(state, (draft) => { + switch (action.type) { + case "ADMIN_LIST_USERS_FULFILLED": { + action.payload.response.data.forEach((user) => { + draft.users.set(user.id, user); + }); + break; + } + + case "ADMIN_GET_STATS_FULFILLED": { + draft.stats = action.payload.response.data; + break; + } + + case "ADMIN_GET_MODULES_PENDING": { + draft.fetchingModules = true; + break; + } + + case "ADMIN_GET_MODULES_FULFILLED": { + draft.fetchingModules = false; + draft.modules = action.payload.response.data; + break; + } + + default: + return draft; + } + }); diff --git a/frontend/js/reducers/alerts.js b/frontend/js/reducers/alerts.js index 2f83324..88ac5d6 100644 --- a/frontend/js/reducers/alerts.js +++ b/frontend/js/reducers/alerts.js @@ -1,37 +1,33 @@ -import { Map } from "immutable"; +import { produce } from "immer"; -const defaultState = Map({ +const defaultState = { show: false, message: "", type: "", -}); - -const handlers = { - ADD_ALERT_ERROR: (state, action) => - state.merge( - Map({ - message: action.payload.message, - show: true, - type: "error", - }) - ), - ADD_ALERT_OK: (state, action) => - state.merge( - Map({ - message: action.payload.message, - show: true, - type: "success", - }) - ), - DISMISS_ALERT: (state) => - state.merge( - Map({ - message: "", - show: false, - type: "", - }) - ), }; export default (state = defaultState, action) => - handlers[action.type] ? handlers[action.type](state, action) : state; + produce(state, (draft) => { + switch (action.type) { + case "ADD_ALERT_ERROR": + draft.show = true; + draft.type = "error"; + draft.message = action.payload.message; + break; + + case "ADD_ALERT_OK": + draft.show = true; + draft.type = "success"; + draft.message = action.payload.message; + break; + + case "DISMISS_ALERT": + draft.show = false; + draft.type = ""; + draft.message = ""; + break; + + default: + return draft; + } + }); diff --git a/frontend/js/reducers/index.js b/frontend/js/reducers/index.js index cd45e16..997ab0a 100644 --- a/frontend/js/reducers/index.js +++ b/frontend/js/reducers/index.js @@ -1,23 +1,27 @@ import { combineReducers } from "redux"; -import movieStore from "./movies"; -import showsStore from "./shows"; -import showStore from "./show"; -import userStore from "./users"; +// Immer +import { enableMapSet } from "immer"; +enableMapSet(); + +import movies from "./movies"; +import shows from "./shows"; +import show from "./show"; +import user from "./users"; import alerts from "./alerts"; -import torrentStore from "./torrents"; -import adminStore from "./admins"; +import torrents from "./torrents"; +import admin from "./admins"; import polochon from "./polochon"; import notifications from "./notifications"; export default combineReducers({ - movieStore, - showsStore, - showStore, - userStore, + movies, + shows, + show, + user, alerts, - torrentStore, - adminStore, + torrents, + admin, polochon, notifications, }); diff --git a/frontend/js/reducers/movies.js b/frontend/js/reducers/movies.js index 96ef659..21a9ce2 100644 --- a/frontend/js/reducers/movies.js +++ b/frontend/js/reducers/movies.js @@ -1,100 +1,106 @@ -import { OrderedMap, Map, fromJS } from "immutable"; +import { produce } from "immer"; +import { formatTorrents } from "../utils"; -const defaultState = Map({ +const defaultState = { loading: false, - movies: OrderedMap(), - filter: "", + movies: new Map(), selectedImdbId: "", lastFetchUrl: "", - exploreOptions: Map(), -}); + exploreOptions: {}, +}; -const handlers = { - MOVIE_LIST_FETCH_PENDING: (state) => state.set("loading", true), - MOVIE_LIST_FETCH_ERROR: (state) => state.set("loading", false), - MOVIE_LIST_FETCH_FULFILLED: (state, action) => { - let allMoviesInPolochon = true; - let movies = Map(); - action.payload.response.data.map(function (movie) { - movie.fetchingDetails = false; - movie.fetchingSubtitles = false; - if (movie.polochon_url === "") { - allMoviesInPolochon = false; - } - movies = movies.set(movie.imdb_id, fromJS(movie)); - }); +const formatMovie = (movie) => { + movie.fetchingDetails = false; + movie.fetchingSubtitles = false; + movie.torrents = formatTorrents(movie.torrents); + return movie; +}; - // Select the first movie if the list is not empty - let selectedImdbId = ""; - if (movies.size > 0) { - // Sort by year - movies = movies.sort((a, b) => { - if (!allMoviesInPolochon) { - return b.get("year") - a.get("year"); - } +const formatMovies = (movies = []) => { + let allMoviesInPolochon = true; + movies.map((movie) => { + formatMovie(movie); + if (movie.polochon_url === "") { + allMoviesInPolochon = false; + } + }); - const dateA = new Date(a.get("date_added")); - const dateB = new Date(b.get("date_added")); - return dateA > dateB ? -1 : dateA < dateB ? 1 : 0; - }); - - selectedImdbId = movies.first().get("imdb_id"); + movies.sort((a, b) => { + if (!allMoviesInPolochon) { + return b.year - a.year; } - return state.delete("movies").merge( - Map({ - movies: movies, - filter: "", - loading: false, - selectedImdbId: selectedImdbId, - }) - ); - }, - MOVIE_GET_DETAILS_PENDING: (state, action) => - state.setIn( - ["movies", action.payload.main.imdbId, "fetchingDetails"], - true - ), - MOVIE_GET_DETAILS_FULFILLED: (state, action) => - state - .setIn( - ["movies", action.payload.response.data.imdb_id], - fromJS(action.payload.response.data) - ) - .setIn( - ["movies", action.payload.response.data.imdb_id, "fetchingDetails"], - false - ) - .setIn( - ["movies", action.payload.response.data.imdb_id, "fetchingSubtitles"], - false - ), - MOVIE_UPDATE_STORE_WISHLIST: (state, action) => - state.setIn( - ["movies", action.payload.imdbId, "wishlisted"], - action.payload.wishlisted - ), - MOVIE_GET_EXPLORE_OPTIONS_FULFILLED: (state, action) => - state.set("exploreOptions", fromJS(action.payload.response.data)), - UPDATE_LAST_MOVIE_FETCH_URL: (state, action) => - state.set("lastFetchUrl", action.payload.url), - MOVIE_SUBTITLES_UPDATE_PENDING: (state, action) => - state.setIn( - ["movies", action.payload.main.imdbId, "fetchingSubtitles"], - true - ), - MOVIE_SUBTITLES_UPDATE_FULFILLED: (state, action) => - state - .setIn(["movies", action.payload.main.imdbId, "fetchingSubtitles"], false) - .setIn( - ["movies", action.payload.main.imdbId, "subtitles"], - fromJS(action.payload.response.data) - ), - SELECT_MOVIE: (state, action) => - state.set("selectedImdbId", action.payload.imdbId), - MOVIE_UPDATE_FILTER: (state, action) => - state.set("filter", action.payload.filter), + const dateA = new Date(a.date_added); + const dateB = new Date(b.date_added); + return dateA > dateB ? -1 : dateA < dateB ? 1 : 0; + }); + + let m = new Map(); + movies.forEach((movie) => { + m.set(movie.imdb_id, movie); + }); + + return m; }; export default (state = defaultState, action) => - handlers[action.type] ? handlers[action.type](state, action) : state; + produce(state, (draft) => { + switch (action.type) { + case "MOVIE_LIST_FETCH_PENDING": + draft.loading = true; + break; + + case "MOVIE_LIST_FETCH_ERROR": + draft.loading = false; + break; + + case "MOVIE_LIST_FETCH_FULFILLED": + draft.loading = false; + draft.movies = formatMovies(action.payload.response.data); + if (draft.movies.size > 0) { + draft.selectedImdbId = draft.movies.keys().next().value; + } + break; + + case "MOVIE_GET_DETAILS_PENDING": + draft.movies.get(action.payload.main.imdbId).fetchingDetails = true; + break; + + case "MOVIE_GET_DETAILS_FULFILLED": + draft.movies.set( + action.payload.response.data.imdb_id, + formatMovie(action.payload.response.data) + ); + break; + + case "MOVIE_UPDATE_STORE_WISHLIST": + draft.movies.get(action.payload.imdbId).wishlisted = + action.payload.wishlisted; + break; + + case "MOVIE_GET_EXPLORE_OPTIONS_FULFILLED": + draft.exploreOptions = action.payload.response.data; + break; + + case "UPDATE_LAST_MOVIE_FETCH_URL": + draft.lastFetchUrl = action.payload.url; + break; + + case "MOVIE_SUBTITLES_UPDATE_PENDING": + draft.movies.get(action.payload.main.imdbId).fetchingSubtitles = true; + break; + + case "MOVIE_SUBTITLES_UPDATE_FULFILLED": + draft.movies.get(action.payload.main.imdbId).fetchingSubtitles = false; + draft.movies.get(action.payload.main.imdbId).subtitles = + action.payload.response.data; + break; + + case "SELECT_MOVIE": + draft.selectedImdbId = action.payload.imdbId; + break; + + default: + return draft; + } + }); diff --git a/frontend/js/reducers/notifications.js b/frontend/js/reducers/notifications.js index bd418bd..f0809e7 100644 --- a/frontend/js/reducers/notifications.js +++ b/frontend/js/reducers/notifications.js @@ -1,18 +1,24 @@ -import { List, fromJS } from "immutable"; +import { produce } from "immer"; -const defaultState = List(); - -const handlers = { - ADD_NOTIFICATION: (state, action) => - state.push( - fromJS({ - id: Math.random().toString(36).substring(7), - ...action.payload, - }) - ), - REMOVE_NOTIFICATION: (state, action) => - state.filter((e) => e.get("id") !== action.payload.id), -}; +const defaultState = new Map(); export default (state = defaultState, action) => - handlers[action.type] ? handlers[action.type](state, action) : state; + produce(state, (draft) => { + switch (action.type) { + case "ADD_NOTIFICATION": { + let id = Math.random().toString(36).substring(7); + draft.set(id, { + id: id, + ...action.payload, + }); + break; + } + + case "REMOVE_NOTIFICATION": + draft.delete(action.payload.id); + break; + + default: + return draft; + } + }); diff --git a/frontend/js/reducers/polochon.js b/frontend/js/reducers/polochon.js index df585b1..682440b 100644 --- a/frontend/js/reducers/polochon.js +++ b/frontend/js/reducers/polochon.js @@ -1,30 +1,34 @@ -import { List, Map, fromJS } from "immutable"; +import { produce } from "immer"; -const defaultState = Map({ +const defaultState = { loadingPublic: false, loadingManaged: false, - public: List(), - managed: List(), -}); - -const handlers = { - PUBLIC_POLOCHON_LIST_FETCH_PENDING: (state) => - state.set("loadingPublic", true), - PUBLIC_POLOCHON_LIST_FETCH_FULFILLED: (state, action) => { - return state.merge({ - loadingPublic: false, - public: List(fromJS(action.payload.response.data)), - }); - }, - MANAGED_POLOCHON_LIST_FETCH_PENDING: (state) => - state.set("loadingManaged", true), - MANAGED_POLOCHON_LIST_FETCH_FULFILLED: (state, action) => { - return state.merge({ - loadingManaged: false, - managed: List(fromJS(action.payload.response.data)), - }); - }, + public: [], + managed: [], }; export default (state = defaultState, action) => - handlers[action.type] ? handlers[action.type](state, action) : state; + produce(state, (draft) => { + switch (action.type) { + case "PUBLIC_POLOCHON_LIST_FETCH_PENDING": + draft.loadingPublic = true; + break; + + case "PUBLIC_POLOCHON_LIST_FETCH_FULFILLED": + draft.loadingPublic = false; + draft.public = action.payload.response.data; + break; + + case "MANAGED_POLOCHON_LIST_FETCH_PENDING": + draft.loadingManaged = true; + break; + + case "MANAGED_POLOCHON_LIST_FETCH_FULFILLED": + draft.loadingManaged = false; + draft.managed = action.payload.response.data; + break; + + default: + return draft; + } + }); diff --git a/frontend/js/reducers/show.js b/frontend/js/reducers/show.js index 065bfd7..9aefe7e 100644 --- a/frontend/js/reducers/show.js +++ b/frontend/js/reducers/show.js @@ -1,117 +1,109 @@ -import { OrderedMap, Map, fromJS } from "immutable"; +import { produce } from "immer"; +import { formatTorrents } from "../utils"; -const defaultState = Map({ +const defaultState = { loading: false, - show: Map({ - seasons: OrderedMap(), - }), -}); - -const handlers = { - SHOW_FETCH_DETAILS_PENDING: (state) => state.set("loading", true), - SHOW_FETCH_DETAILS_FULFILLED: (state, action) => - sortEpisodes(state, action.payload.response.data), - SHOW_UPDATE_STORE_WISHLIST: (state, action) => { - return state.mergeDeep( - fromJS({ - show: { - 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) => { - let data = action.payload.response.data; - if (!data) { - return state; - } - data.fetching = false; - return state.setIn( - ["show", "seasons", data.season, data.episode], - fromJS(data) - ); - }, - EPISODE_SUBTITLES_UPDATE_PENDING: (state, action) => - state.setIn( - [ - "show", - "seasons", - action.payload.main.season, - action.payload.main.episode, - "fetchingSubtitles", - ], - true - ), - EPISODE_SUBTITLES_UPDATE_FULFILLED: (state, action) => - state - .setIn( - [ - "show", - "seasons", - action.payload.main.season, - action.payload.main.episode, - "subtitles", - ], - fromJS(action.payload.response.data) - ) - .setIn( - [ - "show", - "seasons", - action.payload.main.season, - action.payload.main.episode, - "fetchingSubtitles", - ], - false - ), + show: {}, }; -const sortEpisodes = (state, show) => { - let episodes = show.episodes; - delete show["episodes"]; +const formatEpisode = (episode) => { + // Format the episode's torrents + episode.torrents = formatTorrents(episode.torrents); - let ret = state.set("loading", false); - if (episodes.length == 0) { - return ret; - } - - // Set the show data - ret = ret.set("show", fromJS(show)); - - // Set the show episodes - for (let ep of episodes) { - ep.fetching = false; - ret = ret.setIn(["show", "seasons", ep.season, ep.episode], fromJS(ep)); - } - - // Sort the episodes - ret = ret.updateIn(["show", "seasons"], function (seasons) { - return seasons.map(function (episodes) { - return episodes.sort((a, b) => a.get("episode") - b.get("episode")); - }); - }); - - // Sort the seasons - ret = ret.updateIn(["show", "seasons"], function (seasons) { - return seasons.sort(function (a, b) { - return a.first().get("season") - b.first().get("season"); - }); - }); - - return ret; + // Set the default fetching data + episode.fetching = false; + episode.fetchingSubtitles = false; }; export default (state = defaultState, action) => - handlers[action.type] ? handlers[action.type](state, action) : state; + produce(state, (draft) => { + switch (action.type) { + case "SHOW_FETCH_DETAILS_PENDING": + draft.loading = true; + break; + + case "SHOW_FETCH_DETAILS_FULFILLED": { + let show = action.payload.response.data; + let episodes = show.episodes; + delete show.episodes; + draft.show = show; + + let seasons = {}; + let seasonNumbers = []; + if (!episodes) { + episodes = []; + } + episodes.forEach((episode) => { + // Skip special episodes + if (episode.season === 0) { + return; + } + + // Format the episode from polochon + formatEpisode(episode); + + if (!seasons[episode.season]) { + seasons[episode.season] = []; + seasonNumbers.push(episode.season); + } + seasons[episode.season].push(episode); + }); + + let seasonMap = new Map(); + seasonNumbers.sort().forEach((n) => { + let episodes = []; + seasons[n] + .sort((a, b) => a.episode - b.episode) + .forEach((episode) => { + episodes.push([episode.episode, episode]); + }); + seasonMap.set(n, new Map(episodes)); + }); + draft.show.seasons = seasonMap; + + draft.loading = false; + break; + } + + case "SHOW_UPDATE_STORE_WISHLIST": + // TODO: check with we give the imdb in the payload + draft.show.tracked_season = action.payload.season; // eslint-disable-line camelcase + draft.show.tracked_episode = action.payload.episode; // eslint-disable-line camelcase + break; + + case "EPISODE_GET_DETAILS_PENDING": + draft.show.seasons + .get(action.payload.main.season) + .get(action.payload.main.episode).fetching = true; + break; + + case "EPISODE_GET_DETAILS_FULFILLED": { + let episode = action.payload.response.data; + if (!episode) { + return draft; + } + formatEpisode(episode); + draft.show.get(episode.season).set(episode.episode, episode); + break; + } + + case "EPISODE_SUBTITLES_UPDATE_PENDING": + draft.show.seasons + .get(action.payload.main.season) + .get(action.payload.main.episode).fetchingSubtitles = true; + break; + + case "EPISODE_SUBTITLES_UPDATE_FULFILLED": { + draft.show.seasons + .get(action.payload.main.season) + .get(action.payload.main.episode).subtitles = + action.payload.response.data; + draft.show.seasons + .get(action.payload.main.season) + .get(action.payload.main.episode).fetchingSubtitles = false; + break; + } + default: + return draft; + } + }); diff --git a/frontend/js/reducers/shows.js b/frontend/js/reducers/shows.js index 237d601..5408027 100644 --- a/frontend/js/reducers/shows.js +++ b/frontend/js/reducers/shows.js @@ -1,77 +1,83 @@ -import { OrderedMap, Map, fromJS } from "immutable"; +import { produce } from "immer"; -const defaultState = Map({ +const defaultState = { loading: false, - shows: OrderedMap(), - filter: "", + shows: new Map(), selectedImdbId: "", lastFetchUrl: "", - exploreOptions: Map(), -}); + exploreOptions: {}, +}; -const handlers = { - SHOW_LIST_FETCH_PENDING: (state) => state.set("loading", true), - SHOW_LIST_FETCH_FULFILLED: (state, action) => { - let shows = Map(); - action.payload.response.data.map(function (show) { - show.fetchingDetails = false; - show.fetchingSubtitles = false; - shows = shows.set(show.imdb_id, fromJS(show)); - }); +const formatShow = (show) => { + show.fetchingDetails = false; + show.fetchingSubtitles = false; + show.episodes = undefined; + return show; +}; - let selectedImdbId = ""; - if (shows.size > 0) { - // Sort by year - shows = shows.sort((a, b) => b.get("year") - a.get("year")); - selectedImdbId = shows.first().get("imdb_id"); - } +const formatShows = (shows = []) => { + // Add defaults + shows.map((show) => formatShow(show)); - return state.delete("shows").merge( - Map({ - shows: shows, - filter: "", - loading: false, - selectedImdbId: selectedImdbId, - }) - ); - }, - SHOW_GET_DETAILS_PENDING: (state, action) => - state.setIn(["shows", action.payload.main.imdbId, "fetchingDetails"], true), - SHOW_GET_DETAILS_FULFILLED: (state, action) => { - let show = action.payload.response.data; - show.fetchingDetails = false; - show.fetchingSubtitles = false; - return state.setIn(["shows", show.imdb_id], fromJS(show)); - }, - SHOW_GET_EXPLORE_OPTIONS_FULFILLED: (state, action) => - state.set("exploreOptions", fromJS(action.payload.response.data)), - SHOW_UPDATE_STORE_WISHLIST: (state, action) => { - let season = action.payload.season; - let episode = action.payload.episode; - if (action.payload.wishlisted) { - if (season === null) { - season = 0; - } - if (episode === null) { - episode = 0; - } - } + // Sort by year + shows.sort((a, b) => b.year - a.year); - return state.mergeIn( - ["shows", action.payload.imdbId], - Map({ - tracked_season: season, // eslint-disable-line camelcase - tracked_episode: episode, // eslint-disable-line camelcase - }) - ); - }, - UPDATE_LAST_SHOWS_FETCH_URL: (state, action) => - state.set("lastFetchUrl", action.payload.url), - SELECT_SHOW: (state, action) => - state.set("selectedImdbId", action.payload.imdbId), - SHOWS_UPDATE_FILTER: (state, action) => - state.set("filter", action.payload.filter), + let s = new Map(); + shows.forEach((show) => { + s.set(show.imdb_id, show); + }); + + return s; }; export default (state = defaultState, action) => - handlers[action.type] ? handlers[action.type](state, action) : state; + produce(state, (draft) => { + switch (action.type) { + case "SHOW_LIST_FETCH_PENDING": + draft.loading = true; + break; + + case "SHOW_LIST_FETCH_FULFILLED": + draft.loading = false; + draft.shows = formatShows(action.payload.response.data); + if (draft.shows.size > 0) { + draft.selectedImdbId = draft.shows.keys().next().value; + } + break; + + case "SHOW_GET_DETAILS_PENDING": + draft.shows.get(action.payload.main.imdbId).fetchingDetails = true; + break; + + case "SHOW_GET_DETAILS_FULFILLED": + draft.shows.set( + action.payload.response.data.imdb_id, + formatShow(action.payload.response.data) + ); + break; + + case "SHOW_GET_EXPLORE_OPTIONS_FULFILLED": + draft.exploreOptions = action.payload.response.data; + break; + + case "SHOW_UPDATE_STORE_WISHLIST": + // eslint-disable-next-line camelcase + draft.shows.get(action.payload.imdbId).tracked_season = + action.payload.season; + // eslint-disable-next-line camelcase + draft.shows.get(action.payload.imdbId).tracked_episode = + action.payload.episode; // eslint-disable-line camelcase + break; + + case "UPDATE_LAST_SHOWS_FETCH_URL": + draft.lastFetchUrl = action.payload.url; + break; + + case "SELECT_SHOW": + draft.selectedImdbId = action.payload.imdbId; + break; + + default: + return draft; + } + }); diff --git a/frontend/js/reducers/torrents.js b/frontend/js/reducers/torrents.js index f8320f2..a4c834f 100644 --- a/frontend/js/reducers/torrents.js +++ b/frontend/js/reducers/torrents.js @@ -1,30 +1,34 @@ -import { Map, List, fromJS } from "immutable"; +import { produce } from "immer"; -const defaultState = Map({ +const defaultState = { fetching: false, searching: false, - torrents: List(), - searchResults: List(), -}); - -const handlers = { - TORRENTS_FETCH_PENDING: (state) => state.set("fetching", true), - TORRENTS_FETCH_FULFILLED: (state, action) => - state.merge( - fromJS({ - 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, - searchResults: action.payload.response.data, - }) - ), + torrents: [], + searchResults: [], }; export default (state = defaultState, action) => - handlers[action.type] ? handlers[action.type](state, action) : state; + produce(state, (draft) => { + switch (action.type) { + case "TORRENTS_FETCH_PENDING": + draft.fetching = true; + break; + + case "TORRENTS_FETCH_FULFILLED": + draft.fetching = false; + draft.torrents = action.payload.response.data; + break; + + case "TORRENTS_SEARCH_PENDING": + draft.searching = true; + break; + + case "TORRENTS_SEARCH_FULFILLED": + draft.searching = false; + draft.searchResults = action.payload.response.data; + break; + + default: + return draft; + } + }); diff --git a/frontend/js/reducers/users.js b/frontend/js/reducers/users.js index e644ce9..79a4674 100644 --- a/frontend/js/reducers/users.js +++ b/frontend/js/reducers/users.js @@ -1,9 +1,9 @@ -import { Map, List, fromJS } from "immutable"; +import { produce } from "immer"; import jwtDecode from "jwt-decode"; import Cookies from "universal-cookie"; -const defaultState = Map({ +const defaultState = { error: "", loading: false, username: "", @@ -16,98 +16,102 @@ const defaultState = Map({ polochonName: "", polochonId: "", polochonActivated: false, - tokens: List(), - modules: Map(), + tokens: [], + modules: {}, modulesLoading: false, -}); - -let initialState = defaultState; -const token = localStorage.getItem("token"); -if (token && token !== "") { - initialState = updateFromToken(initialState, token); -} - -const handlers = { - USER_LOGIN_PENDING: (state) => state.set("loading", true), - USER_LOGIN_ERROR: (state, action) => { - state.set("loading", false); - return logoutUser(action.payload.response.message); - }, - USER_LOGIN_FULFILLED: (state, action) => { - return updateFromToken(state, action.payload.response.data.token); - }, - USER_SIGNUP_PENDING: (state) => state.set("loading", true), - USER_SIGNUP_ERROR: (state, action) => - state.merge( - Map({ - 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(), - GET_USER_PENDING: (state) => state.set("loading", true), - GET_USER_FULFILLED: (state, action) => - state.merge( - Map({ - polochonToken: action.payload.response.data.token, - polochonUrl: action.payload.response.data.url, - polochonName: action.payload.response.data.name, - polochonId: action.payload.response.data.id, - polochonActivated: action.payload.response.data.activated, - loading: false, - }) - ), - GET_USER_TOKENS_PENDING: (state) => state.set("loading", true), - 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) { +const logoutUser = () => { localStorage.removeItem("token"); const cookies = new Cookies(); cookies.remove("token"); - if (error !== "") { - return defaultState.set("error", error); - } else { - return defaultState; - } -} +}; -function updateFromToken(state, token) { +const updateFromToken = (draft, token) => { const decodedToken = jwtDecode(token); localStorage.setItem("token", token); const cookies = new Cookies(); cookies.set("token", token); - return state.merge( - Map({ - error: "", - isLogged: true, - isTokenSet: true, - isAdmin: decodedToken.isAdmin, - isActivated: decodedToken.isActivated, - username: decodedToken.username, - }) - ); + draft.error = ""; + draft.isLogged = true; + draft.isTokenSet = true; + draft.isAdmin = decodedToken.isAdmin; + draft.isActivated = decodedToken.isActivated; + draft.username = decodedToken.username; +}; + +// Initial state is the default state with the info from the current user +// token +let initialState = defaultState; +const token = localStorage.getItem("token"); +if (token && token !== "") { + updateFromToken(initialState, token); } export default (state = initialState, action) => - handlers[action.type] ? handlers[action.type](state, action) : state; + produce(state, (draft) => { + switch (action.type) { + case "USER_SIGNUP_PENDING": + case "USER_LOGIN_PENDING": + case "GET_USER_PENDING": + case "GET_USER_TOKENS_PENDING": + draft.loading = true; + break; + + case "USER_LOGIN_ERROR": + logoutUser(); + draft.isLogged = false; + draft.loading = false; + draft.error = action.payload.response.message; + break; + + case "USER_LOGIN_FULFILLED": + draft.loading = false; + updateFromToken(draft, action.payload.response.data.token); + break; + + case "USER_SIGNUP_ERROR": + draft.error = action.payload.response.message; + draft.loading = false; + break; + + case "USER_SIGNUP_FULFILLED": + draft.error = ""; + draft.loading = false; + break; + + case "USER_SET_TOKEN": + updateFromToken(draft, action.payload.token); + break; + + case "USER_LOGOUT": + logoutUser(); + draft.isLogged = false; + break; + + case "GET_USER_FULFILLED": + draft.polochonToken = action.payload.response.data.token; + draft.polochonUrl = action.payload.response.data.url; + draft.polochonName = action.payload.response.data.name; + draft.polochonId = action.payload.response.data.id; + draft.polochonActivated = action.payload.response.data.activated; + draft.loading = false; + break; + + case "GET_USER_TOKENS_FULFILLED": + draft.tokens = action.payload.response.data; + draft.loading = false; + break; + + case "GET_USER_MODULES_PENDING": + draft.modulesLoading = true; + break; + + case "GET_USER_MODULES_FULFILLED": + draft.modules = action.payload.response.data; + draft.modulesLoading = false; + break; + } + }); diff --git a/frontend/js/utils.js b/frontend/js/utils.js index 9cf9a01..836ccbd 100644 --- a/frontend/js/utils.js +++ b/frontend/js/utils.js @@ -21,43 +21,6 @@ const pad = (d) => (d < 10 ? "0" + d.toString() : d.toString()); export const prettyEpisodeName = (showName, season, episode) => `${showName} S${pad(season)}E${pad(episode)}`; -export const inLibrary = (element) => element.get("polochon_url", "") !== ""; - -export const isWishlisted = (element) => { - const wishlisted = element.get("wishlisted", undefined); - if (wishlisted !== undefined) { - return wishlisted; - } - - const trackedSeason = element.get("tracked_season", null); - const trackedEpisode = element.get("tracked_episode", null); - if (trackedSeason !== null && trackedEpisode !== null) { - return true; - } - - return false; -}; - -export const isEpisodeWishlisted = (element, trackedSeason, trackedEpisode) => { - if (trackedSeason === null && trackedEpisode === null) { - return false; - } - - if (trackedSeason === 0 && trackedEpisode === 0) { - return true; - } - - const season = element.get("season", 0); - const episode = element.get("episode", 0); - if (season < trackedSeason) { - return false; - } else if (season > trackedSeason) { - return true; - } else { - return episode >= trackedEpisode; - } -}; - export const prettySize = (fileSizeInBytes) => { var i = -1; var byteUnits = [" kB", " MB", " GB", " TB", "PB", "EB", "ZB", "YB"]; @@ -68,3 +31,19 @@ export const prettySize = (fileSizeInBytes) => { return Math.max(fileSizeInBytes, 0.1).toFixed(1) + byteUnits[i]; }; + +export const formatTorrents = (torrents = []) => { + if (!torrents || torrents.length == 0) { + return undefined; + } + + let torrentMap = new Map(); + torrents.forEach((torrent) => { + if (!torrentMap.has(torrent.source)) { + torrentMap.set(torrent.source, new Map()); + } + torrentMap.get(torrent.source).set(torrent.quality, torrent); + }); + + return torrentMap; +}; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 62bf184..2ac5e77 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -5934,10 +5934,10 @@ "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true }, - "immutable": { - "version": "4.0.0-rc.12", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0-rc.12.tgz", - "integrity": "sha512-0M2XxkZLx/mi3t8NVwIm1g8nHoEmM9p9UBl/G9k4+hm0kBgOVdMV/B3CY5dQ8qG8qc80NN4gDV4HQv6FTJ5q7A==" + "immer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-6.0.3.tgz", + "integrity": "sha512-12VvNrfSrXZdm/BJgi/KDW2soq5freVSf3I1+4CLunUM8mAGx2/0Njy0xBVzi5zewQZiwM7z1/1T+8VaI7NkmQ==" }, "import-cwd": { "version": "2.1.0", diff --git a/frontend/package.json b/frontend/package.json index 3041a79..d5de2d7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,7 +11,7 @@ "font-awesome": "^4.7.0", "fuzzy": "^0.1.3", "history": "^4.9.0", - "immutable": "^4.0.0-rc.12", + "immer": "^6.0.3", "jquery": "^3.4.1", "jwt-decode": "^2.1.0", "moment": "^2.20.1", -- 2.47.1