mirror of
https://github.com/yang991178/fluent-reader.git
synced 2025-02-01 01:47:07 +01:00
unread count
This commit is contained in:
parent
69f5a21038
commit
6e7f2cf915
16
dist/styles.css
vendored
16
dist/styles.css
vendored
@ -240,6 +240,22 @@ nav.menu-on .btn-group .btn.system, nav.item-on .btn-group .btn.system {
|
||||
margin: 2px 8px;
|
||||
user-select: none;
|
||||
}
|
||||
.menu .link-stack {
|
||||
overflow: hidden;
|
||||
}
|
||||
.menu .link-text {
|
||||
margin-top: 0px;
|
||||
margin-right: 4px;
|
||||
margin-bottom: 0px;
|
||||
margin-left: 4px;
|
||||
text-align: left;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.menu .unread-count {
|
||||
color: var(--neutralSecondary);
|
||||
}
|
||||
|
||||
.settings-container {
|
||||
position: fixed;
|
||||
|
BIN
screenshot.jpg
Normal file
BIN
screenshot.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 318 KiB |
@ -5,7 +5,7 @@ import { Nav, INavLink, INavLinkGroup } from "office-ui-fabric-react/lib/Nav"
|
||||
import { SourceGroup } from "../scripts/models/group"
|
||||
import { SourceState, RSSSource } from "../scripts/models/source"
|
||||
import { ALL } from "../scripts/models/feed"
|
||||
import { AnimationClassNames } from "@fluentui/react"
|
||||
import { AnimationClassNames, Stack } from "@fluentui/react"
|
||||
|
||||
export type MenuProps = {
|
||||
status: boolean,
|
||||
@ -20,6 +20,8 @@ export type MenuProps = {
|
||||
}
|
||||
|
||||
export class Menu extends React.Component<MenuProps> {
|
||||
countOverflow = (count: number) => count >= 1000 ? "999+" : String(count)
|
||||
|
||||
getItems = (): INavLinkGroup[] => [
|
||||
{
|
||||
links: [
|
||||
@ -31,6 +33,7 @@ export class Menu extends React.Component<MenuProps> {
|
||||
},
|
||||
{
|
||||
name: intl.get("allArticles"),
|
||||
ariaLabel: this.countOverflow(Object.values(this.props.sources).map(s => s.unreadCount).reduce((a, b) => a + b, 0)),
|
||||
key: ALL,
|
||||
icon: "TextDocument",
|
||||
onClick: this.props.allArticles,
|
||||
@ -43,13 +46,15 @@ export class Menu extends React.Component<MenuProps> {
|
||||
getGroups = (): INavLinkGroup[] => [{
|
||||
links: this.props.groups.filter(g => g.sids.length > 0).map((g, i) => {
|
||||
if (g.isMultiple) {
|
||||
let sources = g.sids.map(sid => this.props.sources[sid])
|
||||
return {
|
||||
name: g.name,
|
||||
ariaLabel: this.countOverflow(sources.map(s => s.unreadCount).reduce((a, b) => a + b, 0)),
|
||||
key: "g-" + i,
|
||||
url: null,
|
||||
isExpanded: true,
|
||||
onClick: () => this.props.selectSourceGroup(g, "g-" + i),
|
||||
links: g.sids.map(sid => this.props.sources[sid]).map(this.getSource)
|
||||
links: sources.map(this.getSource)
|
||||
}
|
||||
} else {
|
||||
return this.getSource(this.props.sources[g.sids[0]])
|
||||
@ -59,6 +64,7 @@ export class Menu extends React.Component<MenuProps> {
|
||||
|
||||
getSource = (s: RSSSource): INavLink => ({
|
||||
name: s.name,
|
||||
ariaLabel: this.countOverflow(s.unreadCount),
|
||||
key: "s-" + s.sid,
|
||||
onClick: () => this.props.selectSource(s),
|
||||
iconProps: s.iconurl ? this.getIconStyle(s.iconurl) : null,
|
||||
@ -74,6 +80,16 @@ export class Menu extends React.Component<MenuProps> {
|
||||
}
|
||||
})
|
||||
|
||||
_onRenderLink = (link: INavLink): JSX.Element => {
|
||||
return (
|
||||
<Stack className="link-stack" horizontal grow>
|
||||
<div className="link-text">{link.name}</div>
|
||||
{link.ariaLabel !== "0" && <div className="unread-count">{link.ariaLabel}</div>}
|
||||
</Stack>
|
||||
)
|
||||
return ;
|
||||
};
|
||||
|
||||
render() {
|
||||
return this.props.status && (
|
||||
<div className="menu-container" onClick={this.props.toggleMenu} style={{display: this.props.display ? "block" : "none"}}>
|
||||
@ -84,11 +100,13 @@ export class Menu extends React.Component<MenuProps> {
|
||||
</div>
|
||||
<div className="nav-wrapper">
|
||||
<Nav
|
||||
onRenderLink={this._onRenderLink}
|
||||
groups={this.getItems()}
|
||||
selectedKey={this.props.selected} />
|
||||
<p className={"subs-header " + AnimationClassNames.slideDownIn10}>{intl.get("menu.subscriptions")}</p>
|
||||
<Nav
|
||||
selectedKey={this.props.selected}
|
||||
onRenderLink={this._onRenderLink}
|
||||
groups={this.getGroups()} />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -44,7 +44,13 @@ const mapDispatchToProps = dispatch => {
|
||||
markRead: (item: RSSItem) => dispatch(markRead(item)),
|
||||
markUnread: (item: RSSItem) => dispatch(markUnread(item)),
|
||||
toggleStarred: (item: RSSItem) => dispatch(toggleStarred(item)),
|
||||
toggleHidden: (item: RSSItem) => dispatch(toggleHidden(item)),
|
||||
toggleHidden: (item: RSSItem) => {
|
||||
if(!item.hasRead) {
|
||||
dispatch(markRead(item))
|
||||
item.hasRead = true // get around chaining error
|
||||
}
|
||||
dispatch(toggleHidden(item))
|
||||
},
|
||||
switchView: (viewType: ViewType) => {
|
||||
setDefaultView(viewType)
|
||||
dispatch(switchView(viewType))
|
||||
|
@ -20,6 +20,7 @@ export const idb = new Datastore<RSSItem>({
|
||||
if (err) window.console.log(err)
|
||||
}
|
||||
})
|
||||
idb.ensureIndex({ fieldName: "source" })
|
||||
//idb.removeIndex("id")
|
||||
//idb.update({}, {$unset: {id: true}}, {multi: true})
|
||||
//idb.remove({}, { multi: true })
|
@ -1,7 +1,7 @@
|
||||
import Parser = require("@yang991178/rss-parser")
|
||||
import * as db from "../db"
|
||||
import { rssParser, faviconPromise, ActionStatus, AppThunk } from "../utils"
|
||||
import { RSSItem, insertItems } from "./item"
|
||||
import { RSSItem, insertItems, ItemActionTypes, FETCH_ITEMS, MARK_READ, MARK_UNREAD } from "./item"
|
||||
import { SourceGroup } from "./group"
|
||||
import { saveSettings } from "./app"
|
||||
|
||||
@ -16,6 +16,7 @@ export class RSSSource {
|
||||
name: string
|
||||
description: string
|
||||
openTarget: SourceOpenTarget
|
||||
unreadCount: number
|
||||
|
||||
constructor(url: string, name: string = null) {
|
||||
this.url = url
|
||||
@ -139,17 +140,35 @@ export function initSourcesFailure(err): SourceActionTypes {
|
||||
}
|
||||
}
|
||||
|
||||
function unreadCount(source: RSSSource): Promise<RSSSource> {
|
||||
return new Promise<RSSSource>((resolve, reject) => {
|
||||
db.idb.count({ source: source.sid, hasRead: false }, (err, n) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
source.unreadCount = n
|
||||
resolve(source)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function initSources(): AppThunk<Promise<void>> {
|
||||
return (dispatch) => {
|
||||
dispatch(initSourcesRequest())
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
db.sdb.find({}).sort({ sid: 1 }).exec((err, docs) => {
|
||||
db.sdb.find({}).sort({ sid: 1 }).exec((err, sources) => {
|
||||
if (err) {
|
||||
dispatch(initSourcesFailure(err))
|
||||
reject(err)
|
||||
} else {
|
||||
dispatch(initSourcesSuccess(docs))
|
||||
resolve()
|
||||
let p = sources.map(s => unreadCount(s))
|
||||
Promise.all(p)
|
||||
.then(values => {
|
||||
dispatch(initSourcesSuccess(values))
|
||||
resolve()
|
||||
})
|
||||
.catch(err => reject(err))
|
||||
}
|
||||
})
|
||||
})
|
||||
@ -197,6 +216,7 @@ export function addSource(url: string, name: string = null, batch = false): AppT
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
source.unreadCount = feed.items.length
|
||||
dispatch(addSourceSuccess(source, batch))
|
||||
RSSSource.checkItems(source, feed.items, db.idb)
|
||||
.then(items => insertItems(items))
|
||||
@ -228,7 +248,10 @@ export function updateSourceDone(source: RSSSource): SourceActionTypes {
|
||||
|
||||
export function updateSource(source: RSSSource): AppThunk {
|
||||
return (dispatch) => {
|
||||
db.sdb.update({ sid: source.sid }, { $set: { ...source }}, {}, err => {
|
||||
let sourceCopy = { ...source }
|
||||
delete sourceCopy.sid
|
||||
delete sourceCopy.unreadCount
|
||||
db.sdb.update({ sid: source.sid }, { $set: { ...sourceCopy }}, {}, err => {
|
||||
if (!err) {
|
||||
dispatch(updateSourceDone(source))
|
||||
}
|
||||
@ -268,7 +291,7 @@ export function deleteSource(source: RSSSource): AppThunk {
|
||||
|
||||
export function sourceReducer(
|
||||
state: SourceState = {},
|
||||
action: SourceActionTypes
|
||||
action: SourceActionTypes | ItemActionTypes
|
||||
): SourceState {
|
||||
switch (action.type) {
|
||||
case INIT_SOURCES:
|
||||
@ -298,6 +321,40 @@ export function sourceReducer(
|
||||
delete state[action.source.sid]
|
||||
return { ...state }
|
||||
}
|
||||
case FETCH_ITEMS: {
|
||||
switch (action.status) {
|
||||
case ActionStatus.Success: {
|
||||
let updateMap = new Map<number, number>()
|
||||
for (let item of action.items) {
|
||||
updateMap.set(
|
||||
item.source,
|
||||
updateMap.has(item.source) ? (updateMap.get(item.source) + 1) : 1)
|
||||
}
|
||||
let nextState = {} as SourceState
|
||||
for (let s in state) {
|
||||
let sid = parseInt(s)
|
||||
if (updateMap.has(sid)) {
|
||||
nextState[sid] = {
|
||||
...state[sid],
|
||||
unreadCount: state[sid].unreadCount + updateMap.get(sid)
|
||||
} as RSSSource
|
||||
} else {
|
||||
nextState[sid] = state[sid]
|
||||
}
|
||||
}
|
||||
return nextState
|
||||
}
|
||||
default: return state
|
||||
}
|
||||
}
|
||||
case MARK_UNREAD:
|
||||
case MARK_READ: return {
|
||||
...state,
|
||||
[action.item.source]: {
|
||||
...state[action.item.source],
|
||||
unreadCount: state[action.item.source].unreadCount + (action.type === MARK_UNREAD ? 1 : -1)
|
||||
} as RSSSource
|
||||
}
|
||||
default: return state
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user