Refactor torrent selection state management

Now reduxified and more consistent
This commit is contained in:
Drew DeVault 2017-08-25 09:43:54 -04:00
parent ce3653c294
commit 6983703672
7 changed files with 109 additions and 94 deletions

74
src/actions/selection.js Normal file
View File

@ -0,0 +1,74 @@
import { filter_subscribe, filter_unsubscribe } from './filter_subscribe';
import { unsubscribe } from './subscribe';
import { push } from 'react-router-redux';
export const UNION = 'UNION';
export const SUBTRACT = 'SUBTRACT';
export const EXCLUSIVE = 'EXCLUSIVE';
Set.prototype.difference = function(set) {
var diff = new Set(this);
for (var v of set) {
diff.delete(v);
}
return diff;
}
export default function selectTorrent(id, action) {
return (dispatch, getState) => {
const previous = new Set(getState().selection);
dispatch({ type: action, id });
const state = getState();
const next = new Set(state.selection);
const filter_subscriptions = state.filter_subscribe;
const subscriptions = state.subscribe;
const { peers, files, pieces, trackers } = state;
const added = next.difference(previous);
const removed = previous.difference(next);
added.forEach(t => {
const criteria = [
{ field: "torrent_id", op: "==", value: id }
];
dispatch(filter_subscribe("peer", criteria));
dispatch(filter_subscribe("file", criteria));
dispatch(filter_subscribe("piece", criteria));
dispatch(filter_subscribe("tracker", criteria));
});
removed.forEach(t => {
/* Remove filter subscriptions */
const serials = filter_subscriptions
.filter(sub => sub.criteria[0] && sub.criteria[0].value === t)
.map(sub => sub.serial);
serials.forEach(serial => dispatch(filter_unsubscribe(serial)));
/* Remove resource subscriptions */
const ids = [
...Object.values(files)
.filter(file => file.torrent_id === t)
.map(file => file.id)
//...Object.values(peers)
// .filter(peer => peer.torrent_id === t)
// .map(peer => peer.id),
//...Object.values(trackers)
// .filter(tracker => tracker.torrent_id === t)
// .map(tracker => tracker.id),
//...Object.values(pieces)
// .filter(piece => piece.torrent_id === t)
// .map(piece => piece.id),
];
if (ids.length > 0) {
dispatch(unsubscribe(...ids));
}
});
const url_torrents = state.selection.slice(0, 3);
if (url_torrents.length > 0) {
dispatch(push(`/torrents/${url_torrents}`));
} else {
dispatch(push("/"));
}
};
}

View File

@ -17,8 +17,8 @@ export default function filter_subscribe(state = [], action) {
]; ];
} }
case FILTER_UNSUBSCRIBE: { case FILTER_UNSUBSCRIBE: {
const { _serial } = action; const { serial } = action;
return state.filter(({ serial }) => serial !== _serial); return state.filter(filter => filter.serial !== serial);
} }
} }
return state; return state;

View File

@ -1,5 +1,6 @@
import { combineReducers } from 'redux'; import { combineReducers } from 'redux';
import { routerReducer } from 'react-router-redux' import { routerReducer } from 'react-router-redux'
import selection from './selection';
import subscribe from './subscribe'; import subscribe from './subscribe';
import filter_subscribe from './filter_subscribe'; import filter_subscribe from './filter_subscribe';
import server from './server'; import server from './server';
@ -7,6 +8,7 @@ import torrents from './torrents';
import files from './files'; import files from './files';
const root = combineReducers({ const root = combineReducers({
selection,
subscribe, subscribe,
filter_subscribe, filter_subscribe,
server, server,

14
src/reducers/selection.js Normal file
View File

@ -0,0 +1,14 @@
import { UNION, SUBTRACT, EXCLUSIVE } from '../actions/selection';
export default function selection(state = [], action) {
const { id } = action;
switch (action.type) {
case UNION:
return [id, ...state.filter(t => t !== id)];
case SUBTRACT:
return state.filter(t => t !== id);
case EXCLUSIVE:
return [id];
}
return state;
}

View File

@ -1,71 +0,0 @@
import { filter_subscribe, filter_unsubscribe } from './actions/filter_subscribe';
import { push } from 'react-router-redux';
/* Listen pal, we're just gonna pretend this import isn't happening */
import { dispatch, getState } from './store';
export function activeTorrents() {
const { pathname } = getState().router.location;
if (pathname.indexOf("/torrents/") !== 0) {
return [];
} else {
return pathname.slice("/torrents/".length).split(",");
}
}
export function updateSubscriptions(added, removed) {
if (added.length > 0) {
added.forEach(t => {
const criteria = [
{ field: "torrent_id", op: "==", value: t }
];
dispatch(filter_subscribe("peer", criteria));
dispatch(filter_subscribe("file", criteria));
dispatch(filter_subscribe("piece", criteria));
dispatch(filter_subscribe("tracker", criteria));
});
}
if (removed.length > 0) {
const subscriptions = getState().filter_subscribe;
removed.forEach(t => {
const serials = subscriptions
.filter(sub => sub.criteria[0] && sub.criteria[0].value == t)
.map(sub => sub.serial);
serials.forEach(serial => dispatch(filter_unsubscribe(serial)));
});
}
}
export const selectop = {
EXCLUSIVE: 1,
UNION: 2,
SUBTRACT: 3
};
export function selectTorrent(t, action = UNION) {
let active = activeTorrents(getState().router);
let removed = [], added = [];
switch (action) {
case selectop.EXCLUSIVE:
removed = active.slice();
active = [t.id];
added = [t.id];
break;
case selectop.UNION:
if (active.indexOf(t.id) === -1) {
active = [...active, t.id];
added = [t.id];
}
break;
case selectop.SUBTRACT:
if (active.indexOf(t.id) !== -1) {
removed = [t.id];
active = active.filter(a => a != t.id);
}
break;
}
const url = active.length === 0 ? "/" : `/torrents/${active.join(',')}`;
if (url !== getState().router.location) {
dispatch(push(url));
}
updateSubscriptions(added, removed);
}

View File

@ -1,6 +1,5 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { activeTorrents } from '../torrent_state';
import FontAwesome from 'react-fontawesome'; import FontAwesome from 'react-fontawesome';
import { import {
ButtonGroup, ButtonGroup,
@ -153,10 +152,10 @@ class TorrentDetails extends Component {
}; };
} }
renderHeader(active) { renderHeader(selection) {
return ( return (
<div> <div>
<h3>{active.length} torrents</h3> <h3>{selection.length} torrents</h3>
<ButtonGroup> <ButtonGroup>
<button className="btn btn-default btn-sm">Pause all</button> <button className="btn btn-default btn-sm">Pause all</button>
<button className="btn btn-default btn-sm">Resume all</button> <button className="btn btn-default btn-sm">Resume all</button>
@ -178,16 +177,14 @@ class TorrentDetails extends Component {
} }
render() { render() {
const active = activeTorrents(); const { torrents, files, selection } = this.props;
const { torrents } = this.props;
const { files } = this.props;
const _files = Object.values(files).reduce((s, f) => ({ const _files = Object.values(files).reduce((s, f) => ({
...s, [f.torrent_id]: [...(s[f.torrent_id] || []), f] ...s, [f.torrent_id]: [...(s[f.torrent_id] || []), f]
}), {}); }), {});
return ( return (
<div> <div>
{active.length > 1 ? this.renderHeader(active) : null} {selection.length > 1 ? this.renderHeader(selection) : null}
{active.map(id => <Torrent {selection.map(id => <Torrent
torrent={torrents[id]} torrent={torrents[id]}
files={_files[id] || []} files={_files[id] || []}
/>)} />)}
@ -199,5 +196,6 @@ class TorrentDetails extends Component {
export default connect(state => ({ export default connect(state => ({
router: state.router, router: state.router,
torrents: state.torrents, torrents: state.torrents,
files: state.files files: state.files,
selection: state.selection
}))(TorrentDetails); }))(TorrentDetails);

View File

@ -1,12 +1,11 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { activeTorrents, selectTorrent, selectop } from '../torrent_state'; import selectTorrent, { UNION, SUBTRACT, EXCLUSIVE } from '../actions/selection';
import { formatBitrate } from '../bitrate'; import { formatBitrate } from '../bitrate';
class TorrentTable extends Component { class TorrentTable extends Component {
render() { render() {
const { torrents } = this.props; const { selection, torrents, dispatch } = this.props;
const active = activeTorrents();
return ( return (
<table className="table"> <table className="table">
<thead> <thead>
@ -25,7 +24,7 @@ class TorrentTable extends Component {
className={`torrent progress-row ${ className={`torrent progress-row ${
t.status t.status
} ${ } ${
active.indexOf(t.id) !== -1 ? "selected" : "" selection.indexOf(t.id) !== -1 ? "selected" : ""
}`} }`}
style={{ style={{
backgroundSize: `${t.progress * 100}% 3px` backgroundSize: `${t.progress * 100}% 3px`
@ -34,22 +33,20 @@ class TorrentTable extends Component {
<td> <td>
<input <input
type="checkbox" type="checkbox"
checked={active.indexOf(t.id) !== -1} checked={selection.indexOf(t.id) !== -1}
onChange={e => onChange={e =>
selectTorrent(t, dispatch(selectTorrent(t.id, e.target.checked ? UNION : SUBTRACT))
e.target.checked ? selectop.UNION : selectop.SUBTRACT)} }
/> />
</td> </td>
<td> <td>
<a <a
href="#" href={`/torrents/${t.id}`}
onClick={e => { onClick={e => {
e.preventDefault(); e.preventDefault();
selectTorrent(t, selectop.EXCLUSIVE); dispatch(selectTorrent(t.id, EXCLUSIVE));
}} }}
> >{t.name}</a>
{t.name}
</a>
</td> </td>
<td>{formatBitrate(t.rate_up)}</td> <td>{formatBitrate(t.rate_up)}</td>
<td>{formatBitrate(t.rate_down)}</td> <td>{formatBitrate(t.rate_down)}</td>
@ -64,5 +61,6 @@ class TorrentTable extends Component {
export default connect(state => ({ export default connect(state => ({
torrents: state.torrents, torrents: state.torrents,
selection: state.selection,
router: state.router router: state.router
}))(TorrentTable); }))(TorrentTable);