show unread/starred count

This commit is contained in:
Nazar Kanaev 2020-07-14 10:30:02 +01:00
parent ffd2deb5d8
commit 76a08df741
5 changed files with 81 additions and 5 deletions

View File

@ -45,7 +45,7 @@ func StaticHandler(rw http.ResponseWriter, req *http.Request) {
func StatusHandler(rw http.ResponseWriter, req *http.Request) { func StatusHandler(rw http.ResponseWriter, req *http.Request) {
writeJSON(rw, map[string]interface{}{ writeJSON(rw, map[string]interface{}{
"running": handler(req).fetchRunning, "running": handler(req).fetchRunning,
"stats": map[string]int64{}, "stats": db(req).FeedStats(),
}) })
} }

View File

@ -213,3 +213,31 @@ func (s *Storage) MarkItemsRead(filter ItemFilter) bool {
} }
return err == nil 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
}

View File

@ -25,7 +25,7 @@
<div class="menu-item d-flex align-items-center w-100"> <div class="menu-item d-flex align-items-center w-100">
<img src="./static/images/circle.svg" alt="" class="nav-icon"> <img src="./static/images/circle.svg" alt="" class="nav-icon">
<span class="flex-fill text-left text-truncate">Unread</span> <span class="flex-fill text-left text-truncate">Unread</span>
<span class="counter text-right"></span> <span class="counter text-right">{{totalStats.unread || ''}}</span>
</div> </div>
</label> </label>
<label class="nav-select"> <label class="nav-select">
@ -33,7 +33,7 @@
<div class="menu-item d-flex align-items-center w-100"> <div class="menu-item d-flex align-items-center w-100">
<img src="./static/images/star.svg" alt="" class="nav-icon"> <img src="./static/images/star.svg" alt="" class="nav-icon">
<span class="flex-fill text-left text-truncate">Starred</span> <span class="flex-fill text-left text-truncate">Starred</span>
<span class="counter text-right"></span> <span class="counter text-right">{{totalStats.starred || ''}}</span>
</div> </div>
</label> </label>
</div> </div>
@ -54,7 +54,7 @@
:class="{expanded: folder.is_expanded}" :class="{expanded: folder.is_expanded}"
@click.prevent="toggleFolderExpanded(folder)"> @click.prevent="toggleFolderExpanded(folder)">
<span class="flex-fill text-left text-truncate">{{ folder.title }}</span> <span class="flex-fill text-left text-truncate">{{ folder.title }}</span>
<span class="counter text-right"></span> <span class="counter text-right">{{filteredFolderStats[folder.id] || ''}}</span>
</div> </div>
</label> </label>
<div v-show="!folder.id || folder.is_expanded" class="mt-1" :class="{'pl-3': folder.id}"> <div v-show="!folder.id || folder.is_expanded" class="mt-1" :class="{'pl-3': folder.id}">
@ -64,7 +64,7 @@
<div class="menu-item d-flex align-items-center w-100"> <div class="menu-item d-flex align-items-center w-100">
<img src="./static/images/rss.svg" alt="" class="nav-icon"> <img src="./static/images/rss.svg" alt="" class="nav-icon">
<span class="flex-fill text-left text-truncate">{{ feed.title }}</span> <span class="flex-fill text-left text-truncate">{{ feed.title }}</span>
<span class="counter text-right"></span> <span class="counter text-right">{{filteredFeedStats[feed.id] || ''}}</span>
</div> </div>
</label> </label>
</div> </div>

View File

@ -74,6 +74,9 @@
return api('put', '/api/settings', data) return api('put', '/api/settings', data)
}, },
}, },
status: function() {
return api('get', '/api/status').then(json)
},
upload_opml: function(form) { upload_opml: function(form) {
return fetch('/opml/import', { return fetch('/opml/import', {
method: 'post', method: 'post',

View File

@ -28,6 +28,7 @@ var vm = new Vue({
vm.refreshItems() vm.refreshItems()
}) })
this.refreshFeeds() this.refreshFeeds()
this.refreshStats()
}, },
data: function() { data: function() {
return { return {
@ -47,6 +48,7 @@ var vm = new Vue({
'newfeed': false, 'newfeed': false,
'items': false, 'items': false,
}, },
'feedStats': {},
} }
}, },
computed: { computed: {
@ -71,6 +73,34 @@ var vm = new Vue({
itemsById: function() { itemsById: function() {
return this.items.reduce(function(acc, item) { acc[item.id] = item; return acc }, {}) 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: { watch: {
'filterSelected': function(newVal, oldVal) { 'filterSelected': function(newVal, oldVal) {
@ -87,11 +117,21 @@ var vm = new Vue({
this.itemSelectedDetails = this.itemsById[newVal] this.itemSelectedDetails = this.itemsById[newVal]
if (this.itemSelectedDetails.status == 'unread') { if (this.itemSelectedDetails.status == 'unread') {
this.itemSelectedDetails.status = 'read' this.itemSelectedDetails.status = 'read'
this.feedStats[this.itemSelectedDetails.feed_id].unread -= 1
api.items.update(this.itemSelectedDetails.id, {status: this.itemSelectedDetails.status}) api.items.update(this.itemSelectedDetails.id, {status: this.itemSelectedDetails.status})
} }
}, },
}, },
methods: { 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() { getItemsQuery: function() {
var query = {} var query = {}
if (this.feedSelected) { if (this.feedSelected) {
@ -148,6 +188,7 @@ var vm = new Vue({
var query = this.getItemsQuery() var query = this.getItemsQuery()
api.items.mark_read(query).then(function() { api.items.mark_read(query).then(function() {
vm.items = [] vm.items = []
vm.refreshStats()
}) })
}, },
toggleFolderExpanded: function(folder) { toggleFolderExpanded: function(folder) {
@ -227,16 +268,20 @@ var vm = new Vue({
toggleItemStarred: function(item) { toggleItemStarred: function(item) {
if (item.status == 'starred') { if (item.status == 'starred') {
item.status = 'read' item.status = 'read'
this.feedStats[item.feed_id].starred -= 1
} else if (item.status != 'starred') { } else if (item.status != 'starred') {
item.status = 'starred' item.status = 'starred'
this.feedStats[item.feed_id].starred += 1
} }
api.items.update(item.id, {status: item.status}) api.items.update(item.id, {status: item.status})
}, },
toggleItemRead: function(item) { toggleItemRead: function(item) {
if (item.status == 'unread') { if (item.status == 'unread') {
item.status = 'read' item.status = 'read'
this.feedStats[item.feed_id].unread -= 1
} else if (item.status == 'read') { } else if (item.status == 'read') {
item.status = 'unread' item.status = 'unread'
this.feedStats[item.feed_id].unread += 1
} }
api.items.update(item.id, {status: item.status}) api.items.update(item.id, {status: item.status})
}, },