diff --git a/server/handlers.go b/server/handlers.go
index 9eb4307..980391c 100644
--- a/server/handlers.go
+++ b/server/handlers.go
@@ -45,7 +45,7 @@ func StaticHandler(rw http.ResponseWriter, req *http.Request) {
func StatusHandler(rw http.ResponseWriter, req *http.Request) {
writeJSON(rw, map[string]interface{}{
"running": handler(req).fetchRunning,
- "stats": map[string]int64{},
+ "stats": db(req).FeedStats(),
})
}
diff --git a/storage/item.go b/storage/item.go
index 1b93841..c6bb24d 100644
--- a/storage/item.go
+++ b/storage/item.go
@@ -213,3 +213,31 @@ func (s *Storage) MarkItemsRead(filter ItemFilter) bool {
}
return err == nil
}
+
+type FeedStat struct {
+ FeedId int64 `json:"feed_id"`
+ UnreadCount int64 `json:"unread"`
+ StarredCount int64 `json:"starred"`
+}
+
+func (s *Storage) FeedStats() []FeedStat {
+ result := make([]FeedStat, 0)
+ rows, err := s.db.Query(fmt.Sprintf(`
+ select
+ feed_id,
+ sum(case status when %d then 1 else 0 end),
+ sum(case status when %d then 1 else 0 end)
+ from items
+ group by feed_id
+ `, UNREAD, STARRED))
+ if err != nil {
+ s.log.Print(err)
+ return result
+ }
+ for rows.Next() {
+ stat := FeedStat{}
+ rows.Scan(&stat.FeedId, &stat.UnreadCount, &stat.StarredCount)
+ result = append(result, stat)
+ }
+ return result
+}
diff --git a/template/index.html b/template/index.html
index fb8590c..e5230b8 100644
--- a/template/index.html
+++ b/template/index.html
@@ -25,7 +25,7 @@
@@ -54,7 +54,7 @@
:class="{expanded: folder.is_expanded}"
@click.prevent="toggleFolderExpanded(folder)">
{{ folder.title }}
-
+ {{filteredFolderStats[folder.id] || ''}}
@@ -64,7 +64,7 @@
diff --git a/template/static/javascripts/api.js b/template/static/javascripts/api.js
index d3f556d..80714cd 100644
--- a/template/static/javascripts/api.js
+++ b/template/static/javascripts/api.js
@@ -74,6 +74,9 @@
return api('put', '/api/settings', data)
},
},
+ status: function() {
+ return api('get', '/api/status').then(json)
+ },
upload_opml: function(form) {
return fetch('/opml/import', {
method: 'post',
diff --git a/template/static/javascripts/app.js b/template/static/javascripts/app.js
index bea0536..7e81797 100644
--- a/template/static/javascripts/app.js
+++ b/template/static/javascripts/app.js
@@ -28,6 +28,7 @@ var vm = new Vue({
vm.refreshItems()
})
this.refreshFeeds()
+ this.refreshStats()
},
data: function() {
return {
@@ -47,6 +48,7 @@ var vm = new Vue({
'newfeed': false,
'items': false,
},
+ 'feedStats': {},
}
},
computed: {
@@ -71,6 +73,34 @@ var vm = new Vue({
itemsById: function() {
return this.items.reduce(function(acc, item) { acc[item.id] = item; return acc }, {})
},
+ filteredFeedStats: function() {
+ var filter = this.filterSelected
+ if (filter != 'unread' && filter != 'starred') return {}
+
+ var feedStats = this.feedStats
+ return this.feeds.reduce(function(acc, feed) {
+ if (feedStats[feed.id]) acc[feed.id] = vm.feedStats[feed.id][filter]
+ return acc
+ }, {})
+ },
+ filteredFolderStats: function() {
+ var filter = this.filterSelected
+ if (filter != 'unread' && filter != 'starred') return {}
+
+ var feedStats = this.filteredFeedStats
+ return this.feeds.reduce(function(acc, feed) {
+ if (!acc[feed.folder_id]) acc[feed.folder_id] = 0
+ if (feedStats[feed.id]) acc[feed.folder_id] += feedStats[feed.id]
+ return acc
+ }, {})
+ },
+ totalStats: function() {
+ return Object.values(this.feedStats).reduce(function(acc, stat) {
+ acc.unread += stat.unread
+ acc.starred += stat.starred
+ return acc
+ }, {unread: 0, starred: 0})
+ },
},
watch: {
'filterSelected': function(newVal, oldVal) {
@@ -87,11 +117,21 @@ var vm = new Vue({
this.itemSelectedDetails = this.itemsById[newVal]
if (this.itemSelectedDetails.status == 'unread') {
this.itemSelectedDetails.status = 'read'
+ this.feedStats[this.itemSelectedDetails.feed_id].unread -= 1
api.items.update(this.itemSelectedDetails.id, {status: this.itemSelectedDetails.status})
}
},
},
methods: {
+ refreshStats: function() {
+ var vm = this
+ api.status().then(function(data) {
+ vm.feedStats = data.stats.reduce(function(acc, stat) {
+ acc[stat.feed_id] = stat
+ return acc
+ }, {})
+ })
+ },
getItemsQuery: function() {
var query = {}
if (this.feedSelected) {
@@ -148,6 +188,7 @@ var vm = new Vue({
var query = this.getItemsQuery()
api.items.mark_read(query).then(function() {
vm.items = []
+ vm.refreshStats()
})
},
toggleFolderExpanded: function(folder) {
@@ -227,16 +268,20 @@ var vm = new Vue({
toggleItemStarred: function(item) {
if (item.status == 'starred') {
item.status = 'read'
+ this.feedStats[item.feed_id].starred -= 1
} else if (item.status != 'starred') {
item.status = 'starred'
+ this.feedStats[item.feed_id].starred += 1
}
api.items.update(item.id, {status: item.status})
},
toggleItemRead: function(item) {
if (item.status == 'unread') {
item.status = 'read'
+ this.feedStats[item.feed_id].unread -= 1
} else if (item.status == 'read') {
item.status = 'unread'
+ this.feedStats[item.feed_id].unread += 1
}
api.items.update(item.id, {status: item.status})
},