diff --git a/src/components/context-menu.tsx b/src/components/context-menu.tsx index 0836725..d5200bf 100644 --- a/src/components/context-menu.tsx +++ b/src/components/context-menu.tsx @@ -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 { 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 [] } } diff --git a/src/components/menu.tsx b/src/components/menu.tsx index fa0e888..0a4db3d 100644 --- a/src/components/menu.tsx +++ b/src/components/menu.tsx @@ -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 { @@ -80,9 +81,20 @@ export class Menu extends React.Component { } }) + 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 ( - + this.onContext(link, event)}>
{link.name}
{link.ariaLabel !== "0" &&
{link.ariaLabel}
}
diff --git a/src/components/nav.tsx b/src/components/nav.tsx index 2c3aab3..827f2e4 100644 --- a/src/components/nav.tsx +++ b/src/components/nav.tsx @@ -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 { title={intl.get("nav.refresh")}> - + { }, switchFilter: (filter: FeedFilter) => dispatch(switchFilter(filter)), toggleFilter: (filter: FeedFilter) => dispatch(toggleFilter(filter)), + markAllRead: (sids: number[]) => dispatch(markAllRead(sids)), + settings: () => dispatch(toggleSettings()), close: () => dispatch(closeContextMenu()) } } diff --git a/src/containers/menu-container.tsx b/src/containers/menu-container.tsx index dabe502..441e116 100644 --- a/src/containers/menu-container.tsx +++ b/src/containers/menu-container.tsx @@ -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)) } }) diff --git a/src/containers/nav-container.tsx b/src/containers/nav-container.tsx index 10953a4..5048ba9 100644 --- a/src/containers/nav-container.tsx +++ b/src/containers/nav-container.tsx @@ -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) diff --git a/src/scripts/i18n/en-US.json b/src/scripts/i18n/en-US.json index ddd2eb3..d621efc 100644 --- a/src/scripts/i18n/en-US.json +++ b/src/scripts/i18n/en-US.json @@ -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", diff --git a/src/scripts/i18n/zh-CN.json b/src/scripts/i18n/zh-CN.json index 4187b0d..4bde838 100644 --- a/src/scripts/i18n/zh-CN.json +++ b/src/scripts/i18n/zh-CN.json @@ -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": "选项", diff --git a/src/scripts/models/app.ts b/src/scripts/models/app.ts index 6cd17c9..e2e6017 100644 --- a/src/scripts/models/app.ts +++ b/src/scripts/models/app.ts @@ -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 diff --git a/src/scripts/models/item.ts b/src/scripts/models/item.ts index 0568ac9..cb221fe 100644 --- a/src/scripts/models/item.ts +++ b/src/scripts/models/item.ts @@ -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) { diff --git a/src/scripts/models/source.ts b/src/scripts/models/source.ts index b593531..fb1807a 100644 --- a/src/scripts/models/source.ts +++ b/src/scripts/models/source.ts @@ -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 } } \ No newline at end of file