mirror of
https://github.com/yang991178/fluent-reader.git
synced 2025-05-06 21:08:56 +02: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;
|
margin: 2px 8px;
|
||||||
user-select: none;
|
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 {
|
.settings-container {
|
||||||
position: fixed;
|
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 { SourceGroup } from "../scripts/models/group"
|
||||||
import { SourceState, RSSSource } from "../scripts/models/source"
|
import { SourceState, RSSSource } from "../scripts/models/source"
|
||||||
import { ALL } from "../scripts/models/feed"
|
import { ALL } from "../scripts/models/feed"
|
||||||
import { AnimationClassNames } from "@fluentui/react"
|
import { AnimationClassNames, Stack } from "@fluentui/react"
|
||||||
|
|
||||||
export type MenuProps = {
|
export type MenuProps = {
|
||||||
status: boolean,
|
status: boolean,
|
||||||
@ -20,6 +20,8 @@ export type MenuProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Menu extends React.Component<MenuProps> {
|
export class Menu extends React.Component<MenuProps> {
|
||||||
|
countOverflow = (count: number) => count >= 1000 ? "999+" : String(count)
|
||||||
|
|
||||||
getItems = (): INavLinkGroup[] => [
|
getItems = (): INavLinkGroup[] => [
|
||||||
{
|
{
|
||||||
links: [
|
links: [
|
||||||
@ -31,6 +33,7 @@ export class Menu extends React.Component<MenuProps> {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: intl.get("allArticles"),
|
name: intl.get("allArticles"),
|
||||||
|
ariaLabel: this.countOverflow(Object.values(this.props.sources).map(s => s.unreadCount).reduce((a, b) => a + b, 0)),
|
||||||
key: ALL,
|
key: ALL,
|
||||||
icon: "TextDocument",
|
icon: "TextDocument",
|
||||||
onClick: this.props.allArticles,
|
onClick: this.props.allArticles,
|
||||||
@ -43,13 +46,15 @@ export class Menu extends React.Component<MenuProps> {
|
|||||||
getGroups = (): INavLinkGroup[] => [{
|
getGroups = (): INavLinkGroup[] => [{
|
||||||
links: this.props.groups.filter(g => g.sids.length > 0).map((g, i) => {
|
links: this.props.groups.filter(g => g.sids.length > 0).map((g, i) => {
|
||||||
if (g.isMultiple) {
|
if (g.isMultiple) {
|
||||||
|
let sources = g.sids.map(sid => this.props.sources[sid])
|
||||||
return {
|
return {
|
||||||
name: g.name,
|
name: g.name,
|
||||||
|
ariaLabel: this.countOverflow(sources.map(s => s.unreadCount).reduce((a, b) => a + b, 0)),
|
||||||
key: "g-" + i,
|
key: "g-" + i,
|
||||||
url: null,
|
url: null,
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
onClick: () => this.props.selectSourceGroup(g, "g-" + i),
|
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 {
|
} else {
|
||||||
return this.getSource(this.props.sources[g.sids[0]])
|
return this.getSource(this.props.sources[g.sids[0]])
|
||||||
@ -59,6 +64,7 @@ export class Menu extends React.Component<MenuProps> {
|
|||||||
|
|
||||||
getSource = (s: RSSSource): INavLink => ({
|
getSource = (s: RSSSource): INavLink => ({
|
||||||
name: s.name,
|
name: s.name,
|
||||||
|
ariaLabel: this.countOverflow(s.unreadCount),
|
||||||
key: "s-" + s.sid,
|
key: "s-" + s.sid,
|
||||||
onClick: () => this.props.selectSource(s),
|
onClick: () => this.props.selectSource(s),
|
||||||
iconProps: s.iconurl ? this.getIconStyle(s.iconurl) : null,
|
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() {
|
render() {
|
||||||
return this.props.status && (
|
return this.props.status && (
|
||||||
<div className="menu-container" onClick={this.props.toggleMenu} style={{display: this.props.display ? "block" : "none"}}>
|
<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>
|
||||||
<div className="nav-wrapper">
|
<div className="nav-wrapper">
|
||||||
<Nav
|
<Nav
|
||||||
|
onRenderLink={this._onRenderLink}
|
||||||
groups={this.getItems()}
|
groups={this.getItems()}
|
||||||
selectedKey={this.props.selected} />
|
selectedKey={this.props.selected} />
|
||||||
<p className={"subs-header " + AnimationClassNames.slideDownIn10}>{intl.get("menu.subscriptions")}</p>
|
<p className={"subs-header " + AnimationClassNames.slideDownIn10}>{intl.get("menu.subscriptions")}</p>
|
||||||
<Nav
|
<Nav
|
||||||
selectedKey={this.props.selected}
|
selectedKey={this.props.selected}
|
||||||
|
onRenderLink={this._onRenderLink}
|
||||||
groups={this.getGroups()} />
|
groups={this.getGroups()} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -44,7 +44,13 @@ const mapDispatchToProps = dispatch => {
|
|||||||
markRead: (item: RSSItem) => dispatch(markRead(item)),
|
markRead: (item: RSSItem) => dispatch(markRead(item)),
|
||||||
markUnread: (item: RSSItem) => dispatch(markUnread(item)),
|
markUnread: (item: RSSItem) => dispatch(markUnread(item)),
|
||||||
toggleStarred: (item: RSSItem) => dispatch(toggleStarred(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) => {
|
switchView: (viewType: ViewType) => {
|
||||||
setDefaultView(viewType)
|
setDefaultView(viewType)
|
||||||
dispatch(switchView(viewType))
|
dispatch(switchView(viewType))
|
||||||
|
@ -20,6 +20,7 @@ export const idb = new Datastore<RSSItem>({
|
|||||||
if (err) window.console.log(err)
|
if (err) window.console.log(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
idb.ensureIndex({ fieldName: "source" })
|
||||||
//idb.removeIndex("id")
|
//idb.removeIndex("id")
|
||||||
//idb.update({}, {$unset: {id: true}}, {multi: true})
|
//idb.update({}, {$unset: {id: true}}, {multi: true})
|
||||||
//idb.remove({}, { multi: true })
|
//idb.remove({}, { multi: true })
|
@ -1,7 +1,7 @@
|
|||||||
import Parser = require("@yang991178/rss-parser")
|
import Parser = require("@yang991178/rss-parser")
|
||||||
import * as db from "../db"
|
import * as db from "../db"
|
||||||
import { rssParser, faviconPromise, ActionStatus, AppThunk } from "../utils"
|
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 { SourceGroup } from "./group"
|
||||||
import { saveSettings } from "./app"
|
import { saveSettings } from "./app"
|
||||||
|
|
||||||
@ -16,6 +16,7 @@ export class RSSSource {
|
|||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
openTarget: SourceOpenTarget
|
openTarget: SourceOpenTarget
|
||||||
|
unreadCount: number
|
||||||
|
|
||||||
constructor(url: string, name: string = null) {
|
constructor(url: string, name: string = null) {
|
||||||
this.url = url
|
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>> {
|
export function initSources(): AppThunk<Promise<void>> {
|
||||||
return (dispatch) => {
|
return (dispatch) => {
|
||||||
dispatch(initSourcesRequest())
|
dispatch(initSourcesRequest())
|
||||||
return new Promise<void>((resolve, reject) => {
|
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) {
|
if (err) {
|
||||||
dispatch(initSourcesFailure(err))
|
dispatch(initSourcesFailure(err))
|
||||||
reject(err)
|
reject(err)
|
||||||
} else {
|
} else {
|
||||||
dispatch(initSourcesSuccess(docs))
|
let p = sources.map(s => unreadCount(s))
|
||||||
|
Promise.all(p)
|
||||||
|
.then(values => {
|
||||||
|
dispatch(initSourcesSuccess(values))
|
||||||
resolve()
|
resolve()
|
||||||
|
})
|
||||||
|
.catch(err => reject(err))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -197,6 +216,7 @@ export function addSource(url: string, name: string = null, batch = false): AppT
|
|||||||
if (err) {
|
if (err) {
|
||||||
reject(err)
|
reject(err)
|
||||||
} else {
|
} else {
|
||||||
|
source.unreadCount = feed.items.length
|
||||||
dispatch(addSourceSuccess(source, batch))
|
dispatch(addSourceSuccess(source, batch))
|
||||||
RSSSource.checkItems(source, feed.items, db.idb)
|
RSSSource.checkItems(source, feed.items, db.idb)
|
||||||
.then(items => insertItems(items))
|
.then(items => insertItems(items))
|
||||||
@ -228,7 +248,10 @@ export function updateSourceDone(source: RSSSource): SourceActionTypes {
|
|||||||
|
|
||||||
export function updateSource(source: RSSSource): AppThunk {
|
export function updateSource(source: RSSSource): AppThunk {
|
||||||
return (dispatch) => {
|
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) {
|
if (!err) {
|
||||||
dispatch(updateSourceDone(source))
|
dispatch(updateSourceDone(source))
|
||||||
}
|
}
|
||||||
@ -268,7 +291,7 @@ export function deleteSource(source: RSSSource): AppThunk {
|
|||||||
|
|
||||||
export function sourceReducer(
|
export function sourceReducer(
|
||||||
state: SourceState = {},
|
state: SourceState = {},
|
||||||
action: SourceActionTypes
|
action: SourceActionTypes | ItemActionTypes
|
||||||
): SourceState {
|
): SourceState {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case INIT_SOURCES:
|
case INIT_SOURCES:
|
||||||
@ -298,6 +321,40 @@ export function sourceReducer(
|
|||||||
delete state[action.source.sid]
|
delete state[action.source.sid]
|
||||||
return { ...state }
|
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
|
default: return state
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user