Rich torrent group #26
@ -170,7 +170,7 @@ const TorrentsDropdown = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const TorrentsDropdownTitle = () => {
|
const TorrentsDropdownTitle = () => {
|
||||||
const count = useSelector((state) => state.torrents.torrents.length);
|
const count = useSelector((state) => state.torrents.count);
|
||||||
if (count === 0) {
|
if (count === 0) {
|
||||||
return <span>Torrents</span>;
|
return <span>Torrents</span>;
|
||||||
}
|
}
|
||||||
|
17
frontend/js/components/torrents/list/poster.js
Normal file
17
frontend/js/components/torrents/list/poster.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import React from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
export const Poster = ({ url }) => {
|
||||||
|
if (!url || url === "") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="col-md-2 d-none d-md-block">
|
||||||
|
<img className="card-img" src={url} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
Poster.propTypes = {
|
||||||
|
url: PropTypes.string,
|
||||||
|
};
|
@ -20,25 +20,27 @@ export const Progress = ({ torrent }) => {
|
|||||||
const totalSize = prettySize(torrent.status.total_size);
|
const totalSize = prettySize(torrent.status.total_size);
|
||||||
const downloadRate = prettySize(torrent.status.download_rate) + "/s";
|
const downloadRate = prettySize(torrent.status.download_rate) + "/s";
|
||||||
return (
|
return (
|
||||||
<div className="card-body pb-0">
|
<div>
|
||||||
|
<div className="progress bg-light">
|
||||||
|
<div
|
||||||
|
className={progressBarClass}
|
||||||
|
style={{ width: percentDone }}
|
||||||
|
role="progressbar"
|
||||||
|
aria-valuenow={percentDone}
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="100"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
{started && (
|
{started && (
|
||||||
<>
|
<p>
|
||||||
<div className="progress bg-light">
|
{downloadedSize} / {totalSize} - {percentDone} - {downloadRate}
|
||||||
<div
|
</p>
|
||||||
className={progressBarClass}
|
)}
|
||||||
style={{ width: percentDone }}
|
{!started && (
|
||||||
role="progressbar"
|
<p>
|
||||||
aria-valuenow={percentDone}
|
<small>Not yet started</small>
|
||||||
aria-valuemin="0"
|
</p>
|
||||||
aria-valuemax="100"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
{downloadedSize} / {totalSize} - {percentDone} - {downloadRate}
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
{!started && <p>Download not yet started</p>}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -2,7 +2,7 @@ import React from "react";
|
|||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { useDispatch } from "react-redux";
|
import { useDispatch } from "react-redux";
|
||||||
|
|
||||||
import { prettyEpisodeName } from "../../../utils";
|
import { prettyEpisodeNameWithoutShow } from "../../../utils";
|
||||||
import { removeTorrent } from "../../../actions/torrents";
|
import { removeTorrent } from "../../../actions/torrents";
|
||||||
|
|
||||||
import { Progress } from "./progress";
|
import { Progress } from "./progress";
|
||||||
@ -10,32 +10,31 @@ import { Progress } from "./progress";
|
|||||||
export const Torrent = ({ torrent }) => {
|
export const Torrent = ({ torrent }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const torrentTitle = (torrent) => {
|
const title = (torrent) => {
|
||||||
switch (torrent.type) {
|
if (torrent.type !== "episode" || !torrent.video) {
|
||||||
case "movie":
|
return "";
|
||||||
return torrent.video ? torrent.video.title : torrent.status.name;
|
|
||||||
case "episode":
|
|
||||||
return torrent.video
|
|
||||||
? prettyEpisodeName(
|
|
||||||
torrent.video.show_title,
|
|
||||||
torrent.video.season,
|
|
||||||
torrent.video.episode
|
|
||||||
)
|
|
||||||
: torrent.status.name;
|
|
||||||
default:
|
|
||||||
return torrent.status.name;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
prettyEpisodeNameWithoutShow(
|
||||||
|
torrent.video.season,
|
||||||
|
torrent.video.episode
|
||||||
|
) + " - "
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card w-100 mb-3">
|
<div className="border-top">
|
||||||
<h5 className="card-header">
|
<div className="card-text d-flex flex-row">
|
||||||
<span className="text text-break">{torrentTitle(torrent)}</span>
|
<div className="mt-1 flex-fill">
|
||||||
<span
|
<span className="text text-break">{title(torrent)}</span>
|
||||||
className="fa fa-trash clickable pull-right"
|
<small className="text-muted text-break">{torrent.status.name}</small>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="fa fa-trash btn text-right"
|
||||||
onClick={() => dispatch(removeTorrent(torrent.status.id))}
|
onClick={() => dispatch(removeTorrent(torrent.status.id))}
|
||||||
></span>
|
/>
|
||||||
</h5>
|
</div>
|
||||||
<Progress torrent={torrent} />
|
<Progress torrent={torrent} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
49
frontend/js/components/torrents/list/torrentGroup.js
Normal file
49
frontend/js/components/torrents/list/torrentGroup.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import React from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
|
import { Torrent } from "./torrent";
|
||||||
|
import { Poster } from "./poster";
|
||||||
|
|
||||||
|
export const TorrentGroup = ({ torrentKey }) => {
|
||||||
|
const torrents = useSelector((state) =>
|
||||||
|
state.torrents.torrents.get(torrentKey)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (torrents.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = (torrent) => {
|
||||||
|
switch (torrent.type) {
|
||||||
|
case "movie":
|
||||||
|
return torrent.video.title;
|
||||||
|
case "episode":
|
||||||
|
return torrent.video.show_title;
|
||||||
|
default:
|
||||||
|
return "Files";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-100 mb-3 card">
|
||||||
|
<div className="row no-gutters">
|
||||||
|
<Poster url={torrents[0].img} />
|
||||||
|
<div className="col-sm">
|
||||||
|
<div className="card-body">
|
||||||
|
<h4 className="card-title">{title(torrents[0])}</h4>
|
||||||
|
{torrents.map((torrent, i) => (
|
||||||
|
<Torrent
|
||||||
|
key={torrent.video ? torrent.video.imdb_id : i}
|
||||||
|
torrent={torrent}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
TorrentGroup.propTypes = {
|
||||||
|
torrentKey: PropTypes.string.isRequired,
|
||||||
|
};
|
@ -1,12 +1,14 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
import { Torrent } from "./torrent";
|
import { TorrentGroup } from "./torrentGroup";
|
||||||
|
|
||||||
export const Torrents = () => {
|
export const Torrents = () => {
|
||||||
const torrents = useSelector((state) => state.torrents.torrents);
|
const torrentsKeys = useSelector((state) =>
|
||||||
|
Array.from(state.torrents.torrents.keys())
|
||||||
|
);
|
||||||
|
|
||||||
if (torrents.length === 0) {
|
if (torrentsKeys.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="jumbotron">
|
<div className="jumbotron">
|
||||||
<h2>No torrents</h2>
|
<h2>No torrents</h2>
|
||||||
@ -16,8 +18,8 @@ export const Torrents = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="d-flex flex-wrap">
|
<div className="d-flex flex-wrap">
|
||||||
{torrents.map((torrent, index) => (
|
{torrentsKeys.map((key) => (
|
||||||
<Torrent key={index} torrent={torrent} />
|
<TorrentGroup key={key} torrentKey={key} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -3,10 +3,42 @@ import { produce } from "immer";
|
|||||||
const defaultState = {
|
const defaultState = {
|
||||||
fetching: false,
|
fetching: false,
|
||||||
searching: false,
|
searching: false,
|
||||||
torrents: [],
|
torrents: new Map(),
|
||||||
|
count: 0,
|
||||||
searchResults: [],
|
searchResults: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Group the torrents by imdb id
|
||||||
|
const formatTorrents = (input) => {
|
||||||
|
let torrents = new Map();
|
||||||
|
if (!input) {
|
||||||
|
return torrents;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.forEach((t) => {
|
||||||
|
let key;
|
||||||
|
switch (t.type) {
|
||||||
|
case "movie":
|
||||||
|
key = t.video ? t.video.imdb_id : "unknown";
|
||||||
|
break;
|
||||||
|
case "episode":
|
||||||
|
key = t.video ? t.video.show_imdb_id : "unknown";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
key = "unknown";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!torrents.has(key)) {
|
||||||
|
torrents.set(key, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
torrents.get(key).push(t);
|
||||||
|
});
|
||||||
|
|
||||||
|
return torrents;
|
||||||
|
};
|
||||||
|
|
||||||
export default (state = defaultState, action) =>
|
export default (state = defaultState, action) =>
|
||||||
produce(state, (draft) => {
|
produce(state, (draft) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
@ -16,7 +48,8 @@ export default (state = defaultState, action) =>
|
|||||||
|
|
||||||
case "TORRENTS_FETCH_FULFILLED":
|
case "TORRENTS_FETCH_FULFILLED":
|
||||||
draft.fetching = false;
|
draft.fetching = false;
|
||||||
draft.torrents = action.payload.response.data;
|
draft.torrents = formatTorrents(action.payload.response.data);
|
||||||
|
draft.count = action.payload.response.data.length;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "TORRENTS_SEARCH_PENDING":
|
case "TORRENTS_SEARCH_PENDING":
|
||||||
|
@ -18,6 +18,9 @@ export const prettyDurationFromMinutes = (runtime) => {
|
|||||||
|
|
||||||
const pad = (d) => (d < 10 ? "0" + d.toString() : d.toString());
|
const pad = (d) => (d < 10 ? "0" + d.toString() : d.toString());
|
||||||
|
|
||||||
|
export const prettyEpisodeNameWithoutShow = (season, episode) =>
|
||||||
|
`S${pad(season)}E${pad(episode)}`;
|
||||||
|
|
||||||
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)}`;
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user