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}) },