Add torrent search in the UI
This commit is contained in:
parent
0dda37de50
commit
5d3fc58176
@ -30,3 +30,10 @@ export function fetchTorrents() {
|
|||||||
configureAxios().get("/torrents")
|
configureAxios().get("/torrents")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function searchTorrents(url) {
|
||||||
|
return request(
|
||||||
|
"TORRENTS_SEARCH",
|
||||||
|
configureAxios().get(url)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -64,7 +64,7 @@ export default class AppNavBar extends React.PureComponent {
|
|||||||
<WishlistDropdown />
|
<WishlistDropdown />
|
||||||
}
|
}
|
||||||
{loggedAndActivated &&
|
{loggedAndActivated &&
|
||||||
<Torrents torrentsCount={this.props.torrentCount} />
|
<TorrentsDropdown torrentsCount={this.props.torrentCount} />
|
||||||
}
|
}
|
||||||
<UserDropdown
|
<UserDropdown
|
||||||
username={this.props.username}
|
username={this.props.username}
|
||||||
@ -199,20 +199,31 @@ function WishlistDropdown() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Torrents(props) {
|
function TorrentsDropdown(props) {
|
||||||
|
const title = (<TorrentsDropdownTitle torrentsCount={props.torrentsCount} />)
|
||||||
return(
|
return(
|
||||||
<Nav>
|
<Nav>
|
||||||
<LinkContainer to="/torrents">
|
<NavDropdown title={title} id="navbar-wishlit-dropdown">
|
||||||
<NavItem>
|
<LinkContainer to="/torrents/list">
|
||||||
Torrents
|
<MenuItem>Downloads</MenuItem>
|
||||||
{props.torrentsCount > 0 &&
|
|
||||||
<span>
|
|
||||||
|
|
||||||
<span className="label label-info">{props.torrentsCount}</span>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
</NavItem>
|
|
||||||
</LinkContainer>
|
</LinkContainer>
|
||||||
|
<LinkContainer to="/torrents/search">
|
||||||
|
<MenuItem>Search</MenuItem>
|
||||||
|
</LinkContainer>
|
||||||
|
</NavDropdown>
|
||||||
</Nav>
|
</Nav>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function TorrentsDropdownTitle(props) {
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
Torrents
|
||||||
|
{props.torrentsCount > 0 &&
|
||||||
|
<span>
|
||||||
|
<span className="label label-info">{props.torrentsCount}</span>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
212
src/public/js/components/torrents/search.js
Normal file
212
src/public/js/components/torrents/search.js
Normal 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);
|
@ -2,15 +2,22 @@ import { Map, List, fromJS } from "immutable"
|
|||||||
|
|
||||||
const defaultState = Map({
|
const defaultState = Map({
|
||||||
"fetching": false,
|
"fetching": false,
|
||||||
|
"searching": false,
|
||||||
"torrents": List(),
|
"torrents": List(),
|
||||||
|
"searchResults": List(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const handlers = {
|
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({
|
"TORRENTS_FETCH_FULFILLED": (state, action) => state.merge(fromJS({
|
||||||
fetching: false,
|
fetching: false,
|
||||||
torrents: action.payload.response.data,
|
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) =>
|
||||||
|
@ -6,9 +6,10 @@ import UserEdit from "./components/users/edit"
|
|||||||
import UserActivation from "./components/users/activation"
|
import UserActivation from "./components/users/activation"
|
||||||
import UserSignUp from "./components/users/signup"
|
import UserSignUp from "./components/users/signup"
|
||||||
import TorrentList from "./components/torrents/list"
|
import TorrentList from "./components/torrents/list"
|
||||||
|
import TorrentSearch from "./components/torrents/search"
|
||||||
import AdminPanel from "./components/admins/panel"
|
import AdminPanel from "./components/admins/panel"
|
||||||
|
|
||||||
import { fetchTorrents } from "./actions/torrents"
|
import { fetchTorrents, searchTorrents } from "./actions/torrents"
|
||||||
import { userLogout, getUserInfos } from "./actions/users"
|
import { userLogout, getUserInfos } from "./actions/users"
|
||||||
import { fetchMovies, getMovieExploreOptions } from "./actions/movies"
|
import { fetchMovies, getMovieExploreOptions } from "./actions/movies"
|
||||||
import { fetchShows, fetchShowDetails, getShowExploreOptions } from "./actions/shows"
|
import { fetchShows, fetchShowDetails, getShowExploreOptions } from "./actions/shows"
|
||||||
@ -240,7 +241,7 @@ export default function getRoutes(App) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/torrents",
|
path: "/torrents/list",
|
||||||
component: TorrentList,
|
component: TorrentList,
|
||||||
onEnter: function(nextState, replace, next) {
|
onEnter: function(nextState, replace, next) {
|
||||||
loginCheck(nextState, replace, next, function() {
|
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",
|
path: "/admin",
|
||||||
component: AdminPanel,
|
component: AdminPanel,
|
||||||
|
@ -81,3 +81,25 @@ div.sweet-alert > h2 {
|
|||||||
margin-left: 3px;
|
margin-left: 3px;
|
||||||
margin-right: 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;
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user