diff --git a/package.json b/package.json
index 680f3bf..1ea38c0 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,7 @@
"jwt-decode": "^2.1.0",
"react": "^15.3.2",
"react-bootstrap": "^0.30.6",
+ "react-bootstrap-sweetalert": "^3.0.0",
"react-dom": "^15.3.2",
"react-loading": "^0.0.9",
"react-redux": "^4.4.6",
diff --git a/src/main.go b/src/main.go
index cddf6d1..cbea35a 100644
--- a/src/main.go
+++ b/src/main.go
@@ -87,7 +87,7 @@ func main() {
env.Handle("/shows/refresh", extmedias.RefreshShows).WithRole(users.UserRole).Methods("POST")
env.Handle("/shows/explore", extmedias.ExploreShows).WithRole(users.UserRole).Methods("GET")
env.Handle("/shows/search", shows.SearchShow).WithRole(users.UserRole).Methods("POST")
- env.Handle("/download", torrents.DownloadHandler).WithRole(users.UserRole).Methods("POST")
+ env.Handle("/torrents", torrents.DownloadHandler).WithRole(users.UserRole).Methods("POST")
n := negroni.Classic()
n.Use(authMiddleware)
diff --git a/src/public/js/actions/actionCreators.js b/src/public/js/actions/actionCreators.js
index 9e96a87..32354e6 100644
--- a/src/public/js/actions/actionCreators.js
+++ b/src/public/js/actions/actionCreators.js
@@ -1,21 +1,30 @@
import { configureAxios, request } from '../requests'
// ======================
-// Errors
+// Alerts
// ======================
-export function addError(message) {
+export function addAlertError(message) {
return {
- type: 'ADD_ERROR',
+ type: 'ADD_ALERT_ERROR',
payload: {
message,
}
}
}
-export function dismissError() {
+export function addAlertOk(message) {
return {
- type: 'DISMISS_ERROR',
+ type: 'ADD_ALERT_OK',
+ payload: {
+ message,
+ }
+ }
+}
+
+export function dismissAlert() {
+ return {
+ type: 'DISMISS_ALERT',
}
}
@@ -44,14 +53,15 @@ export function loginUser(username, password) {
username: username,
password: password,
},
- )
+ ),
)
}
export function updateUser(config) {
return request(
'USER_UPDATE',
- configureAxios().post('/users/edit', config)
+ configureAxios().post('/users/edit', config),
+ "User updated",
)
}
@@ -132,3 +142,17 @@ export function selectShow(imdbId) {
imdbId
}
}
+
+// ======================
+// AddTorrent
+// ======================
+
+export function addTorrent(url) {
+ return request(
+ 'ADD_TORRENT',
+ configureAxios().post('/torrents', {
+ url: url,
+ }),
+ "Torrent added",
+ )
+}
diff --git a/src/public/js/app.js b/src/public/js/app.js
index bcd32ce..2032747 100644
--- a/src/public/js/app.js
+++ b/src/public/js/app.js
@@ -27,7 +27,7 @@ import store, { history } from './store'
// Components
import NavBar from './components/navbar'
-import Error from './components/errors'
+import Alert from './components/alerts/alert'
import MovieList from './components/movies/list'
import ShowList from './components/shows/list'
import ShowDetails from './components/shows/details'
@@ -43,7 +43,7 @@ class Main extends React.Component {
return (
-
+
{React.cloneElement(this.props.children, this.props)}
@@ -57,7 +57,7 @@ function mapStateToProps(state) {
movieStore: state.movieStore,
showStore: state.showStore,
userStore: state.userStore,
- errors: state.errors,
+ alerts: state.alerts,
}
}
diff --git a/src/public/js/components/alerts/alert.js b/src/public/js/components/alerts/alert.js
new file mode 100644
index 0000000..bcb46ae
--- /dev/null
+++ b/src/public/js/components/alerts/alert.js
@@ -0,0 +1,17 @@
+import React from 'react'
+
+import SweetAlert from 'react-bootstrap-sweetalert';
+
+export default function Alert(props) {
+ if (!props.alerts.show) {
+ return null
+ }
+
+ return (
+
+ )
+}
diff --git a/src/public/js/components/errors.js b/src/public/js/components/errors.js
deleted file mode 100644
index 6cd9f94..0000000
--- a/src/public/js/components/errors.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import React from 'react'
-
-export default function Error(props) {
- if (!props.errors.message) {
- return null
- }
- return (
-
-
-
-
-
{props.errors.message}
-
-
-
- )
-}
diff --git a/src/public/js/components/list/posters.js b/src/public/js/components/list/posters.js
index 2514a56..64faf12 100644
--- a/src/public/js/components/list/posters.js
+++ b/src/public/js/components/list/posters.js
@@ -13,11 +13,27 @@ export default function ListPosters(props) {
extract: (el) => el.title
});
elmts = filtered.map((el) => el.original);
- }
+ } else {
+ // Get the page number if defined
+ let page = 1;
+ let perPage = props.perPage;
+ if (props.params && props.params.page) {
+ page = parseInt(props.params.page);
+ }
- // Limit the number of results
- if (elmts.length > props.perPage) {
- elmts = elmts.slice(0, props.perPage);
+ let from = 0;
+ let to = perPage - 1;
+ if (page > 1) {
+ from = ((page - 1) * perPage) - 1;
+ to = from + perPage;
+ }
+
+ // Limit the number of results
+ if ((from + perPage) > elmts.length) {
+ elmts = elmts.slice(from);
+ } else {
+ elmts = elmts.slice(from, to);
+ }
}
return (
diff --git a/src/public/js/components/movies/list.js b/src/public/js/components/movies/list.js
index d66bffe..a2aa0f2 100644
--- a/src/public/js/components/movies/list.js
+++ b/src/public/js/components/movies/list.js
@@ -1,5 +1,6 @@
import React from 'react'
+import TorrentsButton from './torrents'
import ListPosters from '../list/posters'
import ListDetails from '../list/details'
import Loader from '../loader/loader'
@@ -37,13 +38,14 @@ class MovieButtons extends React.Component {
Download
}
- {this.props.movie.torrents && this.props.movie.torrents.map(function(torrent, index) {
- return (
-
- {torrent.quality} Torrent
-
- )}
- )}
+
+ {this.props.movie.torrents &&
+
+ }
+
IMDB
@@ -98,6 +100,7 @@ export default class MovieList extends React.Component {
filter={this.props.movieStore.filter}
perPage={this.props.movieStore.perPage}
onClick={this.props.selectMovie}
+ params={this.props.params}
/>
{selectedMovie &&
@@ -105,6 +108,7 @@ export default class MovieList extends React.Component {
movie={selectedMovie}
fetching={this.props.movieStore.fetchingDetails}
getMovieDetails={this.props.getMovieDetails}
+ addTorrent={this.props.addTorrent}
/>
}
diff --git a/src/public/js/components/movies/torrents.js b/src/public/js/components/movies/torrents.js
new file mode 100644
index 0000000..79812d4
--- /dev/null
+++ b/src/public/js/components/movies/torrents.js
@@ -0,0 +1,81 @@
+import React from 'react'
+
+import { DropdownButton, MenuItem } from 'react-bootstrap'
+
+export default class TorrentsButton extends React.Component {
+ constructor(props) {
+ super(props);
+ this.handleClick = this.handleClick.bind(this);
+ }
+ handleClick(e, url) {
+ e.preventDefault();
+ this.props.addTorrent(url);
+ }
+ render() {
+ const entries = buildMenuItems(this.props.torrents);
+ return (
+
+ {entries.map(function(e, index) {
+ switch (e.type) {
+ case 'header':
+ return (
+
+ );
+ case 'divider':
+ return (
+
+ );
+ case 'entry':
+ return (
+
+ );
+ }
+ }, this)}
+
+ );
+ }
+}
+
+function buildMenuItems(torrents) {
+ // Organise by source
+ let sources = {}
+ for (let torrent of torrents) {
+ if (!sources[torrent.source]) {
+ sources[torrent.source] = [];
+ }
+ sources[torrent.source].push(torrent);
+ }
+
+ // Build the array of entries
+ let entries = [];
+ let sourceNames = Object.keys(sources);
+ let dividerCount = sourceNames.length - 1;
+ for (let source of sourceNames) {
+ // Push the title
+ entries.push({
+ type: "header",
+ value: source,
+ });
+
+ // Push the torrents
+ for (let torrent of sources[source]) {
+ entries.push({
+ type: "entry",
+ quality: torrent.quality,
+ url: torrent.url,
+ });
+ }
+
+ // Push the divider
+ if (dividerCount > 0) {
+ dividerCount--;
+ entries.push({ type: "divider" });
+ }
+ }
+
+ return entries;
+}
diff --git a/src/public/js/components/shows/details.js b/src/public/js/components/shows/details.js
index 04043b0..ecaa895 100644
--- a/src/public/js/components/shows/details.js
+++ b/src/public/js/components/shows/details.js
@@ -13,7 +13,10 @@ export default class ShowDetails extends React.Component {
return (
-
+
);
}
@@ -35,7 +38,8 @@ function Header(props){
function HeaderThumbnail(props){
return (
-

+
);
}
@@ -70,7 +74,10 @@ function SeasonsList(props){
{props.data.seasons.length > 0 && props.data.seasons.map(function(season, index) {
return (
-
+
)
})}
@@ -109,9 +116,13 @@ class Season extends React.Component {
{this.props.data.episodes.map(function(episode, index) {
let key = `${episode.season}-${episode.episode}`;
return (
-
+
)
- })}
+ }, this)}
}
@@ -130,7 +141,11 @@ function Episode(props) {
{props.data.torrents && props.data.torrents.map(function(torrent, index) {
let key = `${props.data.season}-${props.data.episode}-${torrent.source}-${torrent.quality}`;
return (
-
+
)
})}
@@ -139,12 +154,25 @@ function Episode(props) {
)
}
-function Torrent(props) {
- return (
-
-
- {props.data.quality}
-
-
- )
+class Torrent extends React.Component {
+ constructor(props) {
+ super(props);
+ this.handleClick = this.handleClick.bind(this);
+ }
+ handleClick(e, url) {
+ e.preventDefault();
+ this.props.addTorrent(url);
+ }
+ render() {
+ return (
+
+ this.handleClick(e, this.props.data.url)}
+ href={this.props.data.url} >
+ {this.props.data.quality}
+
+
+ )
+ }
}
diff --git a/src/public/js/reducers/alerts.js b/src/public/js/reducers/alerts.js
new file mode 100644
index 0000000..c6a0f97
--- /dev/null
+++ b/src/public/js/reducers/alerts.js
@@ -0,0 +1,30 @@
+const defaultState = {
+ show: false,
+ message: "",
+ type: "",
+};
+
+export default function Alert(state = defaultState, action) {
+ switch (action.type) {
+ case 'ADD_ALERT_ERROR':
+ return Object.assign({}, state, {
+ message: action.payload.message,
+ show: true,
+ type: "error",
+ })
+ case 'ADD_ALERT_OK':
+ return Object.assign({}, state, {
+ message: action.payload.message,
+ show: true,
+ type: "success",
+ })
+ case 'DISMISS_ALERT':
+ return Object.assign({}, state, {
+ message: "",
+ show: false,
+ type: "",
+ })
+ default:
+ return state;
+ }
+}
diff --git a/src/public/js/reducers/errors.js b/src/public/js/reducers/errors.js
deleted file mode 100644
index fcc5b8a..0000000
--- a/src/public/js/reducers/errors.js
+++ /dev/null
@@ -1,12 +0,0 @@
-export default function error(state = {}, action) {
- switch (action.type) {
- case 'ADD_ERROR':
- return Object.assign({}, state, {
- message: action.payload.message,
- })
- case 'DISMISS_ERROR':
- return {};
- default:
- return state;
- }
-}
diff --git a/src/public/js/reducers/index.js b/src/public/js/reducers/index.js
index 3d0d277..9e8f6e6 100644
--- a/src/public/js/reducers/index.js
+++ b/src/public/js/reducers/index.js
@@ -4,7 +4,7 @@ import { routerReducer } from 'react-router-redux'
import movieStore from './movies'
import showStore from './shows'
import userStore from './users'
-import errors from './errors'
+import alerts from './alerts'
// Use combine form form react-redux-form, it's a thin wrapper arround the
// default combinedReducers provided with React. It allows the forms to be
@@ -14,7 +14,7 @@ const rootReducer = combineForms({
movieStore,
showStore,
userStore,
- errors,
+ alerts,
})
export default rootReducer;
diff --git a/src/public/js/requests.js b/src/public/js/requests.js
index 87c8bda..cf64b98 100644
--- a/src/public/js/requests.js
+++ b/src/public/js/requests.js
@@ -16,7 +16,7 @@ export function configureAxios(headers = {}) {
// This function takes en event prefix to dispatch evens during the life of the
// request, it also take a promise (axios request)
-export function request(eventPrefix, promise) {
+export function request(eventPrefix, promise, successMessage = null) {
// Events
const pending = `${eventPrefix}_PENDING`;
const fulfilled = `${eventPrefix}_FULFILLED`;
@@ -30,7 +30,7 @@ export function request(eventPrefix, promise) {
if (response.data.status === 'error')
{
dispatch({
- type: 'ADD_ERROR',
+ type: 'ADD_ALERT_ERROR',
payload: {
message: response.data.message,
}
@@ -40,6 +40,14 @@ export function request(eventPrefix, promise) {
type: fulfilled,
payload: response.data,
})
+ if (successMessage) {
+ dispatch({
+ type: 'ADD_ALERT_OK',
+ payload: {
+ message: successMessage,
+ },
+ })
+ }
})
.catch(error => {
// Unauthorized
@@ -49,7 +57,7 @@ export function request(eventPrefix, promise) {
})
}
dispatch({
- type: 'ADD_ERROR',
+ type: 'ADD_ALERT_ERROR',
payload: {
message: error.response.data,
}
diff --git a/src/public/less/app.less b/src/public/less/app.less
index 121e568..d1cfe4f 100644
--- a/src/public/less/app.less
+++ b/src/public/less/app.less
@@ -23,8 +23,19 @@ body {
.list-details-buttons {
position: fixed;
- bottom: 1%;
- right: 1%;
+ bottom: 0px;
+ padding-top: 5px;
+ background-color: @body-bg;
+
+ @media (min-width: @screen-xs-max) {
+ right: 0px;
+ margin-right: 5px;
+ }
+
+ a.btn,
+ div.btn-group {
+ margin-bottom: 5px;
+ }
}
.list-filter {
@@ -42,3 +53,7 @@ body {
.navbar {
opacity: 0.95;
}
+
+div.sweet-alert > h2 {
+ color: @body-bg;
+}
diff --git a/yarn.lock b/yarn.lock
index 7cc1e53..a1fd710 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3077,6 +3077,12 @@ react-bootstrap:
uncontrollable "^4.0.1"
warning "^3.0.0"
+react-bootstrap-sweetalert:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/react-bootstrap-sweetalert/-/react-bootstrap-sweetalert-3.0.0.tgz#65378a42f37845676acf98e8c43ce4a61f23306f"
+ dependencies:
+ object-assign "^4.1.0"
+
react-dom@^15.3.2:
version "15.3.2"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.3.2.tgz#c46b0aa5380d7b838e7a59c4a7beff2ed315531f"