mirror of
https://github.com/yang991178/fluent-reader.git
synced 2025-01-30 00:55:09 +01:00
mark all as read
This commit is contained in:
parent
9bd73e559e
commit
be3bca8420
@ -18,6 +18,7 @@ export type ContextMenuProps = ContextReduxProps & {
|
||||
text?: string
|
||||
viewType?: ViewType
|
||||
filter?: FeedFilter
|
||||
sids?: number[]
|
||||
showItem: (feedId: string, item: RSSItem) => void
|
||||
markRead: (item: RSSItem) => void
|
||||
markUnread: (item: RSSItem) => void
|
||||
@ -26,6 +27,8 @@ export type ContextMenuProps = ContextReduxProps & {
|
||||
switchView: (viewType: ViewType) => void
|
||||
switchFilter: (filter: FeedFilter) => void
|
||||
toggleFilter: (filter: FeedFilter) => void
|
||||
markAllRead: (sids: number[]) => void
|
||||
settings: () => void
|
||||
close: () => void
|
||||
}
|
||||
|
||||
@ -173,6 +176,20 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
|
||||
onClick: () => this.props.toggleFilter(FeedFilter.ShowHidden)
|
||||
}
|
||||
]
|
||||
case ContextMenuType.Group: return [
|
||||
{
|
||||
key: "markAllRead",
|
||||
text: intl.get("nav.markAllRead"),
|
||||
iconProps: { iconName: "CheckMark" },
|
||||
onClick: () => this.props.markAllRead(this.props.sids)
|
||||
},
|
||||
{
|
||||
key: "manage",
|
||||
text: intl.get("context.manageSources"),
|
||||
iconProps: { iconName: "Settings" },
|
||||
onClick: this.props.settings
|
||||
}
|
||||
]
|
||||
default: return []
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,8 @@ export type MenuProps = {
|
||||
toggleMenu: () => void,
|
||||
allArticles: () => void,
|
||||
selectSourceGroup: (group: SourceGroup, menuKey: string) => void,
|
||||
selectSource: (source: RSSSource) => void
|
||||
selectSource: (source: RSSSource) => void,
|
||||
groupContextMenu: (sids: number[], event: React.MouseEvent) => void,
|
||||
}
|
||||
|
||||
export class Menu extends React.Component<MenuProps> {
|
||||
@ -80,9 +81,20 @@ export class Menu extends React.Component<MenuProps> {
|
||||
}
|
||||
})
|
||||
|
||||
onContext = (item: INavLink, event: React.MouseEvent) => {
|
||||
let sids: number[]
|
||||
let [type, index] = item.key.split("-")
|
||||
if (type === "s") {
|
||||
sids = [parseInt(index)]
|
||||
} else {
|
||||
sids = this.props.groups[parseInt(index)].sids
|
||||
}
|
||||
this.props.groupContextMenu(sids, event)
|
||||
}
|
||||
|
||||
_onRenderLink = (link: INavLink): JSX.Element => {
|
||||
return (
|
||||
<Stack className="link-stack" horizontal grow>
|
||||
<Stack className="link-stack" horizontal grow onContextMenu={event => this.onContext(link, event)}>
|
||||
<div className="link-text">{link.name}</div>
|
||||
{link.ariaLabel !== "0" && <div className="unread-count">{link.ariaLabel}</div>}
|
||||
</Stack>
|
||||
|
@ -12,7 +12,8 @@ type NavProps = {
|
||||
menu: () => void,
|
||||
logs: () => void,
|
||||
views: () => void,
|
||||
settings: () => void
|
||||
settings: () => void,
|
||||
markAllRead: () => void
|
||||
}
|
||||
|
||||
type NavState = {
|
||||
@ -90,7 +91,9 @@ class Nav extends React.Component<NavProps, NavState> {
|
||||
title={intl.get("nav.refresh")}>
|
||||
<Icon iconName="Refresh" />
|
||||
</a>
|
||||
<a className="btn" title={intl.get("nav.markAllRead")}>
|
||||
<a className="btn"
|
||||
onClick={this.props.markAllRead}
|
||||
title={intl.get("nav.markAllRead")}>
|
||||
<Icon iconName="InboxCheck" />
|
||||
</a>
|
||||
<a className="btn"
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { connect } from "react-redux"
|
||||
import { createSelector } from "reselect"
|
||||
import { RootState } from "../scripts/reducer"
|
||||
import { ContextMenuType, closeContextMenu } from "../scripts/models/app"
|
||||
import { ContextMenuType, closeContextMenu, toggleSettings } from "../scripts/models/app"
|
||||
import { ContextMenu } from "../components/context-menu"
|
||||
import { RSSItem, markRead, markUnread, toggleStarred, toggleHidden } from "../scripts/models/item"
|
||||
import { RSSItem, markRead, markUnread, toggleStarred, toggleHidden, markAllRead } from "../scripts/models/item"
|
||||
import { showItem, switchView, ViewType, switchFilter, toggleFilter } from "../scripts/models/page"
|
||||
import { setDefaultView } from "../scripts/settings"
|
||||
import { FeedFilter } from "../scripts/models/feed"
|
||||
@ -33,6 +33,11 @@ const mapStateToProps = createSelector(
|
||||
viewType: viewType,
|
||||
filter: filter
|
||||
}
|
||||
case ContextMenuType.Group: return {
|
||||
type: context.type,
|
||||
event: context.event,
|
||||
sids: context.target
|
||||
}
|
||||
default: return { type: ContextMenuType.Hidden }
|
||||
}
|
||||
}
|
||||
@ -57,6 +62,8 @@ const mapDispatchToProps = dispatch => {
|
||||
},
|
||||
switchFilter: (filter: FeedFilter) => dispatch(switchFilter(filter)),
|
||||
toggleFilter: (filter: FeedFilter) => dispatch(toggleFilter(filter)),
|
||||
markAllRead: (sids: number[]) => dispatch(markAllRead(sids)),
|
||||
settings: () => dispatch(toggleSettings()),
|
||||
close: () => dispatch(closeContextMenu())
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { connect } from "react-redux"
|
||||
import { createSelector } from "reselect"
|
||||
import { RootState } from "../scripts/reducer"
|
||||
import { Menu } from "../components/menu"
|
||||
import { toggleMenu } from "../scripts/models/app"
|
||||
import { toggleMenu, openGroupMenu } from "../scripts/models/app"
|
||||
import { SourceGroup } from "../scripts/models/group"
|
||||
import { selectAllArticles, selectSources } from "../scripts/models/page"
|
||||
import { initFeeds } from "../scripts/models/feed"
|
||||
@ -36,6 +36,9 @@ const mapDispatchToProps = dispatch => ({
|
||||
selectSource: (source: RSSSource) => {
|
||||
dispatch(selectSources([source.sid], "s-"+source.sid, source.name))
|
||||
dispatch(initFeeds())
|
||||
},
|
||||
groupContextMenu: (sids: number[], event: React.MouseEvent) => {
|
||||
dispatch(openGroupMenu(sids, event))
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { remote } from "electron"
|
||||
import intl = require("react-intl-universal")
|
||||
import { connect } from "react-redux"
|
||||
import { createSelector } from "reselect"
|
||||
import { RootState } from "../scripts/reducer"
|
||||
import { fetchItems } from "../scripts/models/item"
|
||||
import { fetchItems, markAllRead } from "../scripts/models/item"
|
||||
import { toggleMenu, toggleLogMenu, toggleSettings, openViewMenu } from "../scripts/models/app"
|
||||
import { ViewType } from "../scripts/models/page"
|
||||
import Nav from "../components/nav"
|
||||
@ -22,7 +24,20 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
menu: () => dispatch(toggleMenu()),
|
||||
logs: () => dispatch(toggleLogMenu()),
|
||||
views: () => dispatch(openViewMenu()),
|
||||
settings: () => dispatch(toggleSettings())
|
||||
settings: () => dispatch(toggleSettings()),
|
||||
markAllRead: () => {
|
||||
remote.dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
title: intl.get("nav.markAllRead"),
|
||||
message: intl.get("confirmMarkAll"),
|
||||
buttons: process.platform === "win32" ? ["Yes", "No"] : [intl.get("confirm"), intl.get("cancel")],
|
||||
defaultId: 0,
|
||||
cancelId: 1
|
||||
}).then(response => {
|
||||
if (response.response === 0) {
|
||||
dispatch(markAllRead())
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const NavContainer = connect(mapStateToProps, mapDispatchToProps)(Nav)
|
||||
|
@ -12,6 +12,9 @@
|
||||
"search": "Search",
|
||||
"loadMore": "Load more",
|
||||
"dangerButton": "Confirm {action}?",
|
||||
"confirmMarkAll": "Do you really want to mark all articles on this page as read?",
|
||||
"confirm": "Confirm",
|
||||
"cancel": "Cancel",
|
||||
"log": {
|
||||
"empty": "No notifications",
|
||||
"fetchFailure": "Failed to load source \"{name}\".",
|
||||
@ -53,7 +56,8 @@
|
||||
"filter": "Filtering",
|
||||
"unreadOnly": "Unread only",
|
||||
"starredOnly": "Starred only",
|
||||
"showHidden": "Show hidden articles"
|
||||
"showHidden": "Show hidden articles",
|
||||
"manageSources": "Manage sources"
|
||||
},
|
||||
"settings": {
|
||||
"name": "Settings",
|
||||
|
@ -12,6 +12,9 @@
|
||||
"search": "搜索",
|
||||
"loadMore": "加载更多",
|
||||
"dangerButton": "确认{action}?",
|
||||
"confirmMarkAll": "确认将本页所有文章标为已读?",
|
||||
"confirm": "确认",
|
||||
"cancel": "取消",
|
||||
"log": {
|
||||
"empty": "无消息",
|
||||
"fetchFailure": "无法加载订阅源“{name}”",
|
||||
@ -53,7 +56,8 @@
|
||||
"filter": "筛选",
|
||||
"unreadOnly": "仅未读文章",
|
||||
"starredOnly": "仅星标文章",
|
||||
"showHidden": "显示隐藏文章"
|
||||
"showHidden": "显示隐藏文章",
|
||||
"manageSources": "管理订阅源"
|
||||
},
|
||||
"settings": {
|
||||
"name": "选项",
|
||||
|
@ -9,7 +9,7 @@ import { getCurrentLocale, setLocaleSettings } from "../settings"
|
||||
import locales from "../i18n/_locales"
|
||||
|
||||
export enum ContextMenuType {
|
||||
Hidden, Item, Text, View
|
||||
Hidden, Item, Text, View, Group
|
||||
}
|
||||
|
||||
export enum AppLogType {
|
||||
@ -55,7 +55,7 @@ export class AppState {
|
||||
type: ContextMenuType,
|
||||
event?: MouseEvent | string,
|
||||
position?: [number, number],
|
||||
target?: [RSSItem, string] | RSSSource | string
|
||||
target?: [RSSItem, string] | number[] | string
|
||||
}
|
||||
|
||||
constructor() {
|
||||
@ -69,6 +69,7 @@ export const CLOSE_CONTEXT_MENU = "CLOSE_CONTEXT_MENU"
|
||||
export const OPEN_ITEM_MENU = "OPEN_ITEM_MENU"
|
||||
export const OPEN_TEXT_MENU = "OPEN_TEXT_MENU"
|
||||
export const OPEN_VIEW_MENU = "OPEN_VIEW_MENU"
|
||||
export const OPEN_GROUP_MENU = "OPEN_GROUP_MENU"
|
||||
|
||||
interface CloseContextMenuAction {
|
||||
type: typeof CLOSE_CONTEXT_MENU
|
||||
@ -91,7 +92,14 @@ interface OpenViewMenuAction {
|
||||
type: typeof OPEN_VIEW_MENU
|
||||
}
|
||||
|
||||
export type ContextMenuActionTypes = CloseContextMenuAction | OpenItemMenuAction | OpenTextMenuAction | OpenViewMenuAction
|
||||
interface OpenGroupMenuAction {
|
||||
type: typeof OPEN_GROUP_MENU
|
||||
event: MouseEvent
|
||||
sids: number[]
|
||||
}
|
||||
|
||||
export type ContextMenuActionTypes = CloseContextMenuAction | OpenItemMenuAction
|
||||
| OpenTextMenuAction | OpenViewMenuAction | OpenGroupMenuAction
|
||||
|
||||
export const TOGGLE_LOGS = "TOGGLE_LOGS"
|
||||
export interface LogMenuActionType { type: typeof TOGGLE_LOGS }
|
||||
@ -132,6 +140,14 @@ export function openTextMenu(text: string, position: [number, number]): ContextM
|
||||
|
||||
export const openViewMenu = (): ContextMenuActionTypes => ({ type: OPEN_VIEW_MENU })
|
||||
|
||||
export function openGroupMenu(sids: number[], event: React.MouseEvent): ContextMenuActionTypes {
|
||||
return {
|
||||
type: OPEN_GROUP_MENU,
|
||||
event: event.nativeEvent,
|
||||
sids: sids
|
||||
}
|
||||
}
|
||||
|
||||
export const toggleMenu = () => ({ type: TOGGLE_MENU })
|
||||
export const toggleLogMenu = () => ({ type: TOGGLE_LOGS })
|
||||
export const toggleSettings = () => ({ type: TOGGLE_SETTINGS })
|
||||
@ -331,6 +347,14 @@ export function appReducer(
|
||||
event: "#view-toggle"
|
||||
}
|
||||
}
|
||||
case OPEN_GROUP_MENU: return {
|
||||
...state,
|
||||
contextMenu: {
|
||||
type: ContextMenuType.Group,
|
||||
event: action.event,
|
||||
target: action.sids
|
||||
}
|
||||
}
|
||||
case TOGGLE_MENU: return {
|
||||
...state,
|
||||
menu: !state.menu
|
||||
|
@ -1,7 +1,7 @@
|
||||
import * as db from "../db"
|
||||
import { rssParser, domParser, htmlDecode, ActionStatus, AppThunk } from "../utils"
|
||||
import { RSSSource } from "./source"
|
||||
import { FeedActionTypes, INIT_FEED, LOAD_MORE } from "./feed"
|
||||
import { FeedActionTypes, INIT_FEED, LOAD_MORE, FeedFilter } from "./feed"
|
||||
import Parser = require("@yang991178/rss-parser")
|
||||
|
||||
export class RSSItem {
|
||||
@ -55,6 +55,7 @@ export type ItemState = {
|
||||
|
||||
export const FETCH_ITEMS = 'FETCH_ITEMS'
|
||||
export const MARK_READ = "MARK_READ"
|
||||
export const MARK_ALL_READ = "MARK_ALL_READ"
|
||||
export const MARK_UNREAD = "MARK_UNREAD"
|
||||
export const TOGGLE_STARRED = "TOGGLE_STARRED"
|
||||
export const TOGGLE_HIDDEN = "TOGGLE_HIDDEN"
|
||||
@ -73,6 +74,11 @@ interface MarkReadAction {
|
||||
item: RSSItem
|
||||
}
|
||||
|
||||
interface MarkAllReadAction {
|
||||
type: typeof MARK_ALL_READ,
|
||||
sids: number[]
|
||||
}
|
||||
|
||||
interface MarkUnreadAction {
|
||||
type: typeof MARK_UNREAD
|
||||
item: RSSItem
|
||||
@ -88,7 +94,8 @@ interface ToggleHiddenAction {
|
||||
item: RSSItem
|
||||
}
|
||||
|
||||
export type ItemActionTypes = FetchItemsAction | MarkReadAction | MarkUnreadAction | ToggleStarredAction | ToggleHiddenAction
|
||||
export type ItemActionTypes = FetchItemsAction | MarkReadAction | MarkAllReadAction | MarkUnreadAction
|
||||
| ToggleStarredAction | ToggleHiddenAction
|
||||
|
||||
export function fetchItemsRequest(fetchCount = 0): ItemActionTypes {
|
||||
return {
|
||||
@ -174,6 +181,11 @@ const markReadDone = (item: RSSItem): ItemActionTypes => ({
|
||||
item: item
|
||||
})
|
||||
|
||||
const markAllReadDone = (sids: number[]): ItemActionTypes => ({
|
||||
type: MARK_ALL_READ,
|
||||
sids: sids
|
||||
})
|
||||
|
||||
const markUnreadDone = (item: RSSItem): ItemActionTypes => ({
|
||||
type: MARK_UNREAD,
|
||||
item: item
|
||||
@ -181,15 +193,37 @@ const markUnreadDone = (item: RSSItem): ItemActionTypes => ({
|
||||
|
||||
export function markRead(item: RSSItem): AppThunk {
|
||||
return (dispatch) => {
|
||||
db.idb.update({ _id: item._id }, { $set: { hasRead: true } })
|
||||
dispatch(markReadDone(item))
|
||||
if (!item.hasRead) {
|
||||
db.idb.update({ _id: item._id }, { $set: { hasRead: true } })
|
||||
dispatch(markReadDone(item))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function markAllRead(sids: number[] = null): AppThunk {
|
||||
return (dispatch, getState) => {
|
||||
if (sids === null) {
|
||||
let state = getState()
|
||||
let feed = state.feeds[state.page.feedId]
|
||||
sids = feed.sids
|
||||
}
|
||||
let query = { source: { $in: sids } }
|
||||
db.idb.update(query, { $set: { hasRead: true } }, { multi: true }, (err) => {
|
||||
if (err) {
|
||||
console.log(err)
|
||||
} else {
|
||||
dispatch(markAllReadDone(sids))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function markUnread(item: RSSItem): AppThunk {
|
||||
return (dispatch) => {
|
||||
db.idb.update({ _id: item._id }, { $set: { hasRead: false } })
|
||||
dispatch(markUnreadDone(item))
|
||||
if (item.hasRead) {
|
||||
db.idb.update({ _id: item._id }, { $set: { hasRead: false } })
|
||||
dispatch(markUnreadDone(item))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -272,6 +306,21 @@ export function itemReducer(
|
||||
[action.item._id]: applyItemReduction(action.item, action.type)
|
||||
}
|
||||
}
|
||||
case MARK_ALL_READ: {
|
||||
let nextState = {} as ItemState
|
||||
let sids = new Set(action.sids)
|
||||
for (let [id, item] of Object.entries(state)) {
|
||||
if (sids.has(item.source) && !item.hasRead) {
|
||||
nextState[id] = {
|
||||
...item,
|
||||
hasRead: true
|
||||
}
|
||||
} else {
|
||||
nextState[id] = item
|
||||
}
|
||||
}
|
||||
return nextState
|
||||
}
|
||||
case LOAD_MORE:
|
||||
case INIT_FEED: {
|
||||
switch (action.status) {
|
||||
|
@ -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, ItemActionTypes, FETCH_ITEMS, MARK_READ, MARK_UNREAD } from "./item"
|
||||
import { RSSItem, insertItems, ItemActionTypes, FETCH_ITEMS, MARK_READ, MARK_UNREAD, MARK_ALL_READ } from "./item"
|
||||
import { SourceGroup } from "./group"
|
||||
import { saveSettings } from "./app"
|
||||
|
||||
@ -331,15 +331,15 @@ export function sourceReducer(
|
||||
updateMap.has(item.source) ? (updateMap.get(item.source) + 1) : 1)
|
||||
}
|
||||
let nextState = {} as SourceState
|
||||
for (let s in state) {
|
||||
for (let [s, source] of Object.entries(state)) {
|
||||
let sid = parseInt(s)
|
||||
if (updateMap.has(sid)) {
|
||||
nextState[sid] = {
|
||||
...state[sid],
|
||||
unreadCount: state[sid].unreadCount + updateMap.get(sid)
|
||||
...source,
|
||||
unreadCount: source.unreadCount + updateMap.get(sid)
|
||||
} as RSSSource
|
||||
} else {
|
||||
nextState[sid] = state[sid]
|
||||
nextState[sid] = source
|
||||
}
|
||||
}
|
||||
return nextState
|
||||
@ -355,6 +355,22 @@ export function sourceReducer(
|
||||
unreadCount: state[action.item.source].unreadCount + (action.type === MARK_UNREAD ? 1 : -1)
|
||||
} as RSSSource
|
||||
}
|
||||
case MARK_ALL_READ: {
|
||||
let nextState = {} as SourceState
|
||||
let sids = new Set(action.sids)
|
||||
for (let [s, source] of Object.entries(state)) {
|
||||
let sid = parseInt(s)
|
||||
if (sids.has(sid) && source.unreadCount > 0) {
|
||||
nextState[sid] = {
|
||||
...source,
|
||||
unreadCount: 0
|
||||
} as RSSSource
|
||||
} else {
|
||||
nextState[sid] = source
|
||||
}
|
||||
}
|
||||
return nextState
|
||||
}
|
||||
default: return state
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user