aboutsummaryrefslogtreecommitdiff
path: root/src/web
diff options
context:
space:
mode:
Diffstat (limited to 'src/web')
-rw-r--r--src/web/js/components/MiddlePanel.react.js12
-rw-r--r--src/web/js/components/RightPanel.react.js252
-rw-r--r--src/web/js/components/time.react.js15
-rw-r--r--src/web/js/stores/MiddlePanelStore.js3
-rw-r--r--src/web/js/stores/RightPanelStore.js4
-rw-r--r--src/web/models/feed.py2
-rw-r--r--src/web/static/css/one-page-app.css29
-rw-r--r--src/web/views/views.py11
8 files changed, 265 insertions, 63 deletions
diff --git a/src/web/js/components/MiddlePanel.react.js b/src/web/js/components/MiddlePanel.react.js
index 6b3eb427..6bfdaaa9 100644
--- a/src/web/js/components/MiddlePanel.react.js
+++ b/src/web/js/components/MiddlePanel.react.js
@@ -1,6 +1,4 @@
var React = require('react');
-var ReactIntl = require('react-intl');
-var FormattedRelative = ReactIntl.FormattedRelative;
var Row = require('react-bootstrap/Row');
var Button = require('react-bootstrap/Button');
@@ -11,8 +9,9 @@ var MiddlePanelStore = require('../stores/MiddlePanelStore');
var MiddlePanelActions = require('../actions/MiddlePanelActions');
var RightPanelActions = require('../actions/RightPanelActions');
+var JarrTime = require('./time.react');
+
var TableLine = React.createClass({
- mixins: [ReactIntl.IntlMixin],
propTypes: {article_id: React.PropTypes.number.isRequired,
feed_title: React.PropTypes.string.isRequired,
icon_url: React.PropTypes.string,
@@ -49,11 +48,10 @@ var TableLine = React.createClass({
}
// FIXME https://github.com/yahoo/react-intl/issues/189
// use FormattedRelative when fixed, will have to upgrade to ReactIntlv2
- var date = (<time dateTime={this.props.date} title={this.props.date}>
- {this.formatRelative(this.props.timestamp)}
- </time>);
return (<div className={clsses} onClick={this.loadArticle}>
- <h5><strong>{title}</strong></h5>{date}
+ <h5><strong>{title}</strong></h5>
+ <JarrTime text={this.props.date}
+ stamp={this.props.timestamp} />
<div>{read} {liked} {this.props.title}</div>
</div>
);
diff --git a/src/web/js/components/RightPanel.react.js b/src/web/js/components/RightPanel.react.js
index a18f0ac3..2e2aac3e 100644
--- a/src/web/js/components/RightPanel.react.js
+++ b/src/web/js/components/RightPanel.react.js
@@ -1,70 +1,223 @@
var React = require('react');
var Col = require('react-bootstrap/Col');
-var Panel = require('react-bootstrap/Panel');
+var Glyphicon = require('react-bootstrap/Glyphicon');
+var Button = require('react-bootstrap/Button');
+var ButtonGroup = require('react-bootstrap/ButtonGroup');
+
var RightPanelStore = require('../stores/RightPanelStore');
+var JarrTime = require('./time.react');
-var Article = React.createClass({
- propTypes: {article: React.PropTypes.object.isRequired},
- render: function() {
+var PanelMixin = {
+ propTypes: {obj: React.PropTypes.object.isRequired},
+ getHeader: function() {
var icon = null;
- if(this.props.article.icon_url){
- icon = (<img width="16px" src={this.props.article.icon_url} />);
+ if(this.props.obj.icon_url){
+ icon = (<img width="16px" src={this.props.obj.icon_url} />);
+ }
+ var btn_grp = null;
+ if(this.isEditable() || this.isRemovable()) {
+ var edit_button = null;
+ if(this.isEditable()) {
+ edit_button = (<Button onClick={this.onClickEdit}>
+ <Glyphicon glyph="pencil" />
+ </Button>);
+ }
+ var rem_button = null;
+ if(this.isRemovable()) {
+ rem_button = (<Button onClick={this.onClickRemove}>
+ <Glyphicon glyph="remove-sign" />
+ </Button>);
+ }
+ btn_grp = (<ButtonGroup bsSize="small">
+ {edit_button}
+ {rem_button}
+ </ButtonGroup>);
}
- var header = (<h4>
- {icon}<strong>Title:</strong> {this.props.article.title}
- </h4>);
- return (<Panel header={header}>
- <div dangerouslySetInnerHTML={{__html: this.props.article.content}} />
- </Panel>
+ return (<div id="right-panel-heading" className="panel-heading">
+ <h4>{icon}<strong>Title:</strong> {this.getTitle()}</h4>
+ {btn_grp}
+ </div>);
+ },
+ getKey: function(prefix, suffix) {
+ return ((this.state.edit_mode?'edit':'fix') + prefix
+ + '-' + this.props.obj.id + '-' + suffix);
+ },
+ getCore: function() {
+ var items = [];
+ var key;
+ if(!this.state.edit_mode) {
+ this.fields.map(function(field) {
+ key = this.getKey('dt', field.key);
+ items.push(<dt key={key}>{field.title}</dt>);
+ key = this.getKey('dd', field.key);
+ if(field.type == 'string') {
+ items.push(<dd key={key}>{this.props.obj[field.key]}</dd>);
+ } else if(field.type == 'bool') {
+ if(this.props.obj[field.key]) {
+ items.push(<dd key={key}><Glyphicon glyph="ok" /></dd>);
+ } else {
+ items.push(<dd key={key}><Glyphicon glyph="pause" /></dd>);
+ }
+ } else if (field.type == 'link') {
+ items.push(<dd key={key}>
+ <a href={this.props.obj[field.key]}>
+ {this.props.obj[field.key]}
+ </a>
+ </dd>);
+ }
+ }.bind(this));
+ } else {
+ this.fields.map(function(field) {
+ key = this.getKey('dd', field.key);
+ items.push(<dt key={key}>{field.title}</dt>);
+ key = this.getKey('dt', field.key);
+ if(field.type == 'string' || field.type == 'link') {
+ items.push(<dd key={key}><input type="text"
+ defaultValue={this.props.obj[field.key]} />
+ </dd>);
+ } else if (field.type == 'bool') {
+ items.push(<dd key={key}><input type="checkbox"
+ defaultChecked={this.props.obj[field.key]} /></dd>);
+ }
+ }.bind(this));
+ items.push(<dd key={this.getKey('dd', 'submit')}>
+ <button className="btn btn-default">
+ Submit
+ </button>
+ </dd>);
+ }
+ return (<dl className="dl-horizontal">{items}</dl>);
+ },
+ render: function() {
+ return (<div className="panel panel-default">
+ {this.getHeader()}
+ {this.getBody()}
+ </div>
);
},
+ onClickEdit: function() {
+ this.setState({edit_mode: !this.state.edit_mode});
+ },
+ onClickRemove: function() {
+ },
+};
+
+var Article = React.createClass({
+ mixins: [PanelMixin],
+ getInitialState: function() {return {edit_mode: false};},
+ fields: [],
+ isEditable: function() {return false;},
+ isRemovable: function() {return true;},
+ getTitle: function() {return this.props.obj.title;},
+ getBody: function() {
+ return (<div className="panel-body" dangerouslySetInnerHTML={
+ {__html: this.props.obj.content}} />);
+ },
});
var Feed = React.createClass({
- propTypes: {feed: React.PropTypes.object.isRequired},
- render: function() {
- var icon = null;
- if(this.props.feed.icon_url){
- icon = (<img width="16px" src={this.props.feed.icon_url} />);
+ mixins: [PanelMixin],
+ getInitialState: function() {
+ return {edit_mode: false, filters: this.props.obj.filters};
+ },
+ isEditable: function() {return true;},
+ isRemovable: function() {return true;},
+ fields: [{'title': 'Feed title', 'type': 'string', 'key': 'title'},
+ {'title': 'Description', 'type': 'string', 'key': 'description'},
+ {'title': 'Feed link', 'type': 'link', 'key': 'link'},
+ {'title': 'Site link', 'type': 'link', 'key': 'site_link'},
+ {'title': 'Enabled', 'type': 'bool', 'key': 'enabled'},
+ ],
+ getTitle: function() {return this.props.obj.title;},
+ getFilterRow: function(i, filter) {
+ return (<dd key={'d' + i + '-' + this.props.obj.id}
+ className="input-group filter-row">
+ <span className="input-group-btn">
+ <button className="btn btn-default" type="button"
+ data-index={i} onClick={this.removeFilterRow}>
+ <Glyphicon glyph='minus' />
+ </button>
+ </span>
+ <select name="type" className="form-control"
+ defaultValue={filter.type}>
+ <option value='simple match'>simple match</option>
+ <option value='regex'>regex</option>
+ </select>
+ <input type="text" className="form-control"
+ name="pattern" defaultValue={filter.pattern} />
+ <select name="action_on" className="form-control"
+ defaultValue={filter.action_on}>
+ <option value="match">match</option>
+ <option value="no match">no match</option>
+ </select>
+ <select name="action" className="form-control"
+ defaultValue={filter.action}>
+ <option value="mark as read">mark as read</option>
+ <option value="mark as favorite">mark as favorite</option>
+ </select>
+ </dd>);
+ },
+ getBody: function() {
+ var filter_rows = [];
+ for(var i in this.state.filters) {
+ filter_rows.push(this.getFilterRow(i, this.state.filters[i]));
}
- var header = (<h4>
- {icon}<strong>Title:</strong> {this.props.feed.title}
- </h4>);
- return (<Panel collapsible defaultExpanded header={header}>
+ return (<div className="panel-body">
<dl className="dl-horizontal">
- <dt>Description</dt>
- <dd>{this.props.feed.description}</dd>
<dt>Created on</dt>
- <dd>{this.props.feed.created_date}</dd>
- <dt>Feed adress</dt>
- <dd>
- <a href={this.props.feed.link}>
- {this.props.feed.link}
- </a>
+ <dd><JarrTime stamp={this.props.obj.created_stamp}
+ text={this.props.obj.created_date} />
+ </dd>
+ <dt>Last fetched</dt>
+ <dd><JarrTime stamp={this.props.obj.last_stamp}
+ text={this.props.obj.last_retrieved} />
</dd>
- <dt>Site link</dt>
+ </dl>
+ {this.getCore()}
+ <dl className="dl-horizontal">
+ <form>
+ <dt>Filters</dt>
<dd>
- <a href={this.props.feed.site_link}>
- {this.props.feed.site_link}
- </a>
+ <button className="btn btn-default"
+ type="button" onClick={this.addFilterRow}>
+ <Glyphicon glyph='plus' />
+ </button>
+ <button className="btn btn-default">Submit
+ </button>
</dd>
- <dt>Last fetched</dt>
- <dd>{this.props.feed.last_retrieved}</dd>
- <dt>Enabled</dt>
- <dd>{this.props.feed.enabled}</dd>
+ {filter_rows}
+ </form>
</dl>
- </Panel>
+ </div>
);
},
+ addFilterRow: function() {
+ var filters = this.state.filters;
+ filters.push({action: null, action_on: null,
+ type: null, pattern: null});
+ this.setState({filters: filters});
+ },
+ removeFilterRow: function(evnt) {
+ var filters = this.state.filters;
+ delete filters[evnt.target.getAttribute('data-index')];
+ this.setState({filters: filters});
+ },
});
var Category = React.createClass({
- propTypes: {category: React.PropTypes.object.isRequired},
- render: function() {
- return (<Panel header={this.props.category.name}>
- test
- </Panel>
- );
+ mixins: [PanelMixin],
+ getInitialState: function() {return {edit_mode: false};},
+ isEditable: function() {
+ if(this.props.obj.id != 0) {return true;}
+ else {return false;}
+ },
+ isRemovable: function() {return this.isEditable();},
+ fields: [{'title': 'Category name', 'type': 'string', 'key': 'name'}],
+ getTitle: function() {return this.props.obj.name;},
+ getBody: function() {
+ return (<div className="panel-body">
+ {this.getCore()}
+ </div>);
},
});
@@ -112,18 +265,21 @@ var RightPanel = React.createClass({
</ol>);
}
if(this.state.current == 'article') {
- var content = <Article article={this.state.article} />;
+ var cntnt = (<Article type='article' obj={this.state.article}
+ key={this.state.article.id} />);
} else if(this.state.current == 'feed') {
- var content = <Feed feed={this.state.feed} />;
+ var cntnt = (<Feed type='feed' obj={this.state.feed}
+ key={this.state.feed.id} />);
} else if(this.state.current == 'category') {
- var content = <Category category={this.state.category} />;
+ var cntnt = (<Category type='category' obj={this.state.category}
+ key={this.state.category.id} />);
}
return (<Col id="right-panel" xsOffset={2} smOffset={2}
mdOffset={7} lgOffset={6}
xs={10} sm={10} md={5} lg={6}>
{breadcrum}
- {content}
+ {cntnt}
</Col>
);
},
diff --git a/src/web/js/components/time.react.js b/src/web/js/components/time.react.js
new file mode 100644
index 00000000..8b4d47d9
--- /dev/null
+++ b/src/web/js/components/time.react.js
@@ -0,0 +1,15 @@
+var React = require('react');
+var ReactIntl = require('react-intl');
+
+var JarrTime = React.createClass({
+ mixins: [ReactIntl.IntlMixin],
+ propTypes: {stamp: React.PropTypes.number.isRequired,
+ text: React.PropTypes.string.isRequired},
+ render: function() {
+ return (<time dateTime={this.props.text} title={this.props.text}>
+ {this.formatRelative(this.props.stamp)}
+ </time>);
+ },
+});
+
+module.exports = JarrTime;
diff --git a/src/web/js/stores/MiddlePanelStore.js b/src/web/js/stores/MiddlePanelStore.js
index 1a0a4fab..4a5efd00 100644
--- a/src/web/js/stores/MiddlePanelStore.js
+++ b/src/web/js/stores/MiddlePanelStore.js
@@ -119,9 +119,10 @@ MiddlePanelStore.dispatchToken = JarrDispatcher.register(function(action) {
for (var i in MiddlePanelStore._datas.articles) {
if(MiddlePanelStore._datas.articles[i].article_id == action.article.id) {
MiddlePanelStore._datas.articles[i].read = true;
- console.log(MiddlePanelStore._datas.articles[i]);
+ break;
}
}
+ break;
default:
// pass
}
diff --git a/src/web/js/stores/RightPanelStore.js b/src/web/js/stores/RightPanelStore.js
index 68d3b7e9..85f336e4 100644
--- a/src/web/js/stores/RightPanelStore.js
+++ b/src/web/js/stores/RightPanelStore.js
@@ -30,17 +30,19 @@ RightPanelStore.dispatchToken = JarrDispatcher.register(function(action) {
if(action.filter_id == null) {
RightPanelStore._datas.category = null;
RightPanelStore._datas.feed = null;
+ RightPanelStore._datas.current = null;
} else if(action.filter_type == 'category_id') {
RightPanelStore._datas.category = MenuStore._datas.categories[action.filter_id];
RightPanelStore._datas.feed = null;
RightPanelStore._datas.current = 'category';
+ RightPanelStore.emitChange();
} else {
RightPanelStore._datas.feed = MenuStore._datas.feeds[action.filter_id];
RightPanelStore._datas.category = MenuStore._datas.categories[RightPanelStore._datas.feed.category_id];
RightPanelStore._datas.current = 'feed';
+ RightPanelStore.emitChange();
}
- RightPanelStore.emitChange();
break;
case ActionTypes.LOAD_ARTICLE:
RightPanelStore._datas.feed = MenuStore._datas.feeds[action.article.feed_id];
diff --git a/src/web/models/feed.py b/src/web/models/feed.py
index a9c49282..c5fcbe4c 100644
--- a/src/web/models/feed.py
+++ b/src/web/models/feed.py
@@ -73,6 +73,8 @@ class Feed(db.Model):
"link": self.link,
"site_link": self.site_link,
"etag": self.etag,
+ "enabled": self.enabled,
+ "filters": self.filters,
"icon_url": self.icon_url,
"error_count": self.error_count,
"created_date": self.created_date,
diff --git a/src/web/static/css/one-page-app.css b/src/web/static/css/one-page-app.css
index 9f559ee0..d919a4f7 100644
--- a/src/web/static/css/one-page-app.css
+++ b/src/web/static/css/one-page-app.css
@@ -120,7 +120,6 @@
position: absolute;
top: 2px;
right: 4px;
- display: block;
}
#right-panel>ol{
margin-top: 10px;
@@ -128,3 +127,31 @@
#right-panel .panel-body img {
max-width: 100%;
}
+#right-panel-heading * {
+ display: inline;
+}
+#right-panel-heading h4 img {
+ margin-right: 5px;
+}
+#right-panel-heading div.btn-group {
+ float: right;
+ margin-top: -5px;
+ margin-right: -8px;
+}
+.panel-body dd>input {
+ width: 100%;
+}
+.filter-row>select.form-control:first-child {
+ width: 10%;
+}
+.filter-row>select.form-control {
+ width: 15%;
+}
+.filter-row>select.form-control:last-child {
+ width: 25%;
+}
+.filter-row>input.form-control {
+ width: auto;
+}
+#filter-button-row>*{
+}
diff --git a/src/web/views/views.py b/src/web/views/views.py
index 49554cb3..f8549e93 100644
--- a/src/web/views/views.py
+++ b/src/web/views/views.py
@@ -230,6 +230,7 @@ def signup():
return render_template('signup.html', form=form)
+from calendar import timegm
from flask import jsonify
@@ -250,8 +251,12 @@ def get_menu():
categories[cat_id]['feeds'] = []
feeds = {feed.id: feed.dump() for feed in FeedController(g.user.id).read()}
for feed_id, feed in feeds.items():
+ feed['created_stamp'] = timegm(feed['created_date'].timetuple()) * 1000
+ feed['last_stamp'] = timegm(feed['last_retrieved'].timetuple()) * 1000
feed['category_id'] = feed['category_id'] or 0
feed['unread'] = unread.get(feed['id'], 0)
+ if not feed['filters']:
+ feed['filters'] = []
if feed.get('icon_url'):
feed['icon_url'] = url_for('icon.icon', url=feed['icon_url'])
categories[feed['category_id']]['unread'] += feed['unread']
@@ -284,17 +289,13 @@ def _get_filters(in_dict):
return filters
-import calendar
-
-
def _articles_to_json(articles, fd_hash=None):
return jsonify(**{'articles': [{'title': art.title, 'liked': art.like,
'read': art.readed, 'article_id': art.id, 'selected': False,
'feed_id': art.feed_id, 'category_id': art.category_id or 0,
'feed_title': fd_hash[art.feed_id]['title'] if fd_hash else None,
'icon_url': fd_hash[art.feed_id]['icon_url'] if fd_hash else None,
- 'date': art.date,
- 'timestamp': calendar.timegm(art.date.timetuple()) * 1000}
+ 'date': art.date, 'timestamp': timegm(art.date.timetuple()) * 1000}
for art in articles.limit(1000)]})
bgstack15