Add torrent search in the UI

This commit is contained in:
Grégoire Delattre 2017-08-15 13:04:49 +02:00
parent 0dda37de50
commit 5d3fc58176
6 changed files with 292 additions and 16 deletions

View File

@ -30,3 +30,10 @@ export function fetchTorrents() {
configureAxios().get("/torrents")
)
}
export function searchTorrents(url) {
return request(
"TORRENTS_SEARCH",
configureAxios().get(url)
)
}

View File

@ -64,7 +64,7 @@ export default class AppNavBar extends React.PureComponent {
<WishlistDropdown />
}
{loggedAndActivated &&
<Torrents torrentsCount={this.props.torrentCount} />
<TorrentsDropdown torrentsCount={this.props.torrentCount} />
}
<UserDropdown
username={this.props.username}
@ -199,20 +199,31 @@ function WishlistDropdown() {
);
}
function Torrents(props) {
function TorrentsDropdown(props) {
const title = (<TorrentsDropdownTitle torrentsCount={props.torrentsCount} />)
return(
<Nav>
<LinkContainer to="/torrents">
<NavItem>
Torrents
{props.torrentsCount > 0 &&
<span>
&nbsp;
<span className="label label-info">{props.torrentsCount}</span>
</span>
}
</NavItem>
</LinkContainer>
<NavDropdown title={title} id="navbar-wishlit-dropdown">
<LinkContainer to="/torrents/list">
<MenuItem>Downloads</MenuItem>
</LinkContainer>
<LinkContainer to="/torrents/search">
<MenuItem>Search</MenuItem>
</LinkContainer>
</NavDropdown>
</Nav>
);
}
function TorrentsDropdownTitle(props) {
return (
<span>
Torrents
{props.torrentsCount > 0 &&
<span>
&nbsp; <span className="label label-info">{props.torrentsCount}</span>
</span>
}
</span>
);
}

View File

@ -0,0 +1,212 @@
import React from "react"
import { connect } from "react-redux"
import { bindActionCreators } from "redux"
import { addTorrent, searchTorrents } from "../../actions/torrents"
import Loader from "../loader/loader"
import { OverlayTrigger, Tooltip } from "react-bootstrap"
function mapStateToProps(state) {
return {
searching: state.torrentStore.get("searching"),
results: state.torrentStore.get("searchResults"),
};
}
const mapDispatchToProps = (dispatch) =>
bindActionCreators({ addTorrent, searchTorrents }, dispatch)
class TorrentSearch extends React.PureComponent {
constructor(props) {
super(props);
this.handleSearchInput = this.handleSearchInput.bind(this);
this.state = { search: (this.props.router.params.search || "") };
}
handleSearchInput() {
this.setState({ search: this.refs.search.value });
}
handleClick(type) {
if (this.state.search === "") { return }
const url = `/torrents/search/${type}/${encodeURI(this.state.search)}`;
this.props.router.push(url);
}
render() {
const searchFromURL = this.props.router.params.search || "";
const typeFromURL = this.props.router.params.type || "";
return (
<div>
<div className="col-xs-12">
<form className="form-horizontal" onSubmit={(e) => e.preventDefault()}>
<div className="form-group">
<input
type="text"
className="form-control"
placeholder="Search torrents"
value={this.state.search}
onChange={this.handleSearchInput}
ref="search"
/>
</div>
</form>
</div>
<div className="row">
<SearchButton
text="Search movies"
type="movies"
typeFromURL={typeFromURL}
handleClick={() => this.handleClick("movies")}
/>
<SearchButton
text="Search shows"
type="shows"
typeFromURL={typeFromURL}
handleClick={() => this.handleClick("shows")}
/>
</div>
<hr />
<div className="row">
<TorrentList
searching={this.props.searching}
results={this.props.results}
addTorrent={this.props.addTorrent}
searchFromURL={searchFromURL}
/>
</div>
</div>
);
}
}
function SearchButton(props) {
const color = (props.type === props.typeFromURL) ? "primary" : "default";
return (
<div className="col-xs-6">
<button
className={`btn btn-${color} full-width`}
type="button"
onClick={props.handleClick}
>
<i className="fa fa-search" aria-hidden="true"></i> {props.text}
</button>
</div>
);
}
function TorrentList(props) {
if (props.searching) {
return (<Loader />);
}
if (props.searchFromURL === "") {
return null;
}
if (props.results.size === 0) {
return (
<div className="col-xs-12">
<div className="well well-lg">
<h2>No results</h2>
</div>
</div>
);
}
return (
<div className="col-xs-12">
{props.results.map(function(el, index) {
return (
<Torrent
key={index}
data={el}
addTorrent={props.addTorrent}
/>);
})}
</div>
);
}
function Torrent(props) {
return (
<div className="row">
<div className="col-xs-12">
<table className="table responsive table-align-middle torrent-search-result">
<tbody>
<tr>
<td rowSpan="2" className="col-xs-1 torrent-small-width">
<h4>
<TorrentHealth
url={props.data.get("url")}
seeders={props.data.get("seeders")}
leechers={props.data.get("leechers")}
/>
</h4>
</td>
<td colSpan="4" className="col-xs-9 title">
<span className="torrent-title">{props.data.get("name")}</span>
</td>
<td rowSpan="2" className="col-xs-1 torrent-small-width">
<h4 className="pull-right clickable" onClick={() => props.addTorrent(props.data.get("url"))}>
<i className="fa fa-cloud-download" aria-hidden="true"></i>
</h4>
</td>
</tr>
<tr>
<td className="col-xs-1 torrent-label">
<span className="label label-warning">{props.data.get("quality")}</span>
</td>
<td className="col-xs-1 torrent-label">
<span className="label label-success">{props.data.get("source")}</span>
</td>
<td className="col-xs-1 torrent-label">
<span className="label label-info">{props.data.get("upload_user")}</span>
</td>
<td className="col-xs-7 torrent-label"></td>
</tr>
</tbody>
</table>
</div>
</div>
);
}
function TorrentHealth(props) {
const seeders = props.seeders || 0;
const leechers = props.leechers || 1;
let color;
let health;
let ratio = seeders/leechers;
if (seeders > 20) {
health = "good";
color = "success";
} else {
if (ratio > 1) {
health = "medium";
color = "warning";
} else {
health = "bad";
color = "danger";
}
}
const className = `text text-${color}`;
const tooltip = (
<Tooltip id={`tooltip-health-${props.url}`}>
<p><span className={className}>Health: {health}</span></p>
<p>Seeders: {seeders}</p>
<p>Leechers: {props.leechers}</p>
</Tooltip>
);
return (
<OverlayTrigger placement="right" overlay={tooltip}>
<span className={className}>
<i className="fa fa-circle" aria-hidden="true"></i>
</span>
</OverlayTrigger>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(TorrentSearch);

View File

@ -2,15 +2,22 @@ import { Map, List, fromJS } from "immutable"
const defaultState = Map({
"fetching": false,
"searching": false,
"torrents": List(),
"searchResults": List(),
});
const handlers = {
"TORRENTS_FETCH_PENDING": state => state.set("fetching", false),
"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) =>

View File

@ -6,9 +6,10 @@ import UserEdit from "./components/users/edit"
import UserActivation from "./components/users/activation"
import UserSignUp from "./components/users/signup"
import TorrentList from "./components/torrents/list"
import TorrentSearch from "./components/torrents/search"
import AdminPanel from "./components/admins/panel"
import { fetchTorrents } from "./actions/torrents"
import { fetchTorrents, searchTorrents } from "./actions/torrents"
import { userLogout, getUserInfos } from "./actions/users"
import { fetchMovies, getMovieExploreOptions } from "./actions/movies"
import { fetchShows, fetchShowDetails, getShowExploreOptions } from "./actions/shows"
@ -240,7 +241,7 @@ export default function getRoutes(App) {
},
},
{
path: "/torrents",
path: "/torrents/list",
component: TorrentList,
onEnter: function(nextState, replace, next) {
loginCheck(nextState, replace, next, function() {
@ -248,6 +249,22 @@ export default function getRoutes(App) {
});
},
},
{
path: "/torrents/search",
component: TorrentSearch,
onEnter: function(nextState, replace, next) {
loginCheck(nextState, replace, next);
},
},
{
path: "/torrents/search/:type/:search",
component: TorrentSearch,
onEnter: function(nextState, replace, next) {
loginCheck(nextState, replace, next, function() {
store.dispatch(searchTorrents(`/torrents/search/${nextState.params.type}/${encodeURI(nextState.params.search)}`));
});
},
},
{
path: "/admin",
component: AdminPanel,

View File

@ -81,3 +81,25 @@ div.sweet-alert > h2 {
margin-left: 3px;
margin-right: 3px;
}
button.full-width {
width: 100%;
}
table.torrent-search-result {
font-size: 15px;
margin-bottom: 5px;
border-bottom: 2px solid @gray-light;
td.torrent-small-width {
width: 1%;
}
td.torrent-label {
padding-top: 0px;
padding-bottom: 10px;
}
}
table.table-align-middle > tbody > tr > td {
vertical-align: middle;
}