mirror of
https://github.com/zoriya/flood.git
synced 2026-06-04 03:27:15 +00:00
Add notification system
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
$notification--background: rgba($background, 0.95);
|
||||
$notification--foreground: #8fa2b2;
|
||||
|
||||
.notifications {
|
||||
|
||||
&__list {
|
||||
background: $notification--background;
|
||||
border-radius: 3px;
|
||||
bottom: $spacing-unit * 1/5;
|
||||
color: $notification--foreground;
|
||||
font-size: 0.85rem;
|
||||
padding: $spacing-unit * 2/5 $spacing-unit * 3/5;
|
||||
position: fixed;
|
||||
right: $spacing-unit * 1/5;
|
||||
transition: opacity 0.25s;
|
||||
width: 250px;
|
||||
z-index: 1000;
|
||||
|
||||
&-leave {
|
||||
opacity: 1;
|
||||
|
||||
&-active {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&-enter {
|
||||
opacity: 0;
|
||||
|
||||
&-active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ $torrent-details--header--icon--default--fill: rgba(#4d6f87, 0.5);
|
||||
|
||||
$torrent-details--detail--label--foreground: #527893;
|
||||
|
||||
$directory-tree--filename--foreground: rgba(#527893, 0.7);
|
||||
$directory-tree--foreground: #527893;
|
||||
$directory-tree--directory--foreground: #527893;
|
||||
$directory-tree--directory--foreground--open: darken($directory-tree--directory--foreground, 5%);
|
||||
|
||||
@@ -159,9 +159,9 @@ $torrent-details--directory-tree--parent-directory--icon--fill: rgba(#527893, 0.
|
||||
margin-left: -8px;
|
||||
|
||||
.directory-tree {
|
||||
color: $directory-tree--filename--foreground;
|
||||
|
||||
&__node {
|
||||
color: $directory-tree--foreground;
|
||||
|
||||
&--group {
|
||||
|
||||
|
||||
@@ -105,6 +105,7 @@ $more-info--border: $textbox-repeater--button--border;
|
||||
|
||||
&__more-info {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
@@ -127,6 +128,7 @@ $more-info--border: $textbox-repeater--button--border;
|
||||
margin-top: -16px;
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
right: -5px;
|
||||
top: 50%;
|
||||
transform: translateX(15px);
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
@import "components/icons";
|
||||
@import "components/loading-indicator";
|
||||
@import "components/modals";
|
||||
@import "components/notifications";
|
||||
@import "components/priority-meter";
|
||||
@import "components/progress-bar";
|
||||
@import "components/scrollbars";
|
||||
|
||||
@@ -13,6 +13,7 @@ const TorrentActions = {
|
||||
AppDispatcher.dispatchServerAction({
|
||||
type: ActionTypes.CLIENT_ADD_TORRENT_SUCCESS,
|
||||
data: {
|
||||
request: options,
|
||||
response
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import Application from './components/layout/Application';
|
||||
import ApplicationContent from './components/layout/ApplicationContent';
|
||||
import ApplicationLoadingIndicator from './components/layout/ApplicationLoadingIndicator';
|
||||
import Modals from './components/modals/Modals';
|
||||
import Notifications from './components/notifications/Notifications';
|
||||
import Sidebar from './components/panels/Sidebar';
|
||||
import SettingsStore from './stores/SettingsStore';
|
||||
import TorrentActions from './actions/TorrentActions';
|
||||
@@ -24,6 +25,7 @@ class FloodApp extends React.Component {
|
||||
<TorrentListView />
|
||||
</ApplicationContent>
|
||||
<Modals />
|
||||
<Notifications />
|
||||
</Application>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import classnames from 'classnames';
|
||||
import CSSTransitionGroup from 'react-addons-css-transition-group';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import EventTypes from '../../constants/EventTypes';
|
||||
import NotificationStore from '../../stores/NotificationStore';
|
||||
|
||||
const METHODS_TO_BIND = ['handleNotificationChange'];
|
||||
|
||||
export default class NotificationList extends React.Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
|
||||
this.state = {
|
||||
notifications: []
|
||||
};
|
||||
|
||||
METHODS_TO_BIND.forEach((method) => {
|
||||
this[method] = this[method].bind(this);
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
NotificationStore.listen(EventTypes.NOTIFICATIONS_CHANGE, this.handleNotificationChange);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
NotificationStore.unlisten(EventTypes.NOTIFICATIONS_CHANGE, this.handleNotificationChange);
|
||||
}
|
||||
|
||||
getNotifications() {
|
||||
return this.state.notifications.map((notification, index) => {
|
||||
return (
|
||||
<li className="notifications__list__item" key={index}>
|
||||
{notification.content}
|
||||
</li>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
handleNotificationChange() {
|
||||
this.setState({notifications: NotificationStore.getNotifications()});
|
||||
}
|
||||
|
||||
render() {
|
||||
let notifications = null;
|
||||
|
||||
if (this.state.notifications.length > 0) {
|
||||
notifications = (
|
||||
<ul className="notifications__list" key="notifications-list">
|
||||
{this.getNotifications()}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CSSTransitionGroup transitionName="notifications__list"
|
||||
transitionEnterTimeout={250} transitionLeaveTimeout={250}
|
||||
className="notifications">
|
||||
{notifications}
|
||||
</CSSTransitionGroup>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,47 +1,13 @@
|
||||
import classnames from 'classnames';
|
||||
import React from 'react';
|
||||
|
||||
import ActionBar from '../torrent-list/ActionBar';
|
||||
import ApplicationPanel from '../layout/ApplicationPanel';
|
||||
import EventTypes from '../../constants/EventTypes';
|
||||
import TorrentListContainer from '../torrent-list/TorrentListContainer';
|
||||
import TorrentStore from '../../stores/TorrentStore';
|
||||
import UIStore from '../../stores/UIStore';
|
||||
|
||||
const METHODS_TO_BIND = ['onOpenChange'];
|
||||
|
||||
class TorrentListView extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
isTorrentDetailsOpen: false
|
||||
};
|
||||
|
||||
METHODS_TO_BIND.forEach((method) => {
|
||||
this[method] = this[method].bind(this);
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
UIStore.listen(EventTypes.UI_TORRENT_DETAILS_OPEN_CHANGE, this.onOpenChange);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
UIStore.unlisten(EventTypes.UI_TORRENT_DETAILS_OPEN_CHANGE, this.onOpenChange);
|
||||
}
|
||||
|
||||
onOpenChange() {
|
||||
this.setState({
|
||||
isTorrentDetailsOpen: UIStore.isTorrentDetailsOpen()
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let classes = classnames({'is-open': this.state.isTorrentDetailsOpen}, 'view--torrent-list');
|
||||
|
||||
return (
|
||||
<ApplicationPanel modifier="torrent-list" className={classes}>
|
||||
<ApplicationPanel modifier="torrent-list" className="view--torrent-list">
|
||||
<ActionBar />
|
||||
<TorrentListContainer />
|
||||
</ApplicationPanel>
|
||||
|
||||
@@ -16,6 +16,7 @@ const EventTypes = {
|
||||
CLIENT_TRANSFER_DATA_REQUEST_ERROR: 'CLIENT_TRANSFER_DATA_REQUEST_ERROR',
|
||||
CLIENT_TRANSFER_HISTORY_REQUEST_SUCCESS: 'CLIENT_TRANSFER_HISTORY_REQUEST_SUCCESS',
|
||||
CLIENT_TRANSFER_HISTORY_REQUEST_ERROR: 'CLIENT_TRANSFER_HISTORY_REQUEST_ERROR',
|
||||
NOTIFICATIONS_CHANGE: 'NOTIFICATIONS_CHANGE',
|
||||
SETTINGS_FETCH_REQUEST_SUCCESS: 'SETTINGS_FETCH_REQUEST_SUCCESS',
|
||||
SETTINGS_FETCH_REQUEST_ERROR: 'SETTINGS_FETCH_REQUEST_ERROR',
|
||||
UI_CONTEXT_MENU_CHANGE: 'UI_CONTEXT_MENU_CHANGE',
|
||||
|
||||
@@ -2,7 +2,7 @@ import {EventEmitter} from 'events';
|
||||
|
||||
export default class BaseStore extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
super(...arguments);
|
||||
|
||||
this.dispatcherID = null;
|
||||
this.on('uncaughtException', this.handleError);
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import ActionTypes from '../constants/ActionTypes';
|
||||
import AppDispatcher from '../dispatcher/AppDispatcher';
|
||||
import BaseStore from './BaseStore';
|
||||
import EventTypes from '../constants/EventTypes';
|
||||
|
||||
const DEFAULT_DURATION = 5 * 1000;
|
||||
|
||||
class NotificationStoreClass extends BaseStore {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.notifications = {};
|
||||
this.accumulation = {};
|
||||
}
|
||||
|
||||
accumulate(notification) {
|
||||
let {id, value} = notification.accumulation;
|
||||
|
||||
if (this.accumulation[id] == null) {
|
||||
this.accumulation[id] = value;
|
||||
} else {
|
||||
this.accumulation[id] += value;
|
||||
}
|
||||
}
|
||||
|
||||
add(notification) {
|
||||
notification.duration = this.getDuration(notification);
|
||||
notification.id = this.getID(notification);
|
||||
|
||||
if (notification.content == null) {
|
||||
throw new Error('Notification content cannot be empty.');
|
||||
}
|
||||
|
||||
if (!!notification.accumulation) {
|
||||
this.accumulate(notification);
|
||||
}
|
||||
|
||||
this.scheduleCleanse(notification);
|
||||
|
||||
this.notifications[notification.id] = {
|
||||
content: this.getContent(notification)
|
||||
};
|
||||
|
||||
this.emit(EventTypes.NOTIFICATIONS_CHANGE);
|
||||
}
|
||||
|
||||
getContent(notification) {
|
||||
if (!!notification.accumulation) {
|
||||
return notification.content(this.accumulation[notification.accumulation.id]);
|
||||
}
|
||||
|
||||
return notification.content;
|
||||
}
|
||||
|
||||
getDuration(notification) {
|
||||
return notification.duration || DEFAULT_DURATION;
|
||||
}
|
||||
|
||||
getNotifications() {
|
||||
let notificationIDs = Object.keys(this.notifications).sort();
|
||||
|
||||
return notificationIDs.map((id) => {
|
||||
return this.notifications[id];
|
||||
});
|
||||
}
|
||||
|
||||
getID(notification) {
|
||||
return notification.id || Date.now();
|
||||
}
|
||||
|
||||
removeExpired(notification) {
|
||||
let {accumulation} = notification;
|
||||
|
||||
if (!!accumulation) {
|
||||
this.removeAccumulation(notification);
|
||||
|
||||
if (this.accumulation[accumulation.id] === 0) {
|
||||
delete this.accumulation[accumulation.id];
|
||||
delete this.notifications[notification.id];
|
||||
}
|
||||
} else {
|
||||
delete this.notifications[notification.id];
|
||||
}
|
||||
|
||||
this.emit(EventTypes.NOTIFICATIONS_CHANGE);
|
||||
}
|
||||
|
||||
removeAccumulation(notification) {
|
||||
let {id, value} = notification.accumulation;
|
||||
|
||||
if (this.accumulation[id] == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.accumulation[id] -= value;
|
||||
}
|
||||
|
||||
scheduleCleanse(notification) {
|
||||
setTimeout(this.removeExpired.bind(this, notification),
|
||||
notification.duration);
|
||||
}
|
||||
}
|
||||
|
||||
let NotificationStore = new NotificationStoreClass();
|
||||
|
||||
NotificationStore.dispatcherID = AppDispatcher.register((payload) => {
|
||||
// const {action, source} = payload;
|
||||
|
||||
// switch (action.type) {
|
||||
// }
|
||||
});
|
||||
|
||||
export default NotificationStore;
|
||||
@@ -6,6 +6,7 @@ import BaseStore from './BaseStore';
|
||||
import config from '../../../../config';
|
||||
import EventTypes from '../constants/EventTypes';
|
||||
import {filterTorrents} from '../util/filterTorrents';
|
||||
import NotificationStore from './NotificationStore';
|
||||
import {searchTorrents} from '../util/searchTorrents';
|
||||
import {selectTorrents} from '../util/selectTorrents';
|
||||
import {sortTorrents} from '../util/sortTorrents';
|
||||
@@ -99,8 +100,23 @@ class TorrentStoreClass extends BaseStore {
|
||||
this.emit(EventTypes.CLIENT_ADD_TORRENT_ERROR);
|
||||
}
|
||||
|
||||
handleAddTorrentSuccess() {
|
||||
handleAddTorrentSuccess(responseData) {
|
||||
this.emit(EventTypes.CLIENT_ADD_TORRENT_SUCCESS);
|
||||
|
||||
NotificationStore.add({
|
||||
content: function (count = 0) {
|
||||
if (count === 1) {
|
||||
return 'Successfully added torrent.';
|
||||
}
|
||||
|
||||
return `Successfully added ${count} torrents.`;
|
||||
},
|
||||
accumulation: {
|
||||
id: 'add-torrents',
|
||||
value: responseData.request.urls.length || 1
|
||||
},
|
||||
id: 'add-torrents'
|
||||
});
|
||||
}
|
||||
|
||||
getTorrent(hash) {
|
||||
|
||||
@@ -15,16 +15,8 @@ class UIStoreClass extends BaseStore {
|
||||
this.dependencies = [];
|
||||
this.latestTorrentLocation = null;
|
||||
this.torrentDetailsHash = null;
|
||||
// this.torrentDetailsOpen = false;
|
||||
}
|
||||
|
||||
// closeTorrentDetailsPanel() {
|
||||
// if (this.torrentDetailsOpen) {
|
||||
// this.torrentDetailsOpen = false;
|
||||
// this.emit(EventTypes.UI_TORRENT_DETAILS_OPEN_CHANGE);
|
||||
// }
|
||||
// }
|
||||
|
||||
getActiveContextMenu() {
|
||||
return this.activeContextMenu;
|
||||
}
|
||||
@@ -46,19 +38,10 @@ class UIStoreClass extends BaseStore {
|
||||
this.emit(EventTypes.UI_TORRENT_DETAILS_HASH_CHANGE);
|
||||
}
|
||||
|
||||
handleTorrentDetailsClick(hash, event) {
|
||||
this.torrentDetailsOpen = !this.torrentDetailsOpen;
|
||||
this.emit(EventTypes.UI_TORRENT_DETAILS_OPEN_CHANGE);
|
||||
}
|
||||
|
||||
hasSatisfiedDependencies() {
|
||||
return this.dependencies.length === 0;
|
||||
}
|
||||
|
||||
// isTorrentDetailsOpen() {
|
||||
// return this.torrentDetailsOpen;
|
||||
// }
|
||||
|
||||
registerDependency(ids) {
|
||||
if (!Array.isArray(ids)) {
|
||||
ids = [ids];
|
||||
@@ -104,9 +87,6 @@ UIStore.dispatcherID = AppDispatcher.register((payload) => {
|
||||
const {action, source} = payload;
|
||||
|
||||
switch (action.type) {
|
||||
// case ActionTypes.UI_CLICK_TORRENT_DETAILS:
|
||||
// UIStore.handleTorrentDetailsClick(action.data.hash, action.data.event);
|
||||
// break;
|
||||
case ActionTypes.UI_CLICK_TORRENT:
|
||||
UIStore.handleTorrentClick(action.data.hash);
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user