Update redux state management #21
@ -20,6 +20,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
|
"es6": true,
|
||||||
"browser": true,
|
"browser": true,
|
||||||
"mocha": true,
|
"mocha": true,
|
||||||
"node": true
|
"node": true
|
||||||
|
@ -106,15 +106,6 @@ export function selectShow(imdbId) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateFilter(filter) {
|
|
||||||
return {
|
|
||||||
type: "SHOWS_UPDATE_FILTER",
|
|
||||||
payload: {
|
|
||||||
filter,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateLastShowsFetchUrl(url) {
|
export function updateLastShowsFetchUrl(url) {
|
||||||
return {
|
return {
|
||||||
type: "UPDATE_LAST_SHOWS_FETCH_URL",
|
type: "UPDATE_LAST_SHOWS_FETCH_URL",
|
||||||
|
@ -26,25 +26,25 @@ import store, { history } from "./store";
|
|||||||
// Components
|
// Components
|
||||||
import { AdminPanel } from "./components/admins/panel";
|
import { AdminPanel } from "./components/admins/panel";
|
||||||
import { Notifications } from "./components/notifications/notifications";
|
import { Notifications } from "./components/notifications/notifications";
|
||||||
import Alert from "./components/alerts/alert";
|
import { Alert } from "./components/alerts/alert";
|
||||||
import MovieList from "./components/movies/list";
|
import MovieList from "./components/movies/list";
|
||||||
import NavBar from "./components/navbar";
|
import { AppNavBar } from "./components/navbar";
|
||||||
import WsHandler from "./components/websocket";
|
import { WsHandler } from "./components/websocket";
|
||||||
import { ShowDetails } from "./components/shows/details";
|
import { ShowDetails } from "./components/shows/details";
|
||||||
import ShowList from "./components/shows/list";
|
import ShowList from "./components/shows/list";
|
||||||
import TorrentList from "./components/torrents/list";
|
import { TorrentList } from "./components/torrents/list";
|
||||||
import TorrentSearch from "./components/torrents/search";
|
import { TorrentSearch } from "./components/torrents/search";
|
||||||
import UserActivation from "./components/users/activation";
|
import { UserActivation } from "./components/users/activation";
|
||||||
import UserLoginForm from "./components/users/login";
|
import { UserLoginForm } from "./components/users/login";
|
||||||
import UserLogout from "./components/users/logout";
|
import { UserLogout } from "./components/users/logout";
|
||||||
import UserProfile from "./components/users/profile";
|
import { UserProfile } from "./components/users/profile";
|
||||||
import UserSignUp from "./components/users/signup";
|
import { UserSignUp } from "./components/users/signup";
|
||||||
import UserTokens from "./components/users/tokens";
|
import { UserTokens } from "./components/users/tokens";
|
||||||
|
|
||||||
const App = () => (
|
const App = () => (
|
||||||
<div>
|
<div>
|
||||||
<WsHandler />
|
<WsHandler />
|
||||||
<NavBar />
|
<AppNavBar />
|
||||||
<Alert />
|
<Alert />
|
||||||
<Notifications />
|
<Notifications />
|
||||||
<Container fluid>
|
<Container fluid>
|
||||||
|
@ -8,11 +8,9 @@ import { setUserToken } from "./actions/users";
|
|||||||
export const ProtectedRoute = ({ component: Component, ...otherProps }) => {
|
export const ProtectedRoute = ({ component: Component, ...otherProps }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const isLogged = useSelector((state) => state.userStore.get("isLogged"));
|
const isLogged = useSelector((state) => state.user.isLogged);
|
||||||
const isActivated = useSelector((state) =>
|
const isActivated = useSelector((state) => state.user.isActivated);
|
||||||
state.userStore.get("isActivated")
|
const isTokenSet = useSelector((state) => state.user.isTokenSet);
|
||||||
);
|
|
||||||
const isTokenSet = useSelector((state) => state.userStore.get("isTokenSet"));
|
|
||||||
|
|
||||||
const isAuthenticated = () => {
|
const isAuthenticated = () => {
|
||||||
if (isTokenSet) {
|
if (isTokenSet) {
|
||||||
@ -52,7 +50,7 @@ ProtectedRoute.propTypes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const AdminRoute = ({ component: Component, ...otherProps }) => {
|
export const AdminRoute = ({ component: Component, ...otherProps }) => {
|
||||||
const isAdmin = useSelector((state) => state.userStore.get("isAdmin"));
|
const isAdmin = useSelector((state) => state.user.isAdmin);
|
||||||
return (
|
return (
|
||||||
<Route
|
<Route
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
|
@ -7,10 +7,8 @@ import Modules from "../modules/modules";
|
|||||||
export const AdminModules = () => {
|
export const AdminModules = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const loading = useSelector((state) =>
|
const loading = useSelector((state) => state.admin.fetchingModules);
|
||||||
state.adminStore.get("fetchingModules")
|
const modules = useSelector((state) => state.admin.modules);
|
||||||
);
|
|
||||||
const modules = useSelector((state) => state.adminStore.get("modules"));
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(getAdminModules());
|
dispatch(getAdminModules());
|
||||||
|
@ -25,8 +25,14 @@ export const Stat = ({ name, count, torrentCount, torrentCountById }) => (
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
Stat.propTypes = {
|
Stat.propTypes = {
|
||||||
name: PropTypes.string,
|
name: PropTypes.string.isRequired,
|
||||||
count: PropTypes.number,
|
count: PropTypes.number.isRequired,
|
||||||
torrentCount: PropTypes.number,
|
torrentCount: PropTypes.number.isRequired,
|
||||||
torrentCountById: PropTypes.number,
|
torrentCountById: PropTypes.number.isRequired,
|
||||||
|
};
|
||||||
|
Stat.defaultProps = {
|
||||||
|
name: "",
|
||||||
|
count: 0,
|
||||||
|
torrentCount: 0,
|
||||||
|
torrentCountById: 0,
|
||||||
};
|
};
|
||||||
|
@ -7,7 +7,7 @@ import { getStats } from "../../actions/admins";
|
|||||||
|
|
||||||
export const Stats = () => {
|
export const Stats = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const stats = useSelector((state) => state.adminStore.get("stats"));
|
const stats = useSelector((state) => state.admin.stats);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(getStats());
|
dispatch(getStats());
|
||||||
@ -17,21 +17,21 @@ export const Stats = () => {
|
|||||||
<div className="row d-flex flex-wrap">
|
<div className="row d-flex flex-wrap">
|
||||||
<Stat
|
<Stat
|
||||||
name="Movies"
|
name="Movies"
|
||||||
count={stats.get("movies_count")}
|
count={stats["movies_count"]}
|
||||||
torrentCount={stats.get("movies_torrents_count")}
|
torrentCount={stats["movies_torrents_count"]}
|
||||||
torrentCountById={stats.get("movies_torrents_count_by_id")}
|
torrentCountById={stats["movies_torrents_count_by_id"]}
|
||||||
/>
|
/>
|
||||||
<Stat
|
<Stat
|
||||||
name="Shows"
|
name="Shows"
|
||||||
count={stats.get("shows_count")}
|
count={stats["shows_count"]}
|
||||||
torrentCount={stats.get("episodes_torrents_count")}
|
torrentCount={stats["episodes_torrents_count"]}
|
||||||
torrentCountById={stats.get("shows_torrents_count_by_id")}
|
torrentCountById={stats["shows_torrents_count_by_id"]}
|
||||||
/>
|
/>
|
||||||
<Stat
|
<Stat
|
||||||
name="Episodes"
|
name="Episodes"
|
||||||
count={stats.get("episodes_count")}
|
count={stats["episodes_count"]}
|
||||||
torrentCount={stats.get("episodes_torrents_count")}
|
torrentCount={stats["episodes_torrents_count"]}
|
||||||
torrentCountById={stats.get("episodes_torrents_count_by_id")}
|
torrentCountById={stats["episodes_torrents_count_by_id"]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -15,7 +15,12 @@ export const TorrentsStat = ({ count, torrentCount, torrentCountById }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
TorrentsStat.propTypes = {
|
TorrentsStat.propTypes = {
|
||||||
count: PropTypes.number,
|
count: PropTypes.number.isRequired,
|
||||||
torrentCount: PropTypes.number,
|
torrentCount: PropTypes.number.isRequired,
|
||||||
torrentCountById: PropTypes.number,
|
torrentCountById: PropTypes.number.isRequired,
|
||||||
|
};
|
||||||
|
TorrentsStat.defaultProps = {
|
||||||
|
count: 1,
|
||||||
|
torrentCount: 0,
|
||||||
|
torrentCountById: 0,
|
||||||
};
|
};
|
||||||
|
@ -1,65 +1,43 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
import { UserEdit } from "./userEdit";
|
import { UserEdit } from "./userEdit";
|
||||||
|
|
||||||
export const User = ({
|
export const User = ({ id }) => {
|
||||||
id,
|
const user = useSelector((state) => state.admin.users.get(id));
|
||||||
admin,
|
const polochon = user.polochon;
|
||||||
activated,
|
|
||||||
name,
|
|
||||||
polochonActivated,
|
|
||||||
polochonUrl,
|
|
||||||
polochonName,
|
|
||||||
polochonId,
|
|
||||||
token,
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
<td>{id}</td>
|
<td>{user.id}</td>
|
||||||
<td>{name}</td>
|
<td>{user.name}</td>
|
||||||
<td>
|
<td>
|
||||||
<span
|
<span
|
||||||
className={activated ? "fa fa-check" : "fa fa-times text-danger"}
|
className={user.activated ? "fa fa-check" : "fa fa-times text-danger"}
|
||||||
></span>
|
></span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span className={admin ? "fa fa-check" : "fa fa-times"}></span>
|
<span className={user.admin ? "fa fa-check" : "fa fa-times"}></span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{polochonName !== "" ? polochonName : "-"}
|
{polochon ? polochon.name : "-"}
|
||||||
{polochonUrl !== "" && <small className="ml-1">({polochonUrl})</small>}
|
{polochon && <small className="ml-1">({polochon.url})</small>}
|
||||||
</td>
|
</td>
|
||||||
<td>{token}</td>
|
<td>{user.token}</td>
|
||||||
<td>
|
<td>
|
||||||
<span
|
<span
|
||||||
className={
|
className={
|
||||||
polochonActivated ? "fa fa-check" : "fa fa-times text-danger"
|
user.polochon_activated ? "fa fa-check" : "fa fa-times text-danger"
|
||||||
}
|
}
|
||||||
></span>
|
></span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<UserEdit
|
<UserEdit id={id} />
|
||||||
id={id}
|
|
||||||
name={name}
|
|
||||||
admin={admin}
|
|
||||||
activated={activated}
|
|
||||||
polochonActivated={polochonActivated}
|
|
||||||
polochonToken={token}
|
|
||||||
polochonId={polochonId}
|
|
||||||
/>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
User.propTypes = {
|
User.propTypes = {
|
||||||
id: PropTypes.string,
|
id: PropTypes.string.isRequired,
|
||||||
name: PropTypes.string,
|
|
||||||
polochonId: PropTypes.string,
|
|
||||||
polochonUrl: PropTypes.string,
|
|
||||||
polochonName: PropTypes.string,
|
|
||||||
token: PropTypes.string,
|
|
||||||
admin: PropTypes.bool,
|
|
||||||
activated: PropTypes.bool,
|
|
||||||
polochonActivated: PropTypes.bool,
|
|
||||||
};
|
};
|
||||||
|
@ -10,25 +10,20 @@ import { PolochonSelect } from "../polochons/select";
|
|||||||
import { FormModal } from "../forms/modal";
|
import { FormModal } from "../forms/modal";
|
||||||
import { FormInput } from "../forms/input";
|
import { FormInput } from "../forms/input";
|
||||||
|
|
||||||
export const UserEdit = ({
|
export const UserEdit = ({ id }) => {
|
||||||
id,
|
|
||||||
name,
|
|
||||||
admin: initAdmin,
|
|
||||||
activated: initActivated,
|
|
||||||
polochonToken,
|
|
||||||
polochonId: initPolochonId,
|
|
||||||
polochonActivated: initPolochonActivated,
|
|
||||||
}) => {
|
|
||||||
const dispatch = useDispatch();
|
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 [modal, setModal] = useState(false);
|
||||||
const [admin, setAdmin] = useState(initAdmin);
|
const [admin, setAdmin] = useState(user.admin);
|
||||||
const [activated, setActivated] = useState(initActivated);
|
const [activated, setActivated] = useState(user.activated);
|
||||||
const [token, setToken] = useState(polochonToken);
|
const [token, setToken] = useState(user.token);
|
||||||
const [polochonId, setPolochonId] = useState(initPolochonId);
|
const [polochonId, setPolochonId] = useState(
|
||||||
|
polochon ? polochon.id : undefined
|
||||||
|
);
|
||||||
const [polochonActivated, setPolochonActivated] = useState(
|
const [polochonActivated, setPolochonActivated] = useState(
|
||||||
initPolochonActivated
|
user.polochon_activated
|
||||||
);
|
);
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [confirmDelete, setConfirmDelete] = useState(false);
|
const [confirmDelete, setConfirmDelete] = useState(false);
|
||||||
@ -106,11 +101,7 @@ export const UserEdit = ({
|
|||||||
updateValue={setPassword}
|
updateValue={setPassword}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PolochonSelect
|
<PolochonSelect value={polochonId} changeValue={setPolochonId} />
|
||||||
value={polochonId}
|
|
||||||
changeValue={setPolochonId}
|
|
||||||
polochonList={publicPolochons}
|
|
||||||
/>
|
|
||||||
<FormInput
|
<FormInput
|
||||||
label="Polochon Token"
|
label="Polochon Token"
|
||||||
value={token}
|
value={token}
|
||||||
@ -139,11 +130,5 @@ export const UserEdit = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
UserEdit.propTypes = {
|
UserEdit.propTypes = {
|
||||||
id: PropTypes.string,
|
id: PropTypes.string.isRequired,
|
||||||
name: PropTypes.string,
|
|
||||||
activated: PropTypes.bool,
|
|
||||||
admin: PropTypes.bool,
|
|
||||||
polochonToken: PropTypes.string,
|
|
||||||
polochonId: PropTypes.string,
|
|
||||||
polochonActivated: PropTypes.bool,
|
|
||||||
};
|
};
|
||||||
|
@ -8,13 +8,17 @@ import { getPolochons } from "../../actions/polochon";
|
|||||||
|
|
||||||
export const UserList = () => {
|
export const UserList = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const users = useSelector((state) => state.adminStore.get("users"));
|
const userIds = useSelector((state) => Array.from(state.admin.users.keys()));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(getUsers());
|
dispatch(getUsers());
|
||||||
dispatch(getPolochons());
|
dispatch(getPolochons());
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
|
if (userIds.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="table-responsive my-2">
|
<div className="table-responsive my-2">
|
||||||
<table className="table table-striped">
|
<table className="table table-striped">
|
||||||
@ -31,19 +35,8 @@ export const UserList = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{users.map((el, index) => (
|
{userIds.map((id) => (
|
||||||
<User
|
<User key={id} id={id} />
|
||||||
key={index}
|
|
||||||
id={el.get("id")}
|
|
||||||
name={el.get("name")}
|
|
||||||
admin={el.get("admin")}
|
|
||||||
activated={el.get("activated")}
|
|
||||||
polochonActivated={el.get("polochon_activated")}
|
|
||||||
token={el.get("token", "")}
|
|
||||||
polochonId={el.getIn(["polochon", "id"], "")}
|
|
||||||
polochonUrl={el.getIn(["polochon", "url"], "")}
|
|
||||||
polochonName={el.getIn(["polochon", "name"], "")}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@ -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) => (
|
|
||||||
<div className="table-responsive my-2">
|
|
||||||
<table className="table table-striped">
|
|
||||||
<thead className="table-secondary">
|
|
||||||
<tr>
|
|
||||||
<th>#</th>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Activated</th>
|
|
||||||
<th>Admin</th>
|
|
||||||
<th>Polochon URL</th>
|
|
||||||
<th>Polochon token</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{props.users.map((el, index) => (
|
|
||||||
<User key={index} data={el} updateUser={props.updateUser} />
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
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 (
|
|
||||||
<tr>
|
|
||||||
<td>{props.data.get("id")}</td>
|
|
||||||
<td>{props.data.get("Name")}</td>
|
|
||||||
<td>
|
|
||||||
<UserActivationStatus activated={props.data.get("Activated")} />
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<UserAdminStatus admin={props.data.get("Admin")} />
|
|
||||||
</td>
|
|
||||||
<td>{polochonUrl}</td>
|
|
||||||
<td>{polochonToken}</td>
|
|
||||||
<td>
|
|
||||||
<UserEdit
|
|
||||||
data={props.data}
|
|
||||||
updateUser={props.updateUser}
|
|
||||||
polochonUrl={polochonUrl}
|
|
||||||
polochonToken={polochonToken}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
User.propTypes = {
|
|
||||||
data: PropTypes.object,
|
|
||||||
updateUser: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
const UserAdminStatus = (props) => (
|
|
||||||
<span className={props.admin ? "fa fa-check" : "fa fa-times"}></span>
|
|
||||||
);
|
|
||||||
UserAdminStatus.propTypes = { admin: PropTypes.bool.isRequired };
|
|
||||||
|
|
||||||
const UserActivationStatus = (props) => (
|
|
||||||
<span
|
|
||||||
className={props.activated ? "fa fa-check" : "fa fa-times text-danger"}
|
|
||||||
></span>
|
|
||||||
);
|
|
||||||
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 (
|
|
||||||
<span>
|
|
||||||
<span
|
|
||||||
className="fa fa-pencil clickable"
|
|
||||||
onClick={() => setModal(true)}
|
|
||||||
></span>
|
|
||||||
<Modal show={modal} onHide={() => setModal(false)}>
|
|
||||||
<Modal.Header closeButton>
|
|
||||||
<Modal.Title>
|
|
||||||
<i className="fa fa-pencil"></i>
|
|
||||||
Edit user - {props.data.get("Name")}
|
|
||||||
</Modal.Title>
|
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Body bsPrefix="modal-body admin-edit-user-modal">
|
|
||||||
<form className="form-horizontal" onSubmit={(ev) => handleSubmit(ev)}>
|
|
||||||
<div className="form-group">
|
|
||||||
<label>Account status</label>
|
|
||||||
<Toggle
|
|
||||||
className="pull-right"
|
|
||||||
on="Activated"
|
|
||||||
off="Deactivated"
|
|
||||||
active={activated}
|
|
||||||
offstyle="danger"
|
|
||||||
handlestyle="secondary"
|
|
||||||
onClick={() => setActivated(!activated)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<label>Admin status</label>
|
|
||||||
<Toggle
|
|
||||||
className="pull-right"
|
|
||||||
on="Admin"
|
|
||||||
off="User"
|
|
||||||
active={admin}
|
|
||||||
offstyle="info"
|
|
||||||
handlestyle="secondary"
|
|
||||||
onClick={() => setAdmin(!admin)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<label className="control-label">Polochon URL</label>
|
|
||||||
<input
|
|
||||||
className="form-control"
|
|
||||||
value={url}
|
|
||||||
onChange={(e) => setUrl(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<label className="control-label">Polochon token</label>
|
|
||||||
<input
|
|
||||||
className="form-control"
|
|
||||||
value={token}
|
|
||||||
onChange={(e) => setToken(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Modal.Body>
|
|
||||||
<Modal.Footer>
|
|
||||||
<Button variant="success" onClick={handleSubmit}>
|
|
||||||
Apply
|
|
||||||
</Button>
|
|
||||||
<Button onClick={() => setModal(false)}>Close</Button>
|
|
||||||
</Modal.Footer>
|
|
||||||
</Modal>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
UserEdit.propTypes = {
|
|
||||||
data: PropTypes.object,
|
|
||||||
updateUser: PropTypes.func,
|
|
||||||
polochonUrl: PropTypes.string,
|
|
||||||
polochonToken: PropTypes.string,
|
|
||||||
};
|
|
@ -4,24 +4,20 @@ import { useSelector, useDispatch } from "react-redux";
|
|||||||
|
|
||||||
import { dismissAlert } from "../../actions/alerts";
|
import { dismissAlert } from "../../actions/alerts";
|
||||||
|
|
||||||
const Alert = () => {
|
export const Alert = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const show = useSelector((state) => state.alerts.get("show"));
|
const show = useSelector((state) => state.alerts.show);
|
||||||
const title = useSelector((state) => state.alerts.get("message"));
|
const title = useSelector((state) => state.alerts.message);
|
||||||
const type = useSelector((state) => state.alerts.get("type"));
|
const type = useSelector((state) => state.alerts.type);
|
||||||
|
|
||||||
|
const dismiss = () => {
|
||||||
|
dispatch(dismissAlert());
|
||||||
|
};
|
||||||
|
|
||||||
if (!show) {
|
if (!show) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <SweetAlert type={type} title={title} onConfirm={dismiss} />;
|
||||||
<SweetAlert
|
|
||||||
type={type}
|
|
||||||
title={title}
|
|
||||||
onConfirm={() => dispatch(dismissAlert())}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Alert;
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { List } from "immutable";
|
|
||||||
|
|
||||||
import Modal from "react-bootstrap/Modal";
|
import Modal from "react-bootstrap/Modal";
|
||||||
|
|
||||||
@ -19,7 +18,7 @@ export const DownloadAndStream = ({ url, name, subtitles }) => {
|
|||||||
DownloadAndStream.propTypes = {
|
DownloadAndStream.propTypes = {
|
||||||
url: PropTypes.string,
|
url: PropTypes.string,
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
subtitles: PropTypes.instanceOf(List),
|
subtitles: PropTypes.array,
|
||||||
};
|
};
|
||||||
|
|
||||||
const DownloadButton = ({ url }) => (
|
const DownloadButton = ({ url }) => (
|
||||||
@ -68,25 +67,23 @@ const StreamButton = ({ name, url, subtitles }) => {
|
|||||||
StreamButton.propTypes = {
|
StreamButton.propTypes = {
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
url: PropTypes.string.isRequired,
|
url: PropTypes.string.isRequired,
|
||||||
subtitles: PropTypes.instanceOf(List),
|
subtitles: PropTypes.array,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Player = ({ url, subtitles }) => {
|
const Player = ({ url, subtitles }) => {
|
||||||
const hasSubtitles = !(subtitles === null || subtitles.size === 0);
|
const subs = subtitles || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="embed-responsive embed-responsive-16by9">
|
<div className="embed-responsive embed-responsive-16by9">
|
||||||
<video className="embed-responsive-item" controls>
|
<video className="embed-responsive-item" controls>
|
||||||
<source src={url} type="video/mp4" />
|
<source src={url} type="video/mp4" />
|
||||||
{hasSubtitles &&
|
{subs.map((sub, index) => (
|
||||||
subtitles
|
|
||||||
.toIndexedSeq()
|
|
||||||
.map((el, index) => (
|
|
||||||
<track
|
<track
|
||||||
key={index}
|
key={index}
|
||||||
kind="subtitles"
|
kind="subtitles"
|
||||||
label={el.get("language")}
|
label={sub.language}
|
||||||
src={el.get("vvt_file")}
|
src={sub.vvt_file}
|
||||||
srcLang={el.get("language")}
|
srcLang={sub.language}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</video>
|
</video>
|
||||||
@ -94,9 +91,6 @@ const Player = ({ url, subtitles }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
Player.propTypes = {
|
Player.propTypes = {
|
||||||
subtitles: PropTypes.instanceOf(List),
|
subtitles: PropTypes.array,
|
||||||
url: PropTypes.string.isRequired,
|
url: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
Player.defaultProps = {
|
|
||||||
subtitles: List(),
|
|
||||||
};
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { List } from "immutable";
|
|
||||||
|
|
||||||
import Dropdown from "react-bootstrap/Dropdown";
|
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 (
|
return (
|
||||||
<span className="mr-1 mb-1">
|
<span className="mr-1 mb-1">
|
||||||
<Dropdown drop="up" show={show} onToggle={onToggle} onSelect={onSelect}>
|
<Dropdown drop="up" show={show} onToggle={onToggle} onSelect={onSelect}>
|
||||||
@ -54,9 +53,9 @@ export const SubtitlesButton = ({
|
|||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)}
|
)}
|
||||||
{count > 0 &&
|
{count > 0 &&
|
||||||
subtitles.toIndexedSeq().map((subtitle, index) => (
|
subtitles.map((subtitle, index) => (
|
||||||
<Dropdown.Item href={subtitle.get("url")} key={index}>
|
<Dropdown.Item href={subtitle.url} key={index}>
|
||||||
{subtitle.get("language").split("_")[1]}
|
{subtitle.language.split("_")[1]}
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
))}
|
))}
|
||||||
</Dropdown.Menu>
|
</Dropdown.Menu>
|
||||||
@ -65,7 +64,7 @@ export const SubtitlesButton = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
SubtitlesButton.propTypes = {
|
SubtitlesButton.propTypes = {
|
||||||
subtitles: PropTypes.instanceOf(List),
|
subtitles: PropTypes.array,
|
||||||
inLibrary: PropTypes.bool.isRequired,
|
inLibrary: PropTypes.bool.isRequired,
|
||||||
searching: PropTypes.bool.isRequired,
|
searching: PropTypes.bool.isRequired,
|
||||||
search: PropTypes.func.isRequired,
|
search: PropTypes.func.isRequired,
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { List } from "immutable";
|
|
||||||
import { useDispatch } from "react-redux";
|
import { useDispatch } from "react-redux";
|
||||||
|
|
||||||
import { prettySize } from "../../utils";
|
import { prettySize } from "../../utils";
|
||||||
@ -8,49 +7,60 @@ import { addTorrent } from "../../actions/torrents";
|
|||||||
|
|
||||||
import Dropdown from "react-bootstrap/Dropdown";
|
import Dropdown from "react-bootstrap/Dropdown";
|
||||||
|
|
||||||
function buildMenuItems(torrents) {
|
const buildMenuItems = (torrents) => {
|
||||||
if (!torrents || torrents.size === 0) {
|
if (!torrents || torrents.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const t = torrents.groupBy((el) => el.get("source"));
|
|
||||||
|
|
||||||
// Build the array of entries
|
// Build the array of entries
|
||||||
let 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
|
// Push the title
|
||||||
entries.push({
|
entries.push({
|
||||||
type: "header",
|
type: "header",
|
||||||
value: source,
|
value: source,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Push the torrents
|
torrentsBySource.forEach((torrent) => {
|
||||||
for (let torrent of torrentList) {
|
|
||||||
entries.push({
|
entries.push({
|
||||||
type: "entry",
|
type: "entry",
|
||||||
quality: torrent.get("quality"),
|
quality: torrent.quality,
|
||||||
url: torrent.get("url"),
|
url: torrent.url,
|
||||||
size: torrent.get("size"),
|
size: torrent.size,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// Push the divider
|
// Push the divider
|
||||||
if (dividerCount > 0) {
|
if (dividerCount > 0) {
|
||||||
dividerCount--;
|
dividerCount--;
|
||||||
entries.push({ type: "divider" });
|
entries.push({ type: "divider" });
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
return entries;
|
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 }) => {
|
export const TorrentsButton = ({ torrents, search, searching, url }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const [show, setShow] = useState(false);
|
const [show, setShow] = useState(false);
|
||||||
const entries = buildMenuItems(torrents);
|
const entries = buildMenuItems(torrents);
|
||||||
const count = torrents && torrents.size !== 0 ? torrents.size : 0;
|
const count = countEntries(torrents);
|
||||||
|
|
||||||
const onSelect = (eventKey) => {
|
const onSelect = (eventKey) => {
|
||||||
// Close the dropdown if the eventkey is not specified
|
// Close the dropdown if the eventkey is not specified
|
||||||
@ -115,11 +125,8 @@ export const TorrentsButton = ({ torrents, search, searching, url }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
TorrentsButton.propTypes = {
|
TorrentsButton.propTypes = {
|
||||||
torrents: PropTypes.instanceOf(List),
|
torrents: PropTypes.object,
|
||||||
searching: PropTypes.bool,
|
searching: PropTypes.bool,
|
||||||
search: PropTypes.func.isRequired,
|
search: PropTypes.func.isRequired,
|
||||||
url: PropTypes.string,
|
url: PropTypes.string,
|
||||||
};
|
};
|
||||||
TorrentsButton.defaultProps = {
|
|
||||||
torrents: List(),
|
|
||||||
};
|
|
||||||
|
@ -1,15 +1,13 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { List } from "immutable";
|
|
||||||
|
|
||||||
export const Genres = ({ genres }) => {
|
export const Genres = ({ genres = [] }) => {
|
||||||
if (genres.size === 0) {
|
if (!genres || genres.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Uppercase first genres
|
// Uppercase first genres
|
||||||
const prettyGenres = genres
|
const prettyGenres = genres
|
||||||
.toJS()
|
|
||||||
.map((word) => word[0].toUpperCase() + word.substr(1))
|
.map((word) => word[0].toUpperCase() + word.substr(1))
|
||||||
.join(", ");
|
.join(", ");
|
||||||
|
|
||||||
@ -20,5 +18,4 @@ export const Genres = ({ genres }) => {
|
|||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
Genres.propTypes = { genres: PropTypes.instanceOf(List) };
|
Genres.propTypes = { genres: PropTypes.array };
|
||||||
Genres.defaultProps = { genres: List() };
|
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { Map } from "immutable";
|
|
||||||
|
|
||||||
import { inLibrary, isWishlisted } from "../../utils";
|
|
||||||
|
|
||||||
import { DownloadAndStream } from "../buttons/download";
|
import { DownloadAndStream } from "../buttons/download";
|
||||||
import { ImdbBadge } from "../buttons/imdb";
|
import { ImdbBadge } from "../buttons/imdb";
|
||||||
@ -17,9 +14,10 @@ import { Runtime } from "../details/runtime";
|
|||||||
import { Title } from "../details/title";
|
import { Title } from "../details/title";
|
||||||
|
|
||||||
const ListDetails = (props) => {
|
const ListDetails = (props) => {
|
||||||
if (props.data === undefined) {
|
if (!props.data || Object.keys(props.data).length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.loading) {
|
if (props.loading) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -27,46 +25,44 @@ const ListDetails = (props) => {
|
|||||||
return (
|
return (
|
||||||
<div className="col-8 col-md-4 list-details pl-1 d-flex align-items-start flex-column video-details flex-fill flex-column flex-nowrap">
|
<div className="col-8 col-md-4 list-details pl-1 d-flex align-items-start flex-column video-details flex-fill flex-column flex-nowrap">
|
||||||
<Title
|
<Title
|
||||||
title={props.data.get("title")}
|
title={props.data.title}
|
||||||
wishlist={props.wishlist}
|
wishlist={props.wishlist}
|
||||||
wishlisted={isWishlisted(props.data)}
|
wishlisted={props.wishlisted}
|
||||||
/>
|
|
||||||
<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")}
|
|
||||||
/>
|
/>
|
||||||
|
<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>
|
<div>
|
||||||
<ImdbBadge imdbId={props.data.get("imdb_id")} />
|
<ImdbBadge imdbId={props.data.imdb_id} />
|
||||||
<DownloadAndStream
|
<DownloadAndStream
|
||||||
url={props.data.get("polochon_url")}
|
url={props.data.polochon_url}
|
||||||
name={props.data.get("title")}
|
name={props.data.title}
|
||||||
subtitles={props.data.get("subtitles")}
|
subtitles={props.data.subtitles}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<TrackingLabel
|
<TrackingLabel
|
||||||
wishlisted={props.data.get("wishlisted")}
|
wishlisted={props.data.wishlisted}
|
||||||
inLibrary={inLibrary(props.data)}
|
inLibrary={props.data.polochon_url !== ""}
|
||||||
trackedSeason={props.data.get("tracked_season")}
|
trackedSeason={props.data.tracked_season}
|
||||||
trackedEpisode={props.data.get("tracked_episode")}
|
trackedEpisode={props.data.tracked_episode}
|
||||||
/>
|
/>
|
||||||
<PolochonMetadata
|
<PolochonMetadata
|
||||||
quality={props.data.get("quality")}
|
quality={props.data.quality}
|
||||||
releaseGroup={props.data.get("release_group")}
|
releaseGroup={props.data.release_group}
|
||||||
container={props.data.get("container")}
|
container={props.data.container}
|
||||||
audioCodec={props.data.get("audio_codec")}
|
audioCodec={props.data.audio_codec}
|
||||||
videoCodec={props.data.get("video_codec")}
|
videoCodec={props.data.video_codec}
|
||||||
/>
|
/>
|
||||||
<Plot plot={props.data.get("plot")} />
|
<Plot plot={props.data.plot} />
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
ListDetails.propTypes = {
|
ListDetails.propTypes = {
|
||||||
data: PropTypes.instanceOf(Map),
|
data: PropTypes.object,
|
||||||
wishlist: PropTypes.func,
|
wishlist: PropTypes.func,
|
||||||
|
wishlisted: PropTypes.bool,
|
||||||
loading: PropTypes.bool,
|
loading: PropTypes.bool,
|
||||||
children: PropTypes.object,
|
children: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
@ -31,7 +31,7 @@ const ExplorerOptions = ({
|
|||||||
|
|
||||||
const handleSourceChange = (event) => {
|
const handleSourceChange = (event) => {
|
||||||
let source = event.target.value;
|
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}`);
|
history.push(`/${type}/explore/${source}/${category}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -50,13 +50,13 @@ const ExplorerOptions = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Options are not yet fetched
|
// Options are not yet fetched
|
||||||
if (options.size === 0) {
|
if (Object.keys(options).length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let source = params.source;
|
let source = params.source;
|
||||||
let category = params.category;
|
let category = params.category;
|
||||||
let categories = options.get(params.source);
|
let categories = options[params.source];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
@ -72,7 +72,7 @@ const ExplorerOptions = ({
|
|||||||
onChange={handleSourceChange}
|
onChange={handleSourceChange}
|
||||||
value={source}
|
value={source}
|
||||||
>
|
>
|
||||||
{options.keySeq().map(function (source) {
|
{Object.keys(options).map(function (source) {
|
||||||
return (
|
return (
|
||||||
<option key={source} value={source}>
|
<option key={source} value={source}>
|
||||||
{prettyName(source)}
|
{prettyName(source)}
|
||||||
|
@ -8,6 +8,8 @@ const ListFilter = ({ placeHolder, updateFilter }) => {
|
|||||||
// Start filtering at 3 chars
|
// Start filtering at 3 chars
|
||||||
if (filter.length >= 3) {
|
if (filter.length >= 3) {
|
||||||
updateFilter(filter);
|
updateFilter(filter);
|
||||||
|
} else {
|
||||||
|
updateFilter("");
|
||||||
}
|
}
|
||||||
}, [filter, updateFilter]);
|
}, [filter, updateFilter]);
|
||||||
|
|
||||||
|
106
frontend/js/components/list/keyboard.js
Normal file
106
frontend/js/components/list/keyboard.js
Normal file
@ -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,
|
||||||
|
};
|
@ -1,94 +1,115 @@
|
|||||||
import React, { useState, useEffect, useCallback } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { OrderedMap, Map } from "immutable";
|
|
||||||
import fuzzy from "fuzzy";
|
import fuzzy from "fuzzy";
|
||||||
import InfiniteScroll from "react-infinite-scroll-component";
|
import InfiniteScroll from "react-infinite-scroll-component";
|
||||||
|
|
||||||
import ListFilter from "./filter";
|
import ListFilter from "./filter";
|
||||||
import ExplorerOptions from "./explorerOptions";
|
import ExplorerOptions from "./explorerOptions";
|
||||||
import Poster from "./poster";
|
import Poster from "./poster";
|
||||||
|
import { KeyboardNavigation } from "./keyboard";
|
||||||
|
|
||||||
import Loader from "../loader/loader";
|
import Loader from "../loader/loader";
|
||||||
|
|
||||||
const ListPosters = (props) => {
|
const ListPosters = ({
|
||||||
if (props.loading) {
|
data,
|
||||||
return <Loader />;
|
exploreFetchOptions,
|
||||||
}
|
exploreOptions,
|
||||||
|
loading,
|
||||||
|
onClick,
|
||||||
|
onDoubleClick,
|
||||||
|
onKeyEnter,
|
||||||
|
params,
|
||||||
|
placeHolder,
|
||||||
|
selectedImdbId,
|
||||||
|
type,
|
||||||
|
}) => {
|
||||||
|
const [list, setList] = useState([]);
|
||||||
|
const [filteredList, setFilteredList] = useState([]);
|
||||||
|
const [filter, setFilter] = useState("");
|
||||||
|
|
||||||
let elmts = props.data;
|
useEffect(() => {
|
||||||
const listSize = elmts !== undefined ? elmts.size : 0;
|
let l = [];
|
||||||
|
data.forEach((e) => {
|
||||||
|
l.push({
|
||||||
|
imdbId: e.imdb_id,
|
||||||
|
posterUrl: e.poster_url,
|
||||||
|
title: e.title,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
setList(l);
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
// Filter the list of elements
|
useEffect(() => {
|
||||||
if (props.filter !== "") {
|
if (filter !== "" && filter.length >= 3) {
|
||||||
elmts = elmts.filter((v) => fuzzy.test(props.filter, v.get("title")));
|
setFilteredList(list.filter((e) => fuzzy.test(filter, e.title)));
|
||||||
} else {
|
} else {
|
||||||
elmts = elmts.slice(0, listSize.items);
|
setFilteredList(list);
|
||||||
|
}
|
||||||
|
}, [filter, list]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <Loader />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chose when to display filter / explore options
|
// Chose when to display filter / explore options
|
||||||
let displayFilter = true;
|
let displayFilter = true;
|
||||||
if (
|
if (
|
||||||
(props.params &&
|
(params &&
|
||||||
props.params.category &&
|
params.category &&
|
||||||
props.params.category !== "" &&
|
params.category !== "" &&
|
||||||
props.params.source &&
|
params.source &&
|
||||||
props.params.source !== "") ||
|
params.source !== "") ||
|
||||||
listSize === 0
|
list.length === 0
|
||||||
) {
|
) {
|
||||||
displayFilter = false;
|
displayFilter = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let displayExplorerOptions = false;
|
let displayExplorerOptions = false;
|
||||||
if (listSize !== 0) {
|
if (list.length !== 0) {
|
||||||
displayExplorerOptions = !displayFilter;
|
displayExplorerOptions = !displayFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="col-4 col-md-8 px-1">
|
<div className="col-4 col-md-8 px-1">
|
||||||
{displayFilter && (
|
{displayFilter && (
|
||||||
<ListFilter
|
<ListFilter updateFilter={setFilter} placeHolder={placeHolder} />
|
||||||
updateFilter={props.updateFilter}
|
|
||||||
placeHolder={props.placeHolder}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
<ExplorerOptions
|
<ExplorerOptions
|
||||||
type={props.type}
|
type={type}
|
||||||
display={displayExplorerOptions}
|
display={displayExplorerOptions}
|
||||||
params={props.params}
|
params={params}
|
||||||
fetch={props.exploreFetchOptions}
|
fetch={exploreFetchOptions}
|
||||||
options={props.exploreOptions}
|
options={exploreOptions}
|
||||||
/>
|
/>
|
||||||
<Posters
|
<Posters
|
||||||
elmts={elmts}
|
list={filteredList}
|
||||||
loading={props.loading}
|
loading={loading}
|
||||||
selectedImdbId={props.selectedImdbId}
|
selectedImdbId={selectedImdbId}
|
||||||
selectPoster={props.onClick}
|
selectPoster={onClick}
|
||||||
onDoubleClick={props.onDoubleClick}
|
onDoubleClick={onDoubleClick}
|
||||||
onKeyEnter={props.onKeyEnter}
|
onKeyEnter={onKeyEnter}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
ListPosters.propTypes = {
|
ListPosters.propTypes = {
|
||||||
data: PropTypes.instanceOf(OrderedMap),
|
data: PropTypes.object,
|
||||||
onClick: PropTypes.func,
|
onClick: PropTypes.func,
|
||||||
onDoubleClick: PropTypes.func,
|
onDoubleClick: PropTypes.func,
|
||||||
onKeyEnter: PropTypes.func,
|
onKeyEnter: PropTypes.func,
|
||||||
selectedImdbId: PropTypes.string,
|
selectedImdbId: PropTypes.string,
|
||||||
loading: PropTypes.bool.isRequired,
|
loading: PropTypes.bool.isRequired,
|
||||||
params: PropTypes.object.isRequired,
|
params: PropTypes.object.isRequired,
|
||||||
exploreOptions: PropTypes.instanceOf(Map),
|
exploreOptions: PropTypes.object,
|
||||||
type: PropTypes.string.isRequired,
|
type: PropTypes.string.isRequired,
|
||||||
placeHolder: PropTypes.string.isRequired,
|
placeHolder: PropTypes.string.isRequired,
|
||||||
updateFilter: PropTypes.func.isRequired,
|
|
||||||
filter: PropTypes.string,
|
|
||||||
exploreFetchOptions: PropTypes.func,
|
exploreFetchOptions: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ListPosters;
|
export default ListPosters;
|
||||||
|
|
||||||
const Posters = ({
|
const Posters = ({
|
||||||
elmts,
|
list,
|
||||||
onKeyEnter,
|
onKeyEnter,
|
||||||
selectedImdbId,
|
selectedImdbId,
|
||||||
selectPoster,
|
selectPoster,
|
||||||
@ -96,120 +117,20 @@ const Posters = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const addMoreCount = 20;
|
const addMoreCount = 20;
|
||||||
const [size, setSize] = useState(0);
|
const [size, setSize] = useState(0);
|
||||||
const [postersPerRow, setPostersPerRow] = useState(0);
|
|
||||||
const [posterHeight, setPosterHeight] = useState(0);
|
|
||||||
const [initialLoading, setInitialLoading] = useState(true);
|
|
||||||
|
|
||||||
const loadMore = useCallback(() => {
|
const updateSize = (newSize, maxSize) => {
|
||||||
if (size === elmts.size) {
|
setSize(Math.min(newSize, maxSize));
|
||||||
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);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.onkeypress = move;
|
updateSize(size + addMoreCount, list.length);
|
||||||
return () => {
|
}, [list]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
document.onkeypress = null;
|
|
||||||
};
|
|
||||||
}, [move]);
|
|
||||||
|
|
||||||
if (elmts.size === 0) {
|
const loadMore = () => {
|
||||||
|
updateSize(size + addMoreCount, list.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (list.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="jumbotron">
|
<div className="jumbotron">
|
||||||
<h2>No result</h2>
|
<h2>No result</h2>
|
||||||
@ -218,39 +139,38 @@ const Posters = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={posterCount}>
|
<KeyboardNavigation
|
||||||
|
onKeyEnter={onKeyEnter}
|
||||||
|
selectPoster={selectPoster}
|
||||||
|
list={list}
|
||||||
|
selected={selectedImdbId}
|
||||||
|
>
|
||||||
<InfiniteScroll
|
<InfiniteScroll
|
||||||
className="poster-list d-flex flex-column flex-sm-row flex-sm-wrap justify-content-around"
|
className="poster-list d-flex flex-column flex-sm-row flex-sm-wrap justify-content-around"
|
||||||
dataLength={size}
|
dataLength={size}
|
||||||
next={loadMore}
|
next={loadMore}
|
||||||
hasMore={size !== elmts.size}
|
hasMore={size !== list.length}
|
||||||
loader={<Loader />}
|
loader={<Loader />}
|
||||||
>
|
>
|
||||||
{elmts
|
{list.slice(0, size).map((el, index) => {
|
||||||
.slice(0, size)
|
const imdbId = el.imdbId;
|
||||||
.toIndexedSeq()
|
|
||||||
.map(function (el, index) {
|
|
||||||
const imdbId = el.get("imdb_id");
|
|
||||||
const selected = imdbId === selectedImdbId ? true : false;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Poster
|
<Poster
|
||||||
url={el.get("poster_url")}
|
url={el.posterUrl}
|
||||||
key={index}
|
key={index}
|
||||||
selected={selected}
|
selected={imdbId === selectedImdbId}
|
||||||
onClick={() => selectPoster(imdbId)}
|
onClick={() => selectPoster(imdbId)}
|
||||||
onDoubleClick={() => onDoubleClick(imdbId)}
|
onDoubleClick={() => onDoubleClick(imdbId)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}, this)}
|
}, this)}
|
||||||
</InfiniteScroll>
|
</InfiniteScroll>
|
||||||
</div>
|
</KeyboardNavigation>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
Posters.propTypes = {
|
Posters.propTypes = {
|
||||||
elmts: PropTypes.instanceOf(OrderedMap),
|
list: PropTypes.array.isRequired,
|
||||||
selectedImdbId: PropTypes.string,
|
selectedImdbId: PropTypes.string,
|
||||||
loading: PropTypes.bool.isRequired,
|
|
||||||
onDoubleClick: PropTypes.func,
|
onDoubleClick: PropTypes.func,
|
||||||
onKeyEnter: PropTypes.func,
|
onKeyEnter: PropTypes.func,
|
||||||
selectPoster: PropTypes.func,
|
selectPoster: PropTypes.func,
|
||||||
|
@ -1,65 +1,57 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import Loader from "../loader/loader";
|
import Loader from "../loader/loader";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { Map, List } from "immutable";
|
|
||||||
|
|
||||||
// TODO: udpate this
|
// TODO: udpate this
|
||||||
import { OverlayTrigger, Tooltip } from "react-bootstrap";
|
import { OverlayTrigger, Tooltip } from "react-bootstrap";
|
||||||
|
|
||||||
const Modules = (props) => {
|
const Modules = ({ isLoading, modules }) => {
|
||||||
if (props.isLoading) {
|
if (isLoading) {
|
||||||
return <Loader />;
|
return <Loader />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
{props.modules &&
|
{Object.keys(modules).map((type) => (
|
||||||
props.modules
|
<ModulesByVideoType key={type} type={type} modules={modules[type]} />
|
||||||
.keySeq()
|
|
||||||
.map((value, key) => (
|
|
||||||
<ModulesByVideoType
|
|
||||||
key={key}
|
|
||||||
videoType={value}
|
|
||||||
data={props.modules.get(value)}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
Modules.propTypes = {
|
Modules.propTypes = {
|
||||||
isLoading: PropTypes.bool.isRequired,
|
isLoading: PropTypes.bool.isRequired,
|
||||||
modules: PropTypes.instanceOf(Map),
|
modules: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
export default Modules;
|
export default Modules;
|
||||||
|
|
||||||
const capitalize = (string) => string.charAt(0).toUpperCase() + string.slice(1);
|
const capitalize = (string) => string.charAt(0).toUpperCase() + string.slice(1);
|
||||||
|
|
||||||
const ModulesByVideoType = (props) => (
|
const ModulesByVideoType = ({ type, modules }) => (
|
||||||
<div className="col-12 col-md-6">
|
<div className="col-12 col-md-6">
|
||||||
<div className="card mb-3">
|
<div className="card mb-3">
|
||||||
<div className="card-header">
|
<div className="card-header">
|
||||||
<h3>{`${capitalize(props.videoType)} modules`}</h3>
|
<h3>{`${capitalize(type)} modules`}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
{props.data.keySeq().map((value, key) => (
|
{Object.keys(modules).map((moduleType, i) => (
|
||||||
<ModuleByType key={key} type={value} data={props.data.get(value)} />
|
<ModuleByType key={i} type={type} modules={modules[moduleType]} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
ModulesByVideoType.propTypes = {
|
ModulesByVideoType.propTypes = {
|
||||||
videoType: PropTypes.string.isRequired,
|
type: PropTypes.string.isRequired,
|
||||||
data: PropTypes.instanceOf(Map),
|
modules: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ModuleByType = (props) => (
|
const ModuleByType = ({ type, modules }) => (
|
||||||
<div>
|
<div>
|
||||||
<h4>{capitalize(props.type)}</h4>
|
<h4>{capitalize(type)}</h4>
|
||||||
<table className="table">
|
<table className="table">
|
||||||
<tbody>
|
<tbody>
|
||||||
{props.data.map(function (value, key) {
|
{modules.map((module, type) => {
|
||||||
return <Module key={key} type={key} data={value} />;
|
return <Module key={type} type={type} module={module} />;
|
||||||
})}
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@ -67,14 +59,13 @@ const ModuleByType = (props) => (
|
|||||||
);
|
);
|
||||||
ModuleByType.propTypes = {
|
ModuleByType.propTypes = {
|
||||||
type: PropTypes.string.isRequired,
|
type: PropTypes.string.isRequired,
|
||||||
data: PropTypes.instanceOf(List),
|
modules: PropTypes.array.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Module = (props) => {
|
const Module = ({ module }) => {
|
||||||
let iconClass, prettyStatus, badgeClass;
|
let iconClass, prettyStatus, badgeClass;
|
||||||
const name = props.data.get("name");
|
|
||||||
|
|
||||||
switch (props.data.get("status")) {
|
switch (module.status) {
|
||||||
case "ok":
|
case "ok":
|
||||||
iconClass = "fa fa-check-circle";
|
iconClass = "fa fa-check-circle";
|
||||||
badgeClass = "badge badge-pill badge-success";
|
badgeClass = "badge badge-pill badge-success";
|
||||||
@ -97,19 +88,17 @@ const Module = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tooltip = (
|
const tooltip = (
|
||||||
<Tooltip id={`tooltip-status-${name}`}>
|
<Tooltip id={`tooltip-status-${module.name}}`}>
|
||||||
<p>
|
<p>
|
||||||
<span className={badgeClass}>Status: {prettyStatus}</span>
|
<span className={badgeClass}>Status: {prettyStatus}</span>
|
||||||
</p>
|
</p>
|
||||||
{props.data.get("error") !== "" && (
|
{module.error !== "" && <p>Error: {module.error}</p>}
|
||||||
<p>Error: {props.data.get("error")}</p>
|
|
||||||
)}
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
<th>{name}</th>
|
<th>{module.name}</th>
|
||||||
<td>
|
<td>
|
||||||
<OverlayTrigger placement="right" overlay={tooltip}>
|
<OverlayTrigger placement="right" overlay={tooltip}>
|
||||||
<span className={badgeClass}>
|
<span className={badgeClass}>
|
||||||
@ -121,5 +110,5 @@ const Module = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
Module.propTypes = {
|
Module.propTypes = {
|
||||||
data: PropTypes.instanceOf(Map),
|
module: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
import React, { useEffect, useCallback } from "react";
|
import React, { useEffect, useCallback } from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { useSelector, useDispatch } from "react-redux";
|
import { useSelector, useDispatch } from "react-redux";
|
||||||
import { Map } from "immutable";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
selectMovie,
|
selectMovie,
|
||||||
updateFilter,
|
|
||||||
movieWishlistToggle,
|
movieWishlistToggle,
|
||||||
fetchMovies,
|
fetchMovies,
|
||||||
getMovieExploreOptions,
|
getMovieExploreOptions,
|
||||||
@ -14,8 +12,6 @@ import {
|
|||||||
import ListDetails from "../list/details";
|
import ListDetails from "../list/details";
|
||||||
import ListPosters from "../list/posters";
|
import ListPosters from "../list/posters";
|
||||||
|
|
||||||
import { inLibrary, isWishlisted } from "../../utils";
|
|
||||||
|
|
||||||
import { ShowMore } from "../buttons/showMore";
|
import { ShowMore } from "../buttons/showMore";
|
||||||
|
|
||||||
import { MovieSubtitlesButton } from "./subtitlesButton";
|
import { MovieSubtitlesButton } from "./subtitlesButton";
|
||||||
@ -51,25 +47,17 @@ const MovieList = ({ match }) => {
|
|||||||
}
|
}
|
||||||
}, [dispatch, match]);
|
}, [dispatch, match]);
|
||||||
|
|
||||||
const loading = useSelector((state) => state.movieStore.get("loading"));
|
const loading = useSelector((state) => state.movies.loading);
|
||||||
const movies = useSelector((state) => state.movieStore.get("movies"));
|
const movies = useSelector((state) => state.movies.movies);
|
||||||
const filter = useSelector((state) => state.movieStore.get("filter"));
|
const selectedImdbId = useSelector((state) => state.movies.selectedImdbId);
|
||||||
const selectedImdbId = useSelector((state) =>
|
const exploreOptions = useSelector((state) => state.movies.exploreOptions);
|
||||||
state.movieStore.get("selectedImdbId")
|
|
||||||
);
|
|
||||||
const exploreOptions = useSelector((state) =>
|
|
||||||
state.movieStore.get("exploreOptions")
|
|
||||||
);
|
|
||||||
|
|
||||||
let selectedMovie = Map();
|
let selectedMovie = {};
|
||||||
if (movies !== undefined && movies.has(selectedImdbId)) {
|
if (movies !== undefined && movies.has(selectedImdbId)) {
|
||||||
selectedMovie = movies.get(selectedImdbId);
|
selectedMovie = movies.get(selectedImdbId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectFunc = useCallback((id) => dispatch(selectMovie(id)), [dispatch]);
|
const selectFunc = useCallback((id) => dispatch(selectMovie(id)), [dispatch]);
|
||||||
const filterFunc = useCallback((filter) => dispatch(updateFilter(filter)), [
|
|
||||||
dispatch,
|
|
||||||
]);
|
|
||||||
const exploreFetchOptions = useCallback(
|
const exploreFetchOptions = useCallback(
|
||||||
() => dispatch(getMovieExploreOptions()),
|
() => dispatch(getMovieExploreOptions()),
|
||||||
[dispatch]
|
[dispatch]
|
||||||
@ -84,8 +72,6 @@ const MovieList = ({ match }) => {
|
|||||||
exploreFetchOptions={exploreFetchOptions}
|
exploreFetchOptions={exploreFetchOptions}
|
||||||
exploreOptions={exploreOptions}
|
exploreOptions={exploreOptions}
|
||||||
selectedImdbId={selectedImdbId}
|
selectedImdbId={selectedImdbId}
|
||||||
updateFilter={filterFunc}
|
|
||||||
filter={filter}
|
|
||||||
onClick={selectFunc}
|
onClick={selectFunc}
|
||||||
onDoubleClick={() => {}}
|
onDoubleClick={() => {}}
|
||||||
onKeyEnter={() => {}}
|
onKeyEnter={() => {}}
|
||||||
@ -95,31 +81,19 @@ const MovieList = ({ match }) => {
|
|||||||
<ListDetails
|
<ListDetails
|
||||||
data={selectedMovie}
|
data={selectedMovie}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
wishlisted={selectedMovie.wishlisted}
|
||||||
wishlist={() =>
|
wishlist={() =>
|
||||||
dispatch(
|
dispatch(
|
||||||
movieWishlistToggle(
|
movieWishlistToggle(selectedMovie.imdb_id, selectedMovie.wishlisted)
|
||||||
selectedMovie.get("imdb_id"),
|
|
||||||
isWishlisted(selectedMovie)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ShowMore
|
<ShowMore
|
||||||
id={selectedMovie.get("imdb_id")}
|
id={selectedMovie.imdb_id}
|
||||||
inLibrary={inLibrary(selectedMovie)}
|
inLibrary={selectedMovie.polochon_url !== ""}
|
||||||
>
|
>
|
||||||
<MovieTorrentsButton
|
<MovieTorrentsButton />
|
||||||
torrents={selectedMovie.get("torrents")}
|
<MovieSubtitlesButton />
|
||||||
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)}
|
|
||||||
/>
|
|
||||||
</ShowMore>
|
</ShowMore>
|
||||||
</ListDetails>
|
</ListDetails>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,20 +1,24 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import PropTypes from "prop-types";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { List } from "immutable";
|
|
||||||
import { useDispatch } from "react-redux";
|
|
||||||
|
|
||||||
import { searchMovieSubtitles } from "../../actions/subtitles";
|
import { searchMovieSubtitles } from "../../actions/subtitles";
|
||||||
|
|
||||||
import { SubtitlesButton } from "../buttons/subtitles";
|
import { SubtitlesButton } from "../buttons/subtitles";
|
||||||
|
|
||||||
export const MovieSubtitlesButton = ({
|
export const MovieSubtitlesButton = () => {
|
||||||
inLibrary,
|
|
||||||
imdbId,
|
|
||||||
searching,
|
|
||||||
subtitles,
|
|
||||||
}) => {
|
|
||||||
const dispatch = useDispatch();
|
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 (
|
return (
|
||||||
<SubtitlesButton
|
<SubtitlesButton
|
||||||
inLibrary={inLibrary}
|
inLibrary={inLibrary}
|
||||||
@ -24,9 +28,3 @@ export const MovieSubtitlesButton = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
MovieSubtitlesButton.propTypes = {
|
|
||||||
searching: PropTypes.bool,
|
|
||||||
inLibrary: PropTypes.bool,
|
|
||||||
imdbId: PropTypes.string,
|
|
||||||
subtitles: PropTypes.instanceOf(List),
|
|
||||||
};
|
|
||||||
|
@ -1,13 +1,22 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import PropTypes from "prop-types";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { List } from "immutable";
|
|
||||||
import { useDispatch } from "react-redux";
|
|
||||||
|
|
||||||
import { getMovieDetails } from "../../actions/movies";
|
import { getMovieDetails } from "../../actions/movies";
|
||||||
|
|
||||||
import { TorrentsButton } from "../buttons/torrents";
|
import { TorrentsButton } from "../buttons/torrents";
|
||||||
|
|
||||||
export const MovieTorrentsButton = ({ torrents, imdbId, title, searching }) => {
|
export const MovieTorrentsButton = () => {
|
||||||
const dispatch = useDispatch();
|
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 (
|
return (
|
||||||
<TorrentsButton
|
<TorrentsButton
|
||||||
torrents={torrents}
|
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,
|
|
||||||
};
|
|
||||||
|
@ -8,14 +8,11 @@ import Nav from "react-bootstrap/Nav";
|
|||||||
import Navbar from "react-bootstrap/Navbar";
|
import Navbar from "react-bootstrap/Navbar";
|
||||||
import NavDropdown from "react-bootstrap/NavDropdown";
|
import NavDropdown from "react-bootstrap/NavDropdown";
|
||||||
|
|
||||||
const AppNavBar = () => {
|
export const AppNavBar = () => {
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
|
||||||
const username = useSelector((state) => state.userStore.get("username"));
|
const username = useSelector((state) => state.user.username);
|
||||||
const isAdmin = useSelector((state) => state.userStore.get("isAdmin"));
|
const isAdmin = useSelector((state) => state.user.isAdmin);
|
||||||
const torrentCount = useSelector(
|
|
||||||
(state) => state.torrentStore.get("torrents").size
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Navbar
|
<Navbar
|
||||||
@ -36,7 +33,7 @@ const AppNavBar = () => {
|
|||||||
<MoviesDropdown />
|
<MoviesDropdown />
|
||||||
<ShowsDropdown />
|
<ShowsDropdown />
|
||||||
<WishlistDropdown />
|
<WishlistDropdown />
|
||||||
<TorrentsDropdown torrentsCount={torrentCount} />
|
<TorrentsDropdown />
|
||||||
</Nav>
|
</Nav>
|
||||||
<Nav>
|
<Nav>
|
||||||
<Route
|
<Route
|
||||||
@ -70,7 +67,6 @@ const AppNavBar = () => {
|
|||||||
AppNavBar.propTypes = {
|
AppNavBar.propTypes = {
|
||||||
history: PropTypes.object,
|
history: PropTypes.object,
|
||||||
};
|
};
|
||||||
export default AppNavBar;
|
|
||||||
|
|
||||||
const Search = ({ path, placeholder, setExpanded, history }) => {
|
const Search = ({ path, placeholder, setExpanded, history }) => {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
@ -159,8 +155,8 @@ const WishlistDropdown = () => (
|
|||||||
</NavDropdown>
|
</NavDropdown>
|
||||||
);
|
);
|
||||||
|
|
||||||
const TorrentsDropdown = (props) => {
|
const TorrentsDropdown = () => {
|
||||||
const title = <TorrentsDropdownTitle torrentsCount={props.torrentsCount} />;
|
const title = <TorrentsDropdownTitle />;
|
||||||
return (
|
return (
|
||||||
<NavDropdown title={title} id="navbar-wishlit-dropdown">
|
<NavDropdown title={title} id="navbar-wishlit-dropdown">
|
||||||
<LinkContainer to="/torrents/list">
|
<LinkContainer to="/torrents/list">
|
||||||
@ -172,26 +168,17 @@ const TorrentsDropdown = (props) => {
|
|||||||
</NavDropdown>
|
</NavDropdown>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
TorrentsDropdown.propTypes = { torrentsCount: PropTypes.number.isRequired };
|
|
||||||
|
|
||||||
const TorrentsDropdownTitle = (props) => {
|
const TorrentsDropdownTitle = () => {
|
||||||
if (props.torrentsCount === 0) {
|
const count = useSelector((state) => state.torrents.torrents.length);
|
||||||
|
if (count === 0) {
|
||||||
return <span>Torrents</span>;
|
return <span>Torrents</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
{" "}
|
|
||||||
Torrents
|
Torrents
|
||||||
<span>
|
<span className="badge badge-info badge-pill ml-1">{count}</span>
|
||||||
{" "}
|
|
||||||
<span className="badge badge-info badge-pill">
|
|
||||||
{props.torrentsCount}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
TorrentsDropdownTitle.propTypes = {
|
|
||||||
torrentsCount: PropTypes.number.isRequired,
|
|
||||||
};
|
|
||||||
|
@ -1,58 +1,44 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { useDispatch } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
import { Toast } from "react-bootstrap";
|
import { Toast } from "react-bootstrap";
|
||||||
|
|
||||||
import { removeNotification } from "../../actions/notifications";
|
import { removeNotification } from "../../actions/notifications";
|
||||||
|
|
||||||
export const Notification = ({
|
export const Notification = ({ id }) => {
|
||||||
id,
|
|
||||||
icon,
|
|
||||||
title,
|
|
||||||
message,
|
|
||||||
imageUrl,
|
|
||||||
autohide,
|
|
||||||
delay,
|
|
||||||
}) => {
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const notification = useSelector((state) => state.notifications.get(id));
|
||||||
|
|
||||||
const [show, setShow] = useState(true);
|
const [show, setShow] = useState(true);
|
||||||
|
|
||||||
const hide = () => {
|
const hide = () => {
|
||||||
setShow(false);
|
setShow(false);
|
||||||
setTimeout(() => dispatch(removeNotification(id)), 200);
|
dispatch(removeNotification(id));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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>
|
<Toast.Header>
|
||||||
{icon !== "" && <i className={`fa fa-${icon} mr-2`} />}
|
{notification.icon && (
|
||||||
<strong className="mr-auto">{title}</strong>
|
<i className={`fa fa-${notification.icon} mr-2`} />
|
||||||
|
)}
|
||||||
|
<strong className="mr-auto">{notification.title}</strong>
|
||||||
</Toast.Header>
|
</Toast.Header>
|
||||||
<Toast.Body>
|
<Toast.Body>
|
||||||
{message !== "" && <span>{message}</span>}
|
{notification.message !== "" && <span>{notification.message}</span>}
|
||||||
{imageUrl !== "" && (
|
{notification.imageUrl !== "" && (
|
||||||
<img src={imageUrl} className="img-fluid mt-2 mr-auto" />
|
<img src={notification.imageUrl} className="img-fluid mt-2 mr-auto" />
|
||||||
)}
|
)}
|
||||||
</Toast.Body>
|
</Toast.Body>
|
||||||
</Toast>
|
</Toast>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
Notification.propTypes = {
|
Notification.propTypes = {
|
||||||
id: PropTypes.string,
|
id: PropTypes.string.isRequired,
|
||||||
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: "",
|
|
||||||
};
|
};
|
||||||
|
@ -1,29 +1,18 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import { List } from "immutable";
|
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
import { Notification } from "./notification";
|
import { Notification } from "./notification";
|
||||||
|
|
||||||
export const Notifications = () => {
|
export const Notifications = () => {
|
||||||
const notifications = useSelector((state) => state.notifications);
|
const notificationIds = useSelector((state) =>
|
||||||
|
Array.from(state.notifications.keys())
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="notifications">
|
<div className="notifications">
|
||||||
{notifications.map((el) => (
|
{notificationIds.map((id) => (
|
||||||
<Notification
|
<Notification key={id} id={id} />
|
||||||
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")}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
Notifications.propTypes = {
|
|
||||||
notifications: PropTypes.instanceOf(List),
|
|
||||||
};
|
|
||||||
|
@ -7,7 +7,7 @@ import { Polochon } from "./polochon";
|
|||||||
import { PolochonAdd } from "./add";
|
import { PolochonAdd } from "./add";
|
||||||
|
|
||||||
export const PolochonList = () => {
|
export const PolochonList = () => {
|
||||||
const list = useSelector((state) => state.polochon.get("managed"));
|
const list = useSelector((state) => state.polochon.managed);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -20,15 +20,15 @@ export const PolochonList = () => {
|
|||||||
<h2>My polochons</h2>
|
<h2>My polochons</h2>
|
||||||
<hr />
|
<hr />
|
||||||
<span>
|
<span>
|
||||||
{list.map((el, index) => (
|
{list.map((polochon, index) => (
|
||||||
<Polochon
|
<Polochon
|
||||||
key={index}
|
key={index}
|
||||||
id={el.get("id")}
|
id={polochon.id}
|
||||||
name={el.get("name")}
|
name={polochon.name}
|
||||||
token={el.get("token")}
|
token={polochon.token}
|
||||||
url={el.get("url")}
|
url={polochon.url}
|
||||||
authToken={el.get("auth_token")}
|
authToken={polochon.auth_token}
|
||||||
users={el.get("users")}
|
users={polochon.users}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</span>
|
</span>
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { List } from "immutable";
|
|
||||||
import { useDispatch } from "react-redux";
|
import { useDispatch } from "react-redux";
|
||||||
|
|
||||||
import { PolochonUsers } from "./users";
|
import { PolochonUsers } from "./users";
|
||||||
@ -57,5 +56,5 @@ Polochon.propTypes = {
|
|||||||
token: PropTypes.string,
|
token: PropTypes.string,
|
||||||
url: PropTypes.string,
|
url: PropTypes.string,
|
||||||
authToken: PropTypes.string,
|
authToken: PropTypes.string,
|
||||||
users: PropTypes.instanceOf(List),
|
users: PropTypes.array.isRequired,
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import PropTypes from "prop-types";
|
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 (
|
return (
|
||||||
<select
|
<select
|
||||||
className="form-control"
|
className="form-control"
|
||||||
@ -11,9 +13,9 @@ export const PolochonSelect = ({ value, changeValue, polochonList }) => {
|
|||||||
changeValue(e.target.options[e.target.selectedIndex].value)
|
changeValue(e.target.options[e.target.selectedIndex].value)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{polochonList.map((el, index) => (
|
{publicPolochons.map((polochon) => (
|
||||||
<option value={el.get("id")} key={index}>
|
<option value={polochon.id} key={polochon.id}>
|
||||||
{el.get("name")} ({el.get("url")})
|
{polochon.name} ({polochon.url})
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
@ -21,6 +23,5 @@ export const PolochonSelect = ({ value, changeValue, polochonList }) => {
|
|||||||
};
|
};
|
||||||
PolochonSelect.propTypes = {
|
PolochonSelect.propTypes = {
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
changeValue: PropTypes.func,
|
changeValue: PropTypes.func.isRequired,
|
||||||
polochonList: PropTypes.instanceOf(List),
|
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { List } from "immutable";
|
|
||||||
|
|
||||||
import { PolochonUser } from "./user";
|
import { PolochonUser } from "./user";
|
||||||
|
|
||||||
@ -23,10 +22,10 @@ export const PolochonUsers = ({ id, users }) => {
|
|||||||
<PolochonUser
|
<PolochonUser
|
||||||
key={index}
|
key={index}
|
||||||
polochonId={id}
|
polochonId={id}
|
||||||
id={user.get("id")}
|
id={user.id}
|
||||||
name={user.get("name")}
|
name={user.name}
|
||||||
initialToken={user.get("token")}
|
initialToken={user.token}
|
||||||
initialActivated={user.get("polochon_activated")}
|
initialActivated={user.polochon_activated}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -34,6 +33,6 @@ export const PolochonUsers = ({ id, users }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
PolochonUsers.propTypes = {
|
PolochonUsers.propTypes = {
|
||||||
id: PropTypes.string,
|
id: PropTypes.string.isRequired,
|
||||||
users: PropTypes.instanceOf(List),
|
users: PropTypes.array.isRequired,
|
||||||
};
|
};
|
||||||
|
@ -12,8 +12,7 @@ import { fetchShowDetails } from "../../actions/shows";
|
|||||||
|
|
||||||
export const ShowDetails = ({ match }) => {
|
export const ShowDetails = ({ match }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const loading = useSelector((state) => state.showStore.get("loading"));
|
const loading = useSelector((state) => state.show.loading);
|
||||||
const show = useSelector((state) => state.showStore.get("show"));
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(fetchShowDetails(match.params.imdbId));
|
dispatch(fetchShowDetails(match.params.imdbId));
|
||||||
@ -25,10 +24,10 @@ export const ShowDetails = ({ match }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Fanart url={show.get("fanart_url")} />
|
<Fanart />
|
||||||
<div className="row no-gutters">
|
<div className="row no-gutters">
|
||||||
<Header data={show} />
|
<Header />
|
||||||
<SeasonsList data={show} />
|
<SeasonsList />
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
|
@ -1,15 +1,10 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { Map } from "immutable";
|
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
import { showWishlistToggle } from "../../../actions/shows";
|
import { showWishlistToggle } from "../../../actions/shows";
|
||||||
|
|
||||||
import {
|
import { prettyEpisodeName } from "../../../utils";
|
||||||
inLibrary,
|
|
||||||
isEpisodeWishlisted,
|
|
||||||
prettyEpisodeName,
|
|
||||||
} from "../../../utils";
|
|
||||||
|
|
||||||
import { Plot } from "../../details/plot";
|
import { Plot } from "../../details/plot";
|
||||||
import { PolochonMetadata } from "../../details/polochon";
|
import { PolochonMetadata } from "../../details/polochon";
|
||||||
@ -24,88 +19,75 @@ import { EpisodeSubtitlesButton } from "./subtitlesButton";
|
|||||||
import { EpisodeThumb } from "./episodeThumb";
|
import { EpisodeThumb } from "./episodeThumb";
|
||||||
import { EpisodeTorrentsButton } from "./torrentsButton";
|
import { EpisodeTorrentsButton } from "./torrentsButton";
|
||||||
|
|
||||||
export const Episode = (props) => {
|
export const Episode = ({ season, episode }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const trackedSeason = useSelector((state) =>
|
const imdbId = useSelector((state) => state.show.show.imdb_id);
|
||||||
state.showStore.getIn(["show", "tracked_season"], null)
|
const showTitle = useSelector((state) => state.show.show.title);
|
||||||
);
|
|
||||||
const trackedEpisode = useSelector((state) =>
|
const data = useSelector((state) =>
|
||||||
state.showStore.getIn(["show", "tracked_episode"], null)
|
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 (
|
return (
|
||||||
<div className="d-flex flex-column flex-lg-row mb-3 pb-3 border-bottom border-light">
|
<div className="d-flex flex-column flex-lg-row mb-3 pb-3 border-bottom border-light">
|
||||||
<EpisodeThumb url={props.data.get("thumb")} />
|
<EpisodeThumb season={season} episode={episode} />
|
||||||
<div className="d-flex flex-column">
|
<div className="d-flex flex-column">
|
||||||
<Title
|
<Title
|
||||||
title={`${props.data.get("episode")}. ${props.data.get("title")}`}
|
title={`${episode}. ${data.title}`}
|
||||||
wishlisted={isEpisodeWishlisted(
|
wishlisted={isWishlisted}
|
||||||
props.data,
|
|
||||||
trackedSeason,
|
|
||||||
trackedEpisode
|
|
||||||
)}
|
|
||||||
wishlist={() =>
|
wishlist={() =>
|
||||||
dispatch(
|
dispatch(showWishlistToggle(isWishlisted, imdbId, season, episode))
|
||||||
showWishlistToggle(
|
|
||||||
isEpisodeWishlisted(props.data),
|
|
||||||
props.data.get("show_imdb_id"),
|
|
||||||
props.data.get("season"),
|
|
||||||
props.data.get("episode")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ReleaseDate date={props.data.get("aired")} />
|
<ReleaseDate date={data.aired} />
|
||||||
<Runtime runtime={props.data.get("runtime")} />
|
<Runtime runtime={data.runtime} />
|
||||||
<Plot plot={props.data.get("plot")} />
|
<Plot plot={data.plot} />
|
||||||
<DownloadAndStream
|
<DownloadAndStream
|
||||||
name={prettyEpisodeName(
|
name={prettyEpisodeName(showTitle, season, episode)}
|
||||||
props.showName,
|
url={data.polochon_url}
|
||||||
props.data.get("season"),
|
subtitles={data.subtitles}
|
||||||
props.data.get("episode")
|
|
||||||
)}
|
|
||||||
url={props.data.get("polochon_url")}
|
|
||||||
subtitles={props.data.get("subtitles")}
|
|
||||||
/>
|
/>
|
||||||
<PolochonMetadata
|
<PolochonMetadata
|
||||||
quality={props.data.get("quality")}
|
quality={data.quality}
|
||||||
releaseGroup={props.data.get("release_group")}
|
releaseGroup={data.release_group}
|
||||||
container={props.data.get("container")}
|
container={data.container}
|
||||||
audioCodec={props.data.get("audio_codec")}
|
audioCodec={data.audio_codec}
|
||||||
videoCodec={props.data.get("video_codec")}
|
videoCodec={data.video_codec}
|
||||||
light
|
light
|
||||||
/>
|
/>
|
||||||
<ShowMore
|
<ShowMore
|
||||||
id={prettyEpisodeName(
|
id={prettyEpisodeName(showTitle, season, episode)}
|
||||||
props.showName,
|
inLibrary={data.polochon_url !== ""}
|
||||||
props.data.get("season"),
|
|
||||||
props.data.get("episode")
|
|
||||||
)}
|
|
||||||
inLibrary={inLibrary(props.data)}
|
|
||||||
>
|
>
|
||||||
<EpisodeTorrentsButton
|
<EpisodeTorrentsButton season={season} episode={episode} />
|
||||||
torrents={props.data.get("torrents")}
|
<EpisodeSubtitlesButton season={season} episode={episode} />
|
||||||
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")}
|
|
||||||
/>
|
|
||||||
</ShowMore>
|
</ShowMore>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
Episode.propTypes = {
|
Episode.propTypes = {
|
||||||
data: PropTypes.instanceOf(Map).isRequired,
|
season: PropTypes.number.isRequired,
|
||||||
showName: PropTypes.string.isRequired,
|
episode: PropTypes.number.isRequired,
|
||||||
};
|
};
|
||||||
|
@ -1,15 +1,23 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import PropTypes from "prop-types";
|
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 === "") {
|
if (url === "") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mr-0 mr-lg-2 mb-2 mb-lg-0 episode-thumb">
|
<div className="mr-0 mr-lg-2 mb-2 mb-lg-0 episode-thumb">
|
||||||
<img src={url} />
|
<img src={url} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
EpisodeThumb.propTypes = { url: PropTypes.string };
|
EpisodeThumb.propTypes = {
|
||||||
EpisodeThumb.defaultProps = { url: "" };
|
season: PropTypes.number.isRequired,
|
||||||
|
episode: PropTypes.number.isRequired,
|
||||||
|
};
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import PropTypes from "prop-types";
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
export const Fanart = ({ url }) => (
|
export const Fanart = () => {
|
||||||
|
const url = useSelector((state) => state.show.show.fanart_url);
|
||||||
|
return (
|
||||||
<div className="show-fanart mx-n3 mt-n1">
|
<div className="show-fanart mx-n3 mt-n1">
|
||||||
<img
|
<img
|
||||||
className="img w-100 object-fit-cover object-position-top mh-40vh"
|
className="img w-100 object-fit-cover object-position-top mh-40vh"
|
||||||
@ -9,6 +11,4 @@ export const Fanart = ({ url }) => (
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
Fanart.propTypes = {
|
|
||||||
url: PropTypes.string,
|
|
||||||
};
|
};
|
||||||
|
@ -1,11 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import PropTypes from "prop-types";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { Map } from "immutable";
|
|
||||||
import { useDispatch } from "react-redux";
|
|
||||||
|
|
||||||
import { isWishlisted } from "../../../utils";
|
|
||||||
|
|
||||||
import { showWishlistToggle } from "../../../actions/shows";
|
|
||||||
|
|
||||||
import { Plot } from "../../details/plot";
|
import { Plot } from "../../details/plot";
|
||||||
import { Rating } from "../../details/rating";
|
import { Rating } from "../../details/rating";
|
||||||
@ -15,51 +9,62 @@ import { TrackingLabel } from "../../details/tracking";
|
|||||||
|
|
||||||
import { ImdbBadge } from "../../buttons/imdb";
|
import { ImdbBadge } from "../../buttons/imdb";
|
||||||
|
|
||||||
export const Header = (props) => {
|
import { showWishlistToggle } from "../../../actions/shows";
|
||||||
|
|
||||||
|
export const Header = () => {
|
||||||
const dispatch = useDispatch();
|
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 (
|
return (
|
||||||
<div className="card col-12 col-md-10 offset-md-1 mt-n3 mb-3">
|
<div className="card col-12 col-md-10 offset-md-1 mt-n3 mb-3">
|
||||||
<div className="d-flex flex-column flex-md-row">
|
<div className="d-flex flex-column flex-md-row">
|
||||||
<div className="d-flex justify-content-center">
|
<div className="d-flex justify-content-center">
|
||||||
<img
|
<img className="overflow-hidden object-fit-cover" src={posterUrl} />
|
||||||
className="overflow-hidden object-fit-cover"
|
|
||||||
src={props.data.get("poster_url")}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<p className="card-title">
|
<p className="card-title">
|
||||||
<Title
|
<Title
|
||||||
title={props.data.get("title")}
|
title={title}
|
||||||
wishlisted={isWishlisted(props.data)}
|
wishlisted={wishlisted}
|
||||||
wishlist={() =>
|
wishlist={() =>
|
||||||
dispatch(
|
dispatch(showWishlistToggle(wishlisted, imdbId))
|
||||||
showWishlistToggle(
|
|
||||||
isWishlisted(props.data),
|
|
||||||
props.data.get("imdb_id")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
<p className="card-text">
|
<p className="card-text">
|
||||||
<ReleaseDate date={props.data.get("year")} />
|
<ReleaseDate date={year} />
|
||||||
</p>
|
</p>
|
||||||
<p className="card-text">
|
<p className="card-text">
|
||||||
<Rating rating={props.data.get("rating")} />
|
<Rating rating={rating} />
|
||||||
</p>
|
</p>
|
||||||
<p className="card-text">
|
<p className="card-text">
|
||||||
<ImdbBadge imdbId={props.data.get("imdb_id")} />
|
<ImdbBadge imdbId={imdbId} />
|
||||||
</p>
|
</p>
|
||||||
<p className="card-text">
|
<p className="card-text">
|
||||||
<TrackingLabel
|
<TrackingLabel
|
||||||
inLibrary={false}
|
inLibrary={false}
|
||||||
trackedSeason={props.data.get("tracked_season")}
|
trackedSeason={trackedSeason}
|
||||||
trackedEpisode={props.data.get("tracked_episode")}
|
trackedEpisode={trackedEpisode}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
<p className="card-text">
|
<p className="card-text">
|
||||||
<Plot plot={props.data.get("plot")} />
|
<Plot plot={plot} />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -67,6 +72,3 @@ export const Header = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
Header.propTypes = {
|
|
||||||
data: PropTypes.instanceOf(Map),
|
|
||||||
};
|
|
||||||
|
@ -1,20 +1,28 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { Map } from "immutable";
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
import { Episode } from "./episode";
|
import { Episode } from "./episode";
|
||||||
|
|
||||||
export const Season = (props) => {
|
export const Season = ({ season }) => {
|
||||||
const [show, setShow] = useState(false);
|
const [show, setShow] = useState(false);
|
||||||
|
|
||||||
|
const episodeNumbers = useSelector((state) =>
|
||||||
|
Array.from(state.show.show.seasons.get(season).keys())
|
||||||
|
);
|
||||||
|
|
||||||
|
const showToggle = () => {
|
||||||
|
setShow(!show);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card mb-3 show-season">
|
<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">
|
<h5 className="m-0">
|
||||||
Season {props.season}
|
Season {season}
|
||||||
<small className="text-primary">
|
<small className="text-primary">
|
||||||
{" "}
|
{" "}
|
||||||
— ({props.data.toList().size} episodes)
|
— ({episodeNumbers.length} episodes)
|
||||||
</small>
|
</small>
|
||||||
<i
|
<i
|
||||||
className={`float-right fa fa-chevron-${show ? "down" : "left"}`}
|
className={`float-right fa fa-chevron-${show ? "down" : "left"}`}
|
||||||
@ -22,30 +30,17 @@ export const Season = (props) => {
|
|||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div className={`card-body ${show ? "d-flex flex-column" : "d-none"}`}>
|
<div className={`card-body ${show ? "d-flex flex-column" : "d-none"}`}>
|
||||||
{props.data.toList().map(function (episode) {
|
{episodeNumbers.map((episode) => (
|
||||||
let key = `${episode.get("season")}-${episode.get("episode")}`;
|
|
||||||
return (
|
|
||||||
<Episode
|
<Episode
|
||||||
key={key}
|
key={`${season}-${episode}`}
|
||||||
data={episode}
|
season={season}
|
||||||
showName={props.showName}
|
episode={episode}
|
||||||
addTorrent={props.addTorrent}
|
|
||||||
addToWishlist={props.addToWishlist}
|
|
||||||
getEpisodeDetails={props.getEpisodeDetails}
|
|
||||||
refreshSubtitles={props.refreshSubtitles}
|
|
||||||
/>
|
/>
|
||||||
);
|
))}
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
Season.propTypes = {
|
Season.propTypes = {
|
||||||
data: PropTypes.instanceOf(Map),
|
season: PropTypes.number.isRequired,
|
||||||
season: PropTypes.number,
|
|
||||||
showName: PropTypes.string,
|
|
||||||
addToWishlist: PropTypes.func,
|
|
||||||
addTorrent: PropTypes.func,
|
|
||||||
refreshSubtitles: PropTypes.func,
|
|
||||||
getEpisodeDetails: PropTypes.func,
|
|
||||||
};
|
};
|
||||||
|
@ -1,37 +1,19 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import PropTypes from "prop-types";
|
import { useSelector } from "react-redux";
|
||||||
import { Map } from "immutable";
|
|
||||||
|
|
||||||
import { Season } from "./season";
|
import { Season } from "./season";
|
||||||
|
|
||||||
export const SeasonsList = (props) => (
|
export const SeasonsList = () => {
|
||||||
<div className="col col-12 col-md-10 offset-md-1">
|
const seasonNumbers = useSelector((state) => {
|
||||||
{props.data
|
const keys = state.show.show.seasons ? state.show.show.seasons.keys() : [];
|
||||||
.get("seasons")
|
return Array.from(keys);
|
||||||
.entrySeq()
|
});
|
||||||
.map(function ([season, data]) {
|
|
||||||
if (season === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<Season
|
<div className="col col-12 col-md-10 offset-md-1">
|
||||||
key={`season-list-key-${season}`}
|
{seasonNumbers.map((season) => (
|
||||||
data={data}
|
<Season key={`season-list-key-${season}`} season={season} />
|
||||||
season={season}
|
))}
|
||||||
showName={props.data.get("title")}
|
|
||||||
addTorrent={props.addTorrent}
|
|
||||||
addToWishlist={props.addToWishlist}
|
|
||||||
getEpisodeDetails={props.getEpisodeDetails}
|
|
||||||
refreshSubtitles={props.refreshSubtitles}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
SeasonsList.propTypes = {
|
|
||||||
data: PropTypes.instanceOf(Map),
|
|
||||||
addToWishlist: PropTypes.func,
|
|
||||||
addTorrent: PropTypes.func,
|
|
||||||
refreshSubtitles: PropTypes.func,
|
|
||||||
getEpisodeDetails: PropTypes.func,
|
|
||||||
};
|
};
|
||||||
|
@ -1,37 +1,45 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { List } from "immutable";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { useDispatch } from "react-redux";
|
|
||||||
|
|
||||||
import { searchEpisodeSubtitles } from "../../../actions/subtitles";
|
import { searchEpisodeSubtitles } from "../../../actions/subtitles";
|
||||||
|
|
||||||
import { SubtitlesButton } from "../../buttons/subtitles";
|
import { SubtitlesButton } from "../../buttons/subtitles";
|
||||||
|
|
||||||
export const EpisodeSubtitlesButton = ({
|
export const EpisodeSubtitlesButton = ({ season, episode }) => {
|
||||||
inLibrary,
|
|
||||||
imdbId,
|
|
||||||
season,
|
|
||||||
episode,
|
|
||||||
searching,
|
|
||||||
subtitles,
|
|
||||||
}) => {
|
|
||||||
const dispatch = useDispatch();
|
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 (
|
return (
|
||||||
<SubtitlesButton
|
<SubtitlesButton
|
||||||
subtitles={subtitles}
|
subtitles={subtitles}
|
||||||
inLibrary={inLibrary}
|
inLibrary={inLibrary}
|
||||||
searching={searching}
|
searching={searching}
|
||||||
search={() => dispatch(searchEpisodeSubtitles(imdbId, season, episode))}
|
search={search}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
EpisodeSubtitlesButton.propTypes = {
|
EpisodeSubtitlesButton.propTypes = {
|
||||||
inLibrary: PropTypes.bool,
|
season: PropTypes.number.isRequired,
|
||||||
searching: PropTypes.bool,
|
episode: PropTypes.number.isRequired,
|
||||||
imdbId: PropTypes.string,
|
|
||||||
season: PropTypes.number,
|
|
||||||
episode: PropTypes.number,
|
|
||||||
subtitles: PropTypes.instanceOf(List),
|
|
||||||
};
|
};
|
||||||
|
@ -1,39 +1,46 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { useDispatch } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { List } from "immutable";
|
|
||||||
|
|
||||||
import { getEpisodeDetails } from "../../../actions/shows";
|
import { getEpisodeDetails } from "../../../actions/shows";
|
||||||
|
|
||||||
import { TorrentsButton } from "../../buttons/torrents";
|
import { TorrentsButton } from "../../buttons/torrents";
|
||||||
import { prettyEpisodeName } from "../../../utils";
|
import { prettyEpisodeName } from "../../../utils";
|
||||||
|
|
||||||
export const EpisodeTorrentsButton = ({
|
export const EpisodeTorrentsButton = ({ season, episode }) => {
|
||||||
torrents,
|
|
||||||
imdbId,
|
|
||||||
season,
|
|
||||||
episode,
|
|
||||||
showName,
|
|
||||||
searching,
|
|
||||||
}) => {
|
|
||||||
const dispatch = useDispatch();
|
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 (
|
return (
|
||||||
<TorrentsButton
|
<TorrentsButton
|
||||||
torrents={torrents}
|
torrents={torrents}
|
||||||
searching={searching}
|
searching={searching}
|
||||||
search={() => dispatch(getEpisodeDetails(imdbId, season, episode))}
|
search={search}
|
||||||
url={`#/torrents/search/shows/${encodeURI(
|
url={url}
|
||||||
prettyEpisodeName(showName, season, episode)
|
|
||||||
)}`}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
EpisodeTorrentsButton.propTypes = {
|
EpisodeTorrentsButton.propTypes = {
|
||||||
torrents: PropTypes.instanceOf(List),
|
|
||||||
showName: PropTypes.string.isRequired,
|
|
||||||
imdbId: PropTypes.string.isRequired,
|
|
||||||
episode: PropTypes.number.isRequired,
|
episode: PropTypes.number.isRequired,
|
||||||
season: PropTypes.number.isRequired,
|
season: PropTypes.number.isRequired,
|
||||||
searching: PropTypes.bool.isRequired,
|
|
||||||
};
|
};
|
||||||
|
@ -1,18 +1,14 @@
|
|||||||
import React, { useEffect, useCallback } from "react";
|
import React, { useEffect, useCallback } from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { Map } from "immutable";
|
|
||||||
import { useSelector, useDispatch } from "react-redux";
|
import { useSelector, useDispatch } from "react-redux";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
fetchShows,
|
fetchShows,
|
||||||
selectShow,
|
selectShow,
|
||||||
showWishlistToggle,
|
showWishlistToggle,
|
||||||
updateFilter,
|
|
||||||
getShowExploreOptions,
|
getShowExploreOptions,
|
||||||
} from "../../actions/shows";
|
} from "../../actions/shows";
|
||||||
|
|
||||||
import { isWishlisted } from "../../utils";
|
|
||||||
|
|
||||||
import ListDetails from "../list/details";
|
import ListDetails from "../list/details";
|
||||||
import ListPosters from "../list/posters";
|
import ListPosters from "../list/posters";
|
||||||
|
|
||||||
@ -46,34 +42,30 @@ const ShowList = ({ match, history }) => {
|
|||||||
}
|
}
|
||||||
}, [dispatch, match]);
|
}, [dispatch, match]);
|
||||||
|
|
||||||
const loading = useSelector((state) => state.showsStore.get("loading"));
|
const loading = useSelector((state) => state.shows.loading);
|
||||||
const shows = useSelector((state) => state.showsStore.get("shows"));
|
const shows = useSelector((state) => state.shows.shows);
|
||||||
const filter = useSelector((state) => state.showsStore.get("filter"));
|
const selectedImdbId = useSelector((state) => state.shows.selectedImdbId);
|
||||||
const selectedImdbId = useSelector((state) =>
|
const exploreOptions = useSelector((state) => state.shows.exploreOptions);
|
||||||
state.showsStore.get("selectedImdbId")
|
|
||||||
);
|
|
||||||
const exploreOptions = useSelector((state) =>
|
|
||||||
state.showsStore.get("exploreOptions")
|
|
||||||
);
|
|
||||||
|
|
||||||
const showDetails = (imdbId) => {
|
const showDetails = (imdbId) => {
|
||||||
history.push("/shows/details/" + imdbId);
|
history.push("/shows/details/" + imdbId);
|
||||||
};
|
};
|
||||||
|
|
||||||
let selectedShow = Map();
|
let selectedShow = new Map();
|
||||||
if (selectedImdbId !== "") {
|
if (selectedImdbId !== "") {
|
||||||
selectedShow = shows.get(selectedImdbId);
|
selectedShow = shows.get(selectedImdbId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectFunc = useCallback((id) => dispatch(selectShow(id)), [dispatch]);
|
const selectFunc = useCallback((id) => dispatch(selectShow(id)), [dispatch]);
|
||||||
const filterFunc = useCallback((filter) => dispatch(updateFilter(filter)), [
|
|
||||||
dispatch,
|
|
||||||
]);
|
|
||||||
const exploreFetchOptions = useCallback(
|
const exploreFetchOptions = useCallback(
|
||||||
() => dispatch(getShowExploreOptions()),
|
() => dispatch(getShowExploreOptions()),
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const wishlisted =
|
||||||
|
selectedShow.tracked_season !== null &&
|
||||||
|
selectedShow.tracked_episode !== null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="row" id="container">
|
<div className="row" id="container">
|
||||||
<ListPosters
|
<ListPosters
|
||||||
@ -82,9 +74,7 @@ const ShowList = ({ match, history }) => {
|
|||||||
placeHolder="Filter shows..."
|
placeHolder="Filter shows..."
|
||||||
exploreOptions={exploreOptions}
|
exploreOptions={exploreOptions}
|
||||||
exploreFetchOptions={exploreFetchOptions}
|
exploreFetchOptions={exploreFetchOptions}
|
||||||
updateFilter={filterFunc}
|
|
||||||
selectedImdbId={selectedImdbId}
|
selectedImdbId={selectedImdbId}
|
||||||
filter={filter}
|
|
||||||
onClick={selectFunc}
|
onClick={selectFunc}
|
||||||
onDoubleClick={showDetails}
|
onDoubleClick={showDetails}
|
||||||
onKeyEnter={showDetails}
|
onKeyEnter={showDetails}
|
||||||
@ -94,18 +84,14 @@ const ShowList = ({ match, history }) => {
|
|||||||
<ListDetails
|
<ListDetails
|
||||||
data={selectedShow}
|
data={selectedShow}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
wishlisted={wishlisted}
|
||||||
wishlist={() =>
|
wishlist={() =>
|
||||||
dispatch(
|
dispatch(showWishlistToggle(wishlisted, selectedShow.imdb_id))
|
||||||
showWishlistToggle(
|
|
||||||
isWishlisted(selectedShow),
|
|
||||||
selectedShow.get("imdb_id")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
<button
|
<button
|
||||||
onClick={() => showDetails(selectedShow.get("imdb_id"))}
|
onClick={() => showDetails(selectedShow.imdb_id)}
|
||||||
className="btn btn-primary btn-sm w-md-100"
|
className="btn btn-primary btn-sm w-md-100"
|
||||||
>
|
>
|
||||||
<i className="fa fa-external-link mr-1" />
|
<i className="fa fa-external-link mr-1" />
|
||||||
|
@ -1,30 +1,23 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { Map, List } from "immutable";
|
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
import { prettySize } from "../../utils";
|
import { prettySize } from "../../utils";
|
||||||
import { addTorrent, removeTorrent } from "../../actions/torrents";
|
import { addTorrent, removeTorrent } from "../../actions/torrents";
|
||||||
|
|
||||||
const TorrentList = () => {
|
export const TorrentList = () => {
|
||||||
const torrents = useSelector((state) => state.torrentStore.get("torrents"));
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<AddTorrent addTorrent={(url) => dispatch(addTorrent(url))} />
|
<AddTorrent />
|
||||||
<Torrents
|
<Torrents />
|
||||||
torrents={torrents}
|
|
||||||
removeTorrent={(id) => dispatch(removeTorrent(id))}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default TorrentList;
|
|
||||||
|
|
||||||
const AddTorrent = (props) => {
|
const AddTorrent = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
const [url, setUrl] = useState("");
|
const [url, setUrl] = useState("");
|
||||||
|
|
||||||
const handleSubmit = (e) => {
|
const handleSubmit = (e) => {
|
||||||
@ -32,7 +25,7 @@ const AddTorrent = (props) => {
|
|||||||
if (url === "") {
|
if (url === "") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
props.addTorrent(url);
|
dispatch(addTorrent(url));
|
||||||
setUrl("");
|
setUrl("");
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -49,12 +42,11 @@ const AddTorrent = (props) => {
|
|||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
AddTorrent.propTypes = {
|
|
||||||
addTorrent: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
const Torrents = (props) => {
|
const Torrents = () => {
|
||||||
if (props.torrents.size === 0) {
|
const torrents = useSelector((state) => state.torrents.torrents);
|
||||||
|
|
||||||
|
if (torrents.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="jumbotron">
|
<div className="jumbotron">
|
||||||
<h2>No torrents</h2>
|
<h2>No torrents</h2>
|
||||||
@ -64,45 +56,38 @@ const Torrents = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="d-flex flex-wrap">
|
<div className="d-flex flex-wrap">
|
||||||
{props.torrents.map((el, index) => (
|
{torrents.map((torrent, index) => (
|
||||||
<Torrent key={index} data={el} removeTorrent={props.removeTorrent} />
|
<Torrent key={index} torrent={torrent} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
Torrents.propTypes = {
|
|
||||||
removeTorrent: PropTypes.func.isRequired,
|
|
||||||
torrents: PropTypes.instanceOf(List),
|
|
||||||
};
|
|
||||||
|
|
||||||
const Torrent = (props) => {
|
const Torrent = ({ torrent }) => {
|
||||||
const handleClick = () => {
|
const dispatch = useDispatch();
|
||||||
props.removeTorrent(props.data.get("id"));
|
|
||||||
};
|
|
||||||
|
|
||||||
const done = props.data.get("is_finished");
|
var progressStyle = torrent.is_finished
|
||||||
var progressStyle = done
|
|
||||||
? "success"
|
? "success"
|
||||||
: "info progress-bar-striped progress-bar-animated";
|
: "info progress-bar-striped progress-bar-animated";
|
||||||
const progressBarClass = "progress-bar bg-" + progressStyle;
|
const progressBarClass = "progress-bar bg-" + progressStyle;
|
||||||
|
|
||||||
var percentDone = props.data.get("percent_done");
|
var percentDone = torrent.percent_done;
|
||||||
const started = percentDone !== 0;
|
const started = percentDone !== 0;
|
||||||
if (started) {
|
if (started) {
|
||||||
percentDone = Number(percentDone).toFixed(1) + "%";
|
percentDone = Number(percentDone).toFixed(1) + "%";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pretty sizes
|
// Pretty sizes
|
||||||
const downloadedSize = prettySize(props.data.get("downloaded_size"));
|
const downloadedSize = prettySize(torrent.downloaded_size);
|
||||||
const totalSize = prettySize(props.data.get("total_size"));
|
const totalSize = prettySize(torrent.total_size);
|
||||||
const downloadRate = prettySize(props.data.get("download_rate")) + "/s";
|
const downloadRate = prettySize(torrent.download_rate) + "/s";
|
||||||
return (
|
return (
|
||||||
<div className="card w-100 mb-3">
|
<div className="card w-100 mb-3">
|
||||||
<h5 className="card-header">
|
<h5 className="card-header">
|
||||||
<span className="text text-break">{props.data.get("name")}</span>
|
<span className="text text-break">{torrent.name}</span>
|
||||||
<span
|
<span
|
||||||
className="fa fa-trash clickable pull-right"
|
className="fa fa-trash clickable pull-right"
|
||||||
onClick={() => handleClick()}
|
onClick={() => dispatch(removeTorrent(torrent.id))}
|
||||||
></span>
|
></span>
|
||||||
</h5>
|
</h5>
|
||||||
<div className="card-body pb-0">
|
<div className="card-body pb-0">
|
||||||
@ -129,6 +114,5 @@ const Torrent = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
Torrent.propTypes = {
|
Torrent.propTypes = {
|
||||||
removeTorrent: PropTypes.func.isRequired,
|
torrent: PropTypes.object.isRequired,
|
||||||
data: PropTypes.instanceOf(Map),
|
|
||||||
};
|
};
|
||||||
|
@ -2,21 +2,15 @@ import React, { useState, useEffect, useCallback } from "react";
|
|||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { addTorrent, searchTorrents } from "../../actions/torrents";
|
import { addTorrent, searchTorrents } from "../../actions/torrents";
|
||||||
import { Map, List } from "immutable";
|
|
||||||
import Loader from "../loader/loader";
|
import Loader from "../loader/loader";
|
||||||
|
|
||||||
import { OverlayTrigger, Tooltip } from "react-bootstrap";
|
import { OverlayTrigger, Tooltip } from "react-bootstrap";
|
||||||
|
|
||||||
import { prettySize } from "../../utils";
|
import { prettySize } from "../../utils";
|
||||||
|
|
||||||
const TorrentSearch = ({ history, match }) => {
|
export const TorrentSearch = ({ history, match }) => {
|
||||||
const dispatch = useDispatch();
|
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 [search, setSearch] = useState(match.params.search || "");
|
||||||
const [type, setType] = useState(match.params.type || "");
|
const [type, setType] = useState(match.params.type || "");
|
||||||
|
|
||||||
@ -74,18 +68,13 @@ const TorrentSearch = ({ history, match }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<TorrentList
|
<TorrentList search={search} />
|
||||||
searching={searching}
|
|
||||||
results={results}
|
|
||||||
addTorrent={(url) => dispatch(addTorrent(url))}
|
|
||||||
searchFromURL={search}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
TorrentSearch.propTypes = {
|
TorrentSearch.propTypes = {
|
||||||
searchFromURL: PropTypes.string,
|
search: PropTypes.string,
|
||||||
match: PropTypes.object,
|
match: PropTypes.object,
|
||||||
history: PropTypes.object,
|
history: PropTypes.object,
|
||||||
};
|
};
|
||||||
@ -109,16 +98,19 @@ SearchButton.propTypes = {
|
|||||||
handleClick: PropTypes.func.isRequired,
|
handleClick: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
const TorrentList = (props) => {
|
const TorrentList = ({ search }) => {
|
||||||
if (props.searching) {
|
const searching = useSelector((state) => state.torrents.searching);
|
||||||
|
const results = useSelector((state) => state.torrents.searchResults);
|
||||||
|
|
||||||
|
if (searching) {
|
||||||
return <Loader />;
|
return <Loader />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.searchFromURL === "") {
|
if (search === "") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.results.size === 0) {
|
if (results.size === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="jumbotron">
|
<div className="jumbotron">
|
||||||
<h2>No results</h2>
|
<h2>No results</h2>
|
||||||
@ -128,63 +120,60 @@ const TorrentList = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{props.results.map(function (el, index) {
|
{results.map(function (el, index) {
|
||||||
return <Torrent key={index} data={el} addTorrent={props.addTorrent} />;
|
return <Torrent key={index} torrent={el} />;
|
||||||
})}
|
})}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
TorrentList.propTypes = {
|
TorrentList.propTypes = {
|
||||||
searching: PropTypes.bool.isRequired,
|
search: PropTypes.string,
|
||||||
results: PropTypes.instanceOf(List),
|
|
||||||
searchFromURL: PropTypes.string,
|
|
||||||
addTorrent: PropTypes.func.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const Torrent = (props) => (
|
const Torrent = ({ torrent }) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
return (
|
||||||
<div className="alert d-flex border-bottom border-secondary align-items-center">
|
<div className="alert d-flex border-bottom border-secondary align-items-center">
|
||||||
<TorrentHealth
|
<TorrentHealth
|
||||||
url={props.data.get("url")}
|
url={torrent.url}
|
||||||
seeders={props.data.get("seeders")}
|
seeders={torrent.seeders}
|
||||||
leechers={props.data.get("leechers")}
|
leechers={torrent.leechers}
|
||||||
/>
|
/>
|
||||||
<span className="mx-3 text text-start text-break flex-fill">
|
<span className="mx-3 text text-start text-break flex-fill">
|
||||||
{props.data.get("name")}
|
{torrent.name}
|
||||||
</span>
|
</span>
|
||||||
<div>
|
<div>
|
||||||
{props.data.get("size") !== 0 && (
|
{torrent.size !== 0 && (
|
||||||
<span className="mx-1 badge badge-pill badge-warning">
|
<span className="mx-1 badge badge-pill badge-warning">
|
||||||
{prettySize(props.data.get("size"))}
|
{prettySize(torrent.size)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<span className="mx-1 badge badge-pill badge-warning">
|
<span className="mx-1 badge badge-pill badge-warning">
|
||||||
{props.data.get("quality")}
|
{torrent.quality}
|
||||||
</span>
|
</span>
|
||||||
<span className="mx-1 badge badge-pill badge-success">
|
<span className="mx-1 badge badge-pill badge-success">
|
||||||
{props.data.get("source")}
|
{torrent.source}
|
||||||
</span>
|
</span>
|
||||||
<span className="mx-1 badge badge-pill badge-info">
|
<span className="mx-1 badge badge-pill badge-info">
|
||||||
{props.data.get("upload_user")}
|
{torrent.upload_user}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="align-self-end ml-3">
|
<div className="align-self-end ml-3">
|
||||||
<i
|
<i
|
||||||
className="fa fa-cloud-download clickable"
|
className="fa fa-cloud-download clickable"
|
||||||
onClick={() => props.addTorrent(props.data.get("url"))}
|
onClick={() => dispatch(addTorrent(torrent.url))}
|
||||||
></i>
|
></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
Torrent.propTypes = {
|
Torrent.propTypes = {
|
||||||
data: PropTypes.instanceOf(Map),
|
torrent: PropTypes.object.isRequired,
|
||||||
addTorrent: PropTypes.func.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const TorrentHealth = (props) => {
|
const TorrentHealth = ({ url, seeders = 0, leechers = 1 }) => {
|
||||||
const seeders = props.seeders || 0;
|
|
||||||
const leechers = props.leechers || 1;
|
|
||||||
|
|
||||||
let color;
|
let color;
|
||||||
let health;
|
let health;
|
||||||
let ratio = seeders / leechers;
|
let ratio = seeders / leechers;
|
||||||
@ -204,12 +193,12 @@ const TorrentHealth = (props) => {
|
|||||||
|
|
||||||
const className = `align-self-start text text-center text-${color}`;
|
const className = `align-self-start text text-center text-${color}`;
|
||||||
const tooltip = (
|
const tooltip = (
|
||||||
<Tooltip id={`tooltip-health-${props.url}`}>
|
<Tooltip id={`tooltip-health-${url}`}>
|
||||||
<p>
|
<p>
|
||||||
<span className={className}>Health: {health}</span>
|
<span className={className}>Health: {health}</span>
|
||||||
</p>
|
</p>
|
||||||
<p>Seeders: {seeders}</p>
|
<p>Seeders: {seeders}</p>
|
||||||
<p>Leechers: {props.leechers}</p>
|
<p>Leechers: {leechers}</p>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -226,5 +215,3 @@ TorrentHealth.propTypes = {
|
|||||||
seeders: PropTypes.number,
|
seeders: PropTypes.number,
|
||||||
leechers: PropTypes.number,
|
leechers: PropTypes.number,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TorrentSearch;
|
|
||||||
|
@ -2,11 +2,9 @@ import React from "react";
|
|||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { Redirect, Link } from "react-router-dom";
|
import { Redirect, Link } from "react-router-dom";
|
||||||
|
|
||||||
const UserActivation = () => {
|
export const UserActivation = () => {
|
||||||
const isLogged = useSelector((state) => state.userStore.get("isLogged"));
|
const isLogged = useSelector((state) => state.user.isLogged);
|
||||||
const isActivated = useSelector((state) =>
|
const isActivated = useSelector((state) => state.user.isActivated);
|
||||||
state.userStore.get("isActivated")
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isLogged) {
|
if (!isLogged) {
|
||||||
return <Redirect to="/users/login" />;
|
return <Redirect to="/users/login" />;
|
||||||
@ -30,4 +28,3 @@ const UserActivation = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default UserActivation;
|
|
||||||
|
@ -14,11 +14,10 @@ export const UserEdit = () => {
|
|||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [passwordConfirm, setPasswordConfirm] = useState("");
|
const [passwordConfirm, setPasswordConfirm] = useState("");
|
||||||
|
|
||||||
const loading = useSelector((state) => state.userStore.get("loading"));
|
const loading = useSelector((state) => state.user.loading);
|
||||||
const publicPolochons = useSelector((state) => state.polochon.get("public"));
|
const polochonId = useSelector((state) => state.user.polochonId);
|
||||||
const polochonId = useSelector((state) => state.userStore.get("polochonId"));
|
const polochonActivated = useSelector(
|
||||||
const polochonActivated = useSelector((state) =>
|
(state) => state.user.polochonActivated
|
||||||
state.userStore.get("polochonActivated")
|
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -60,11 +59,7 @@ export const UserEdit = () => {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</label>
|
</label>
|
||||||
<PolochonSelect
|
<PolochonSelect value={id} changeValue={setId} />
|
||||||
value={id}
|
|
||||||
changeValue={setId}
|
|
||||||
polochonList={publicPolochons}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
@ -4,12 +4,12 @@ import { Redirect, Link } from "react-router-dom";
|
|||||||
|
|
||||||
import { loginUser } from "../../actions/users";
|
import { loginUser } from "../../actions/users";
|
||||||
|
|
||||||
const UserLoginForm = () => {
|
export const UserLoginForm = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const isLogged = useSelector((state) => state.userStore.get("isLogged"));
|
const isLogged = useSelector((state) => state.user.isLogged);
|
||||||
const isLoading = useSelector((state) => state.userStore.get("loading"));
|
const isLoading = useSelector((state) => state.user.loading);
|
||||||
const error = useSelector((state) => state.userStore.get("error"));
|
const error = useSelector((state) => state.user.error);
|
||||||
|
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
@ -85,5 +85,3 @@ const UserLoginForm = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default UserLoginForm;
|
|
||||||
|
@ -4,15 +4,14 @@ import { Redirect } from "react-router-dom";
|
|||||||
|
|
||||||
import { userLogout } from "../../actions/users";
|
import { userLogout } from "../../actions/users";
|
||||||
|
|
||||||
const UserLogout = () => {
|
export const UserLogout = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const isLogged = useSelector((state) => state.userStore.get("isLogged"));
|
const isLogged = useSelector((state) => state.user.isLogged);
|
||||||
|
|
||||||
if (isLogged) {
|
if (isLogged) {
|
||||||
dispatch(userLogout());
|
dispatch(userLogout());
|
||||||
}
|
return null;
|
||||||
|
} else {
|
||||||
return <Redirect to="/users/login" />;
|
return <Redirect to="/users/login" />;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default UserLogout;
|
|
||||||
|
@ -7,12 +7,10 @@ import { UserEdit } from "./edit";
|
|||||||
import { getUserModules } from "../../actions/users";
|
import { getUserModules } from "../../actions/users";
|
||||||
import Modules from "../modules/modules";
|
import Modules from "../modules/modules";
|
||||||
|
|
||||||
const UserProfile = () => {
|
export const UserProfile = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const modules = useSelector((state) => state.userStore.get("modules"));
|
const modules = useSelector((state) => state.user.modules);
|
||||||
const modulesLoading = useSelector((state) =>
|
const modulesLoading = useSelector((state) => state.user.modulesLoading);
|
||||||
state.userStore.get("modulesLoading")
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(getUserModules());
|
dispatch(getUserModules());
|
||||||
@ -26,5 +24,3 @@ const UserProfile = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default UserProfile;
|
|
||||||
|
@ -4,16 +4,16 @@ import { Redirect } from "react-router-dom";
|
|||||||
|
|
||||||
import { userSignUp } from "../../actions/users";
|
import { userSignUp } from "../../actions/users";
|
||||||
|
|
||||||
const UserSignUp = () => {
|
export const UserSignUp = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [passwordConfirm, setPasswordConfirm] = useState("");
|
const [passwordConfirm, setPasswordConfirm] = useState("");
|
||||||
|
|
||||||
const isLogged = useSelector((state) => state.userStore.get("isLogged"));
|
const isLogged = useSelector((state) => state.user.isLogged);
|
||||||
const isLoading = useSelector((state) => state.userStore.get("loading"));
|
const isLoading = useSelector((state) => state.user.loading);
|
||||||
const error = useSelector((state) => state.userStore.get("error"));
|
const error = useSelector((state) => state.user.error);
|
||||||
|
|
||||||
if (isLogged) {
|
if (isLogged) {
|
||||||
return <Redirect to="/" />;
|
return <Redirect to="/" />;
|
||||||
@ -90,5 +90,3 @@ const UserSignUp = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default UserSignUp;
|
|
||||||
|
@ -3,14 +3,12 @@ import PropTypes from "prop-types";
|
|||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { UAParser } from "ua-parser-js";
|
import { UAParser } from "ua-parser-js";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { Map } from "immutable";
|
|
||||||
|
|
||||||
import { getUserTokens, deleteUserToken } from "../../actions/users";
|
import { getUserTokens, deleteUserToken } from "../../actions/users";
|
||||||
|
|
||||||
const UserTokens = () => {
|
export const UserTokens = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const tokens = useSelector((state) => state.user.tokens);
|
||||||
const tokens = useSelector((state) => state.userStore.get("tokens"));
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(getUserTokens());
|
dispatch(getUserTokens());
|
||||||
@ -19,34 +17,30 @@ const UserTokens = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
{tokens.map((el, index) => (
|
{tokens.map((token, index) => (
|
||||||
<Token
|
<Token key={index} token={token} />
|
||||||
key={index}
|
|
||||||
data={el}
|
|
||||||
deleteToken={(token) => dispatch(deleteUserToken(token))}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Token = (props) => {
|
const Token = ({ token }) => {
|
||||||
const ua = UAParser(props.data.get("user_agent"));
|
const ua = UAParser(token.user_agent);
|
||||||
return (
|
return (
|
||||||
<div className="card mt-3">
|
<div className="card mt-3">
|
||||||
<div className="card-header">
|
<div className="card-header">
|
||||||
<h4>
|
<h4>
|
||||||
<Logo {...ua} />
|
<Logo {...ua} />
|
||||||
<span className="ml-3">{props.data.get("description")}</span>
|
<span className="ml-3">{token.description}</span>
|
||||||
<Actions {...props} />
|
<Actions token={token.token} />
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div className="card-body row">
|
<div className="card-body row">
|
||||||
<div className="col-12 col-md-6">
|
<div className="col-12 col-md-6">
|
||||||
<p>Last IP: {props.data.get("ip")}</p>
|
<p>Last IP: {token.ip}</p>
|
||||||
<p>Last used: {moment(props.data.get("last_used")).fromNow()}</p>
|
<p>Last used: {moment(token.last_used).fromNow()}</p>
|
||||||
<p>Created: {moment(props.data.get("created_at")).fromNow()}</p>
|
<p>Created: {moment(token.created_at).fromNow()}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-12 col-md-6">
|
<div className="col-12 col-md-6">
|
||||||
<p>
|
<p>
|
||||||
@ -64,12 +58,14 @@ const Token = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
Token.propTypes = {
|
Token.propTypes = {
|
||||||
data: PropTypes.instanceOf(Map).isRequired,
|
token: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Actions = (props) => {
|
const Actions = ({ token }) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
props.deleteToken(props.data.get("token"));
|
dispatch(deleteUserToken(token));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -80,8 +76,7 @@ const Actions = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
Actions.propTypes = {
|
Actions.propTypes = {
|
||||||
data: PropTypes.instanceOf(Map).isRequired,
|
token: PropTypes.string.isRequired,
|
||||||
deleteToken: PropTypes.func.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const Logo = ({ ua, device, browser }) => {
|
const Logo = ({ ua, device, browser }) => {
|
||||||
@ -165,5 +160,3 @@ Browser.propTypes = {
|
|||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
version: PropTypes.string,
|
version: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default UserTokens;
|
|
||||||
|
@ -4,10 +4,10 @@ import { setFetchedTorrents } from "../actions/torrents";
|
|||||||
import { newMovieEvent } from "../actions/movies";
|
import { newMovieEvent } from "../actions/movies";
|
||||||
import { newEpisodeEvent } from "../actions/shows";
|
import { newEpisodeEvent } from "../actions/shows";
|
||||||
|
|
||||||
const WsHandler = () => {
|
export const WsHandler = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const isLogged = useSelector((state) => state.userStore.get("isLogged"));
|
const isLogged = useSelector((state) => state.user.isLogged);
|
||||||
|
|
||||||
const [ws, setWs] = useState(null);
|
const [ws, setWs] = useState(null);
|
||||||
|
|
||||||
@ -107,5 +107,3 @@ const WsHandler = () => {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default WsHandler;
|
|
||||||
|
@ -1,26 +1,39 @@
|
|||||||
import { Map, List, fromJS } from "immutable";
|
import { produce } from "immer";
|
||||||
|
|
||||||
export const defaultState = Map({
|
export const defaultState = {
|
||||||
fetchingModules: false,
|
fetchingModules: false,
|
||||||
users: List(),
|
users: new Map(),
|
||||||
stats: Map({}),
|
stats: {},
|
||||||
modules: Map({}),
|
modules: {},
|
||||||
});
|
|
||||||
|
|
||||||
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,
|
|
||||||
})
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default (state = defaultState, action) =>
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
@ -1,37 +1,33 @@
|
|||||||
import { Map } from "immutable";
|
import { produce } from "immer";
|
||||||
|
|
||||||
const defaultState = Map({
|
const defaultState = {
|
||||||
show: false,
|
show: false,
|
||||||
message: "",
|
message: "",
|
||||||
type: "",
|
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) =>
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
@ -1,23 +1,27 @@
|
|||||||
import { combineReducers } from "redux";
|
import { combineReducers } from "redux";
|
||||||
|
|
||||||
import movieStore from "./movies";
|
// Immer
|
||||||
import showsStore from "./shows";
|
import { enableMapSet } from "immer";
|
||||||
import showStore from "./show";
|
enableMapSet();
|
||||||
import userStore from "./users";
|
|
||||||
|
import movies from "./movies";
|
||||||
|
import shows from "./shows";
|
||||||
|
import show from "./show";
|
||||||
|
import user from "./users";
|
||||||
import alerts from "./alerts";
|
import alerts from "./alerts";
|
||||||
import torrentStore from "./torrents";
|
import torrents from "./torrents";
|
||||||
import adminStore from "./admins";
|
import admin from "./admins";
|
||||||
import polochon from "./polochon";
|
import polochon from "./polochon";
|
||||||
import notifications from "./notifications";
|
import notifications from "./notifications";
|
||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
movieStore,
|
movies,
|
||||||
showsStore,
|
shows,
|
||||||
showStore,
|
show,
|
||||||
userStore,
|
user,
|
||||||
alerts,
|
alerts,
|
||||||
torrentStore,
|
torrents,
|
||||||
adminStore,
|
admin,
|
||||||
polochon,
|
polochon,
|
||||||
notifications,
|
notifications,
|
||||||
});
|
});
|
||||||
|
@ -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,
|
loading: false,
|
||||||
movies: OrderedMap(),
|
movies: new Map(),
|
||||||
filter: "",
|
|
||||||
selectedImdbId: "",
|
selectedImdbId: "",
|
||||||
lastFetchUrl: "",
|
lastFetchUrl: "",
|
||||||
exploreOptions: Map(),
|
exploreOptions: {},
|
||||||
});
|
};
|
||||||
|
|
||||||
const handlers = {
|
const formatMovie = (movie) => {
|
||||||
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.fetchingDetails = false;
|
||||||
movie.fetchingSubtitles = false;
|
movie.fetchingSubtitles = false;
|
||||||
|
movie.torrents = formatTorrents(movie.torrents);
|
||||||
|
return movie;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatMovies = (movies = []) => {
|
||||||
|
let allMoviesInPolochon = true;
|
||||||
|
movies.map((movie) => {
|
||||||
|
formatMovie(movie);
|
||||||
if (movie.polochon_url === "") {
|
if (movie.polochon_url === "") {
|
||||||
allMoviesInPolochon = false;
|
allMoviesInPolochon = false;
|
||||||
}
|
}
|
||||||
movies = movies.set(movie.imdb_id, fromJS(movie));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Select the first movie if the list is not empty
|
movies.sort((a, b) => {
|
||||||
let selectedImdbId = "";
|
|
||||||
if (movies.size > 0) {
|
|
||||||
// Sort by year
|
|
||||||
movies = movies.sort((a, b) => {
|
|
||||||
if (!allMoviesInPolochon) {
|
if (!allMoviesInPolochon) {
|
||||||
return b.get("year") - a.get("year");
|
return b.year - a.year;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dateA = new Date(a.get("date_added"));
|
const dateA = new Date(a.date_added);
|
||||||
const dateB = new Date(b.get("date_added"));
|
const dateB = new Date(b.date_added);
|
||||||
return dateA > dateB ? -1 : dateA < dateB ? 1 : 0;
|
return dateA > dateB ? -1 : dateA < dateB ? 1 : 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
selectedImdbId = movies.first().get("imdb_id");
|
let m = new Map();
|
||||||
}
|
movies.forEach((movie) => {
|
||||||
|
m.set(movie.imdb_id, movie);
|
||||||
|
});
|
||||||
|
|
||||||
return state.delete("movies").merge(
|
return m;
|
||||||
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),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default (state = defaultState, action) =>
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
@ -1,18 +1,24 @@
|
|||||||
import { List, fromJS } from "immutable";
|
import { produce } from "immer";
|
||||||
|
|
||||||
const defaultState = List();
|
const defaultState = new Map();
|
||||||
|
|
||||||
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),
|
|
||||||
};
|
|
||||||
|
|
||||||
export default (state = defaultState, action) =>
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
@ -1,30 +1,34 @@
|
|||||||
import { List, Map, fromJS } from "immutable";
|
import { produce } from "immer";
|
||||||
|
|
||||||
const defaultState = Map({
|
const defaultState = {
|
||||||
loadingPublic: false,
|
loadingPublic: false,
|
||||||
loadingManaged: false,
|
loadingManaged: false,
|
||||||
public: List(),
|
public: [],
|
||||||
managed: List(),
|
managed: [],
|
||||||
});
|
|
||||||
|
|
||||||
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)),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default (state = defaultState, action) =>
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
@ -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,
|
loading: false,
|
||||||
show: Map({
|
show: {},
|
||||||
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
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const sortEpisodes = (state, show) => {
|
const formatEpisode = (episode) => {
|
||||||
let episodes = show.episodes;
|
// Format the episode's torrents
|
||||||
delete show["episodes"];
|
episode.torrents = formatTorrents(episode.torrents);
|
||||||
|
|
||||||
let ret = state.set("loading", false);
|
// Set the default fetching data
|
||||||
if (episodes.length == 0) {
|
episode.fetching = false;
|
||||||
return ret;
|
episode.fetchingSubtitles = false;
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default (state = defaultState, action) =>
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
@ -1,77 +1,83 @@
|
|||||||
import { OrderedMap, Map, fromJS } from "immutable";
|
import { produce } from "immer";
|
||||||
|
|
||||||
const defaultState = Map({
|
const defaultState = {
|
||||||
loading: false,
|
loading: false,
|
||||||
shows: OrderedMap(),
|
shows: new Map(),
|
||||||
filter: "",
|
|
||||||
selectedImdbId: "",
|
selectedImdbId: "",
|
||||||
lastFetchUrl: "",
|
lastFetchUrl: "",
|
||||||
exploreOptions: Map(),
|
exploreOptions: {},
|
||||||
});
|
};
|
||||||
|
|
||||||
const handlers = {
|
const formatShow = (show) => {
|
||||||
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.fetchingDetails = false;
|
||||||
show.fetchingSubtitles = false;
|
show.fetchingSubtitles = false;
|
||||||
shows = shows.set(show.imdb_id, fromJS(show));
|
show.episodes = undefined;
|
||||||
});
|
return show;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatShows = (shows = []) => {
|
||||||
|
// Add defaults
|
||||||
|
shows.map((show) => formatShow(show));
|
||||||
|
|
||||||
let selectedImdbId = "";
|
|
||||||
if (shows.size > 0) {
|
|
||||||
// Sort by year
|
// Sort by year
|
||||||
shows = shows.sort((a, b) => b.get("year") - a.get("year"));
|
shows.sort((a, b) => b.year - a.year);
|
||||||
selectedImdbId = shows.first().get("imdb_id");
|
|
||||||
}
|
|
||||||
|
|
||||||
return state.delete("shows").merge(
|
let s = new Map();
|
||||||
Map({
|
shows.forEach((show) => {
|
||||||
shows: shows,
|
s.set(show.imdb_id, show);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return state.mergeIn(
|
return s;
|
||||||
["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),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default (state = defaultState, action) =>
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
@ -1,30 +1,34 @@
|
|||||||
import { Map, List, fromJS } from "immutable";
|
import { produce } from "immer";
|
||||||
|
|
||||||
const defaultState = Map({
|
const defaultState = {
|
||||||
fetching: false,
|
fetching: false,
|
||||||
searching: false,
|
searching: false,
|
||||||
torrents: List(),
|
torrents: [],
|
||||||
searchResults: List(),
|
searchResults: [],
|
||||||
});
|
|
||||||
|
|
||||||
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,
|
|
||||||
})
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default (state = defaultState, action) =>
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { Map, List, fromJS } from "immutable";
|
import { produce } from "immer";
|
||||||
|
|
||||||
import jwtDecode from "jwt-decode";
|
import jwtDecode from "jwt-decode";
|
||||||
import Cookies from "universal-cookie";
|
import Cookies from "universal-cookie";
|
||||||
|
|
||||||
const defaultState = Map({
|
const defaultState = {
|
||||||
error: "",
|
error: "",
|
||||||
loading: false,
|
loading: false,
|
||||||
username: "",
|
username: "",
|
||||||
@ -16,98 +16,102 @@ const defaultState = Map({
|
|||||||
polochonName: "",
|
polochonName: "",
|
||||||
polochonId: "",
|
polochonId: "",
|
||||||
polochonActivated: false,
|
polochonActivated: false,
|
||||||
tokens: List(),
|
tokens: [],
|
||||||
modules: Map(),
|
modules: {},
|
||||||
modulesLoading: false,
|
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");
|
localStorage.removeItem("token");
|
||||||
const cookies = new Cookies();
|
const cookies = new Cookies();
|
||||||
cookies.remove("token");
|
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);
|
const decodedToken = jwtDecode(token);
|
||||||
localStorage.setItem("token", token);
|
localStorage.setItem("token", token);
|
||||||
|
|
||||||
const cookies = new Cookies();
|
const cookies = new Cookies();
|
||||||
cookies.set("token", token);
|
cookies.set("token", token);
|
||||||
|
|
||||||
return state.merge(
|
draft.error = "";
|
||||||
Map({
|
draft.isLogged = true;
|
||||||
error: "",
|
draft.isTokenSet = true;
|
||||||
isLogged: true,
|
draft.isAdmin = decodedToken.isAdmin;
|
||||||
isTokenSet: true,
|
draft.isActivated = decodedToken.isActivated;
|
||||||
isAdmin: decodedToken.isAdmin,
|
draft.username = decodedToken.username;
|
||||||
isActivated: decodedToken.isActivated,
|
};
|
||||||
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) =>
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
@ -21,43 +21,6 @@ const pad = (d) => (d < 10 ? "0" + d.toString() : d.toString());
|
|||||||
export const prettyEpisodeName = (showName, season, episode) =>
|
export const prettyEpisodeName = (showName, season, episode) =>
|
||||||
`${showName} S${pad(season)}E${pad(episode)}`;
|
`${showName} S${pad(season)}E${pad(episode)}`;
|
||||||
|
|
||||||
export const inLibrary = (element) => element.get("polochon_url", "") !== "";
|
|
||||||
|
|
||||||
export const 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) => {
|
export const prettySize = (fileSizeInBytes) => {
|
||||||
var i = -1;
|
var i = -1;
|
||||||
var byteUnits = [" kB", " MB", " GB", " TB", "PB", "EB", "ZB", "YB"];
|
var byteUnits = [" kB", " MB", " GB", " TB", "PB", "EB", "ZB", "YB"];
|
||||||
@ -68,3 +31,19 @@ export const prettySize = (fileSizeInBytes) => {
|
|||||||
|
|
||||||
return Math.max(fileSizeInBytes, 0.1).toFixed(1) + byteUnits[i];
|
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;
|
||||||
|
};
|
||||||
|
8
frontend/package-lock.json
generated
8
frontend/package-lock.json
generated
@ -5934,10 +5934,10 @@
|
|||||||
"integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
|
"integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"immutable": {
|
"immer": {
|
||||||
"version": "4.0.0-rc.12",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0-rc.12.tgz",
|
"resolved": "https://registry.npmjs.org/immer/-/immer-6.0.3.tgz",
|
||||||
"integrity": "sha512-0M2XxkZLx/mi3t8NVwIm1g8nHoEmM9p9UBl/G9k4+hm0kBgOVdMV/B3CY5dQ8qG8qc80NN4gDV4HQv6FTJ5q7A=="
|
"integrity": "sha512-12VvNrfSrXZdm/BJgi/KDW2soq5freVSf3I1+4CLunUM8mAGx2/0Njy0xBVzi5zewQZiwM7z1/1T+8VaI7NkmQ=="
|
||||||
},
|
},
|
||||||
"import-cwd": {
|
"import-cwd": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
"font-awesome": "^4.7.0",
|
"font-awesome": "^4.7.0",
|
||||||
"fuzzy": "^0.1.3",
|
"fuzzy": "^0.1.3",
|
||||||
"history": "^4.9.0",
|
"history": "^4.9.0",
|
||||||
"immutable": "^4.0.0-rc.12",
|
"immer": "^6.0.3",
|
||||||
"jquery": "^3.4.1",
|
"jquery": "^3.4.1",
|
||||||
"jwt-decode": "^2.1.0",
|
"jwt-decode": "^2.1.0",
|
||||||
"moment": "^2.20.1",
|
"moment": "^2.20.1",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user