diff --git a/dist/styles.css b/dist/styles.css index b652e70..2b382da 100644 --- a/dist/styles.css +++ b/dist/styles.css @@ -337,7 +337,6 @@ img.favicon { .main { height: calc(100% - 32px); - position: relative; overflow-y: scroll; } .main::before { @@ -352,6 +351,19 @@ img.favicon { background: linear-gradient(var(--neutralLighterAlt), #faf9f800); z-index: 1; } +.article-search { + z-index: 4; + position: absolute; + top:0; + left: 36px; + width: 100%; + max-width: calc(100% - 484px); + margin: 4px 16px; + border: none; + -webkit-app-region: none; + height: 28px; + box-shadow: 0 1.6px 3.6px 0 rgba(0,0,0,.132), 0 0.3px 0.9px 0 rgba(0,0,0,.108); +} @media (min-width: 1441px) { #root > nav.menu-on { @@ -386,6 +398,10 @@ img.favicon { .main.menu-on, .list-main.menu-on { margin-left: 280px; } + .menu-on .article-search { + left: 280px; + max-width: calc(100% - 728px); + } nav.hide-btns .btn-group .btn, nav.menu-on .btn-group .btn.hide-wide, .menu .btn-group .btn.hide-wide { display: none; @@ -460,7 +476,7 @@ img.favicon { .side-article-wrapper .article > .ms-Stack { border-top: 1px solid var(--neutralQuaternaryAlt); } -.list-feed-container::before, .side-article-wrapper::before { +.list-feed-container:first-child::before, .side-article-wrapper::before { content: ""; display: block; width: 100%; @@ -478,6 +494,11 @@ img.favicon { overflow: hidden; background: var(--white); } +.list-main .article-search { + left: 0; + max-width: 330px; + margin: 4px 10px; +} .list-feed-container { width: 350px; background-color: var(--neutralLighterAlt); diff --git a/package.json b/package.json index 468061b..636f9f4 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "build": "webpack --config ./webpack.config.js", "electron": "electron ./dist/electron.js", "start": "npm run build && npm run electron", - "package-win": "electron-builder --win --x64", + "package-win": "electron-builder -w --x64 && electron-builder -w --ia32 && electron-builder -w --arm64", "package-mac": "sudo electron-builder --mac" }, "keywords": [], @@ -19,13 +19,10 @@ "copyright": "Copyright © 2020 Haoyuan Liu", "files": "./dist/**/*", "directories": { - "output": "./bin/" + "output": "./bin/${platform}/${arch}/" }, "win": { - "target": [ - "nsis", - "appx" - ], + "target": [ "nsis", "appx" ], "certificateFile": "./bin/key.pfx" }, "appx": { @@ -41,7 +38,7 @@ "setBuildNumber": true }, "mac": { - "target": ["dmg"] + "target": [ "dmg" ] } }, "devDependencies": { @@ -55,7 +52,7 @@ "@types/reselect": "^2.2.0", "@yang991178/electron-proxy-agent": "^1.2.1", "@yang991178/rss-parser": "^3.8.1", - "electron": "^8.3.0", + "electron": "^9.0.4", "electron-builder": "^22.7.0", "electron-react-devtools": "^0.5.3", "electron-store": "^5.2.0", diff --git a/src/components/cards/default-card.tsx b/src/components/cards/default-card.tsx index 8580352..da34a7c 100644 --- a/src/components/cards/default-card.tsx +++ b/src/components/cards/default-card.tsx @@ -24,7 +24,7 @@ class DefaultCard extends Card { ) : null}

{this.props.item.title}

-

{this.props.item.snippet}

+

{this.props.item.snippet.slice(0, 325)}

) } diff --git a/src/components/context-menu.tsx b/src/components/context-menu.tsx index d5200bf..606c323 100644 --- a/src/components/context-menu.tsx +++ b/src/components/context-menu.tsx @@ -7,7 +7,7 @@ import { ContextMenuType } from "../scripts/models/app" import { RSSItem } from "../scripts/models/item" import { ContextReduxProps } from "../containers/context-menu-container" import { ViewType } from "../scripts/models/page" -import { FeedFilter } from "../scripts/models/feed" +import { FilterType } from "../scripts/models/feed" export type ContextMenuProps = ContextReduxProps & { type: ContextMenuType @@ -17,7 +17,7 @@ export type ContextMenuProps = ContextReduxProps & { feedId?: string text?: string viewType?: ViewType - filter?: FeedFilter + filter?: FilterType sids?: number[] showItem: (feedId: string, item: RSSItem) => void markRead: (item: RSSItem) => void @@ -25,8 +25,8 @@ export type ContextMenuProps = ContextReduxProps & { toggleStarred: (item: RSSItem) => void toggleHidden: (item: RSSItem) => void switchView: (viewType: ViewType) => void - switchFilter: (filter: FeedFilter) => void - toggleFilter: (filter: FeedFilter) => void + switchFilter: (filter: FilterType) => void + toggleFilter: (filter: FilterType) => void markAllRead: (sids: number[]) => void settings: () => void close: () => void @@ -146,34 +146,41 @@ export class ContextMenu extends React.Component { text: intl.get("allArticles"), iconProps: { iconName: "ClearFilter" }, canCheck: true, - checked: (this.props.filter & ~FeedFilter.ShowHidden) == FeedFilter.Default, - onClick: () => this.props.switchFilter(FeedFilter.Default) + checked: (this.props.filter & ~FilterType.Toggles) == FilterType.Default, + onClick: () => this.props.switchFilter(FilterType.Default) }, { key: "unreadOnly", text: intl.get("context.unreadOnly"), iconProps: { iconName: "RadioBtnOn", style: { fontSize: 14, textAlign: "center" } }, canCheck: true, - checked: (this.props.filter & ~FeedFilter.ShowHidden) == FeedFilter.UnreadOnly, - onClick: () => this.props.switchFilter(FeedFilter.UnreadOnly) + checked: (this.props.filter & ~FilterType.Toggles) == FilterType.UnreadOnly, + onClick: () => this.props.switchFilter(FilterType.UnreadOnly) }, { key: "starredOnly", text: intl.get("context.starredOnly"), iconProps: { iconName: "FavoriteStarFill" }, canCheck: true, - checked: (this.props.filter & ~FeedFilter.ShowHidden) == FeedFilter.StarredOnly, - onClick: () => this.props.switchFilter(FeedFilter.StarredOnly) + checked: (this.props.filter & ~FilterType.Toggles) == FilterType.StarredOnly, + onClick: () => this.props.switchFilter(FilterType.StarredOnly) } ] } }, + { + key: "fullSearch", + text: intl.get("context.fullSearch"), + canCheck: true, + checked: Boolean(this.props.filter & FilterType.FullSearch), + onClick: () => this.props.toggleFilter(FilterType.FullSearch) + }, { key: "showHidden", text: intl.get("context.showHidden"), canCheck: true, - checked: Boolean(this.props.filter & FeedFilter.ShowHidden), - onClick: () => this.props.toggleFilter(FeedFilter.ShowHidden) + checked: Boolean(this.props.filter & FilterType.ShowHidden), + onClick: () => this.props.toggleFilter(FilterType.ShowHidden) } ] case ContextMenuType.Group: return [ diff --git a/src/components/menu.tsx b/src/components/menu.tsx index 84bd872..56077e6 100644 --- a/src/components/menu.tsx +++ b/src/components/menu.tsx @@ -13,12 +13,14 @@ export type MenuProps = { selected: string, sources: SourceState, groups: SourceGroup[], + searchOn: boolean, toggleMenu: () => void, allArticles: () => void, selectSourceGroup: (group: SourceGroup, menuKey: string) => void, selectSource: (source: RSSSource) => void, groupContextMenu: (sids: number[], event: React.MouseEvent) => void, updateGroupExpansion: (event: React.MouseEvent, key: string, selected: string) => void, + toggleSearch: () => void, } export class Menu extends React.Component { @@ -29,8 +31,10 @@ export class Menu extends React.Component { links: [ { name: intl.get("search"), + ariaLabel: this.props.searchOn ? "✓" : "0", key: "search", icon: "Search", + onClick: this.props.toggleSearch, url: null }, { @@ -100,7 +104,6 @@ export class Menu extends React.Component { {link.ariaLabel !== "0" &&
{link.ariaLabel}
} ) - return ; }; render() { diff --git a/src/components/page.tsx b/src/components/page.tsx index 30fcd9e..c1993c7 100644 --- a/src/components/page.tsx +++ b/src/components/page.tsx @@ -3,6 +3,7 @@ import { FeedContainer } from "../containers/feed-container" import { AnimationClassNames, Icon } from "@fluentui/react" import ArticleContainer from "../containers/article-container" import { ViewType } from "../scripts/models/page" +import ArticleSearch from "./utils/article-search" type PageProps = { menuOn: boolean @@ -29,6 +30,7 @@ class Page extends React.Component { <> {this.props.settingsOn ? null :
+ {this.props.feeds.map(fid => ( ))} @@ -48,6 +50,7 @@ class Page extends React.Component { <> {this.props.settingsOn ? null :
+
{this.props.feeds.map(fid => ( diff --git a/src/components/utils/article-search.tsx b/src/components/utils/article-search.tsx new file mode 100644 index 0000000..f9eacee --- /dev/null +++ b/src/components/utils/article-search.tsx @@ -0,0 +1,63 @@ +import * as React from "react" +import intl = require("react-intl-universal") +import { connect } from "react-redux" +import { RootState } from "../../scripts/reducer" +import { SearchBox, IRefObject, ISearchBox } from "@fluentui/react" +import { AppDispatch } from "../../scripts/utils" +import { performSearch } from "../../scripts/models/page" + +class Debounced { + public use = (func: (...args: any[]) => any, delay: number): ((...args: any[]) => void) => { + let timer: NodeJS.Timeout + return (...args: any[]) => { + clearTimeout(timer) + timer = setTimeout(() => { + func.apply(this, args) + }, delay) + } + } +} + +type SearchProps = { + searchOn: boolean + initQuery: string + dispatch: AppDispatch +} + +class ArticleSearch extends React.Component { + debouncedSearch: (query: string) => void + inputRef: React.RefObject + + constructor(props: SearchProps) { + super(props) + this.debouncedSearch = new Debounced().use((query: string) => props.dispatch(performSearch(query)), 750) + this.inputRef = React.createRef() + } + + onSearchChange = (_, newValue: string) => { + this.debouncedSearch(newValue) + } + + componentDidUpdate(prevProps: SearchProps) { + if (this.props.searchOn && !prevProps.searchOn) { + this.inputRef.current.focus() + } + } + + render() { + return this.props.searchOn && ( + + ) + } +} + +const getSearchProps = (state: RootState) => ({ + searchOn: state.page.searchOn, + initQuery: state.page.filter.search +}) +export default connect(getSearchProps)(ArticleSearch) \ No newline at end of file diff --git a/src/containers/context-menu-container.tsx b/src/containers/context-menu-container.tsx index 59a43c7..6702346 100644 --- a/src/containers/context-menu-container.tsx +++ b/src/containers/context-menu-container.tsx @@ -6,7 +6,7 @@ import { ContextMenu } from "../components/context-menu" 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" +import { FilterType } from "../scripts/models/feed" const getContext = (state: RootState) => state.app.contextMenu const getViewType = (state: RootState) => state.page.viewType @@ -31,7 +31,7 @@ const mapStateToProps = createSelector( type: context.type, event: context.event, viewType: viewType, - filter: filter + filter: filter.type } case ContextMenuType.Group: return { type: context.type, @@ -60,8 +60,8 @@ const mapDispatchToProps = dispatch => { setDefaultView(viewType) dispatch(switchView(viewType)) }, - switchFilter: (filter: FeedFilter) => dispatch(switchFilter(filter)), - toggleFilter: (filter: FeedFilter) => dispatch(toggleFilter(filter)), + switchFilter: (filter: FilterType) => dispatch(switchFilter(filter)), + toggleFilter: (filter: FilterType) => 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 ce18248..ff7e75b 100644 --- a/src/containers/menu-container.tsx +++ b/src/containers/menu-container.tsx @@ -4,22 +4,24 @@ import { RootState } from "../scripts/reducer" import { Menu } from "../components/menu" import { toggleMenu, openGroupMenu } from "../scripts/models/app" import { SourceGroup, toggleGroupExpansion } from "../scripts/models/group" -import { selectAllArticles, selectSources } from "../scripts/models/page" +import { selectAllArticles, selectSources, toggleSearch } from "../scripts/models/page" import { initFeeds } from "../scripts/models/feed" import { RSSSource } from "../scripts/models/source" const getApp = (state: RootState) => state.app const getSources = (state: RootState) => state.sources const getGroups = (state: RootState) => state.groups +const getSearchOn = (state: RootState) => state.page.searchOn const mapStateToProps = createSelector( - [getApp, getSources, getGroups], - (app, sources, groups) => ({ + [getApp, getSources, getGroups, getSearchOn], + (app, sources, groups, searchOn) => ({ status: app.sourceInit, display: app.menu, selected: app.menuKey, sources: sources, - groups: groups + groups: groups, + searchOn: searchOn, }) ) @@ -45,7 +47,8 @@ const mapDispatchToProps = dispatch => ({ let [type, index] = key.split("-") if (type === "g") dispatch(toggleGroupExpansion(parseInt(index))) } - } + }, + toggleSearch: () => dispatch(toggleSearch()), }) const MenuContainer = connect(mapStateToProps, mapDispatchToProps)(Menu) diff --git a/src/containers/settings/sources-container.tsx b/src/containers/settings/sources-container.tsx index 78ab96d..8ad7115 100644 --- a/src/containers/settings/sources-container.tsx +++ b/src/containers/settings/sources-container.tsx @@ -28,14 +28,15 @@ const mapDispatchToProps = (dispatch: AppDispatch) => { }, deleteSource: (source: RSSSource) => dispatch(deleteSource(source)), importOPML: () => { - let path = remote.dialog.showOpenDialogSync( + remote.dialog.showOpenDialog( remote.getCurrentWindow(), { filters: [{ name: intl.get("sources.opmlFile"), extensions: ["xml", "opml"] }], properties: ["openFile"] } - ) - if (path && path.length > 0) dispatch(importOPML(path[0])) + ).then(result => { + if (!result.canceled && result.filePaths.length > 0) dispatch(importOPML(result.filePaths[0])) + }) }, exportOPML: () => { remote.dialog.showSaveDialog( diff --git a/src/electron.ts b/src/electron.ts index 237e73a..9a4a6b9 100644 --- a/src/electron.ts +++ b/src/electron.ts @@ -26,7 +26,8 @@ function createWindow() { show: false, webPreferences: { nodeIntegration: true, - webviewTag: true + webviewTag: true, + enableRemoteModule: true } }) mainWindowState.manage(mainWindow) diff --git a/src/scripts/i18n/en-US.json b/src/scripts/i18n/en-US.json index 6b29108..1f5bf82 100644 --- a/src/scripts/i18n/en-US.json +++ b/src/scripts/i18n/en-US.json @@ -35,6 +35,7 @@ "subscriptions": "Subscriptions" }, "article": { + "untitled": "(Untitled)", "hide": "Hide article", "unhide": "Unhide article", "markRead": "Mark as read", @@ -56,6 +57,7 @@ "filter": "Filtering", "unreadOnly": "Unread only", "starredOnly": "Starred only", + "fullSearch": "Search in full text", "showHidden": "Show hidden articles", "manageSources": "Manage sources" }, @@ -72,6 +74,8 @@ "feedback": "Feedback" }, "sources": { + "untitled": "Source", + "errorAdd": "An error has occured when adding the source.", "errorImport": "Error importing {count, plural, =1 {# source} other {# sources}}.", "opmlFile": "OPML File", "name": "Source name", diff --git a/src/scripts/i18n/zh-CN.json b/src/scripts/i18n/zh-CN.json index 524b44b..e27d14f 100644 --- a/src/scripts/i18n/zh-CN.json +++ b/src/scripts/i18n/zh-CN.json @@ -35,6 +35,7 @@ "subscriptions": "订阅源" }, "article": { + "untitled": "(无标题)", "hide": "隐藏文章", "unhide": "取消隐藏", "markRead": "标为已读", @@ -56,6 +57,7 @@ "filter": "筛选", "unreadOnly": "仅未读文章", "starredOnly": "仅星标文章", + "fullSearch": "在正文中搜索", "showHidden": "显示隐藏文章", "manageSources": "管理订阅源" }, @@ -72,6 +74,8 @@ "feedback": "反馈" }, "sources": { + "untitled": "订阅源", + "errorAdd": "添加订阅源时出错", "errorImport": "导入{count}项订阅源时出错", "opmlFile": "OPML文件", "name": "订阅源名称", diff --git a/src/scripts/models/feed.ts b/src/scripts/models/feed.ts index 0ba8a61..dd22e46 100644 --- a/src/scripts/models/feed.ts +++ b/src/scripts/models/feed.ts @@ -4,34 +4,65 @@ import { ItemActionTypes, FETCH_ITEMS, RSSItem, MARK_READ, MARK_UNREAD, TOGGLE_S import { ActionStatus, AppThunk } from "../utils" import { PageActionTypes, SELECT_PAGE, PageType, APPLY_FILTER } from "./page" -export enum FeedFilter { +export enum FilterType { None, ShowRead = 1 << 0, ShowNotStarred = 1 << 1, ShowHidden = 1 << 2, + FullSearch = 1 << 3, Default = ShowRead | ShowNotStarred, UnreadOnly = ShowNotStarred, - StarredOnly = ShowRead + StarredOnly = ShowRead, + Toggles = ShowHidden | FullSearch } -export namespace FeedFilter { - export function toQueryObject(filter: FeedFilter) { +export class FeedFilter { + type: FilterType + search: string + + constructor(type=FilterType.Default, search="") { + this.type = type + this.search = search + } + + static toQueryObject(filter: FeedFilter) { + let type = filter.type let query = { hasRead: false, starred: true, hidden: { $exists: false } + } as any + if (type & FilterType.ShowRead) delete query.hasRead + if (type & FilterType.ShowNotStarred) delete query.starred + if (type & FilterType.ShowHidden) delete query.hidden + if (filter.search !== "") { + let regex = RegExp(filter.search) + if (type & FilterType.FullSearch) { + query.$or = [ + { title: { $regex: regex } }, + { snippet: { $regex: regex } } + ] + } else { + query.title = { $regex: regex } + } } - if (filter & FeedFilter.ShowRead) delete query.hasRead - if (filter & FeedFilter.ShowNotStarred) delete query.starred - if (filter & FeedFilter.ShowHidden) delete query.hidden return query } - export function testItem(filter: FeedFilter, item: RSSItem) { + static testItem(filter: FeedFilter, item: RSSItem) { + let type = filter.type let flag = true - if (!(filter & FeedFilter.ShowRead)) flag = flag && !item.hasRead - if (!(filter & FeedFilter.ShowNotStarred)) flag = flag && item.starred - if (!(filter & FeedFilter.ShowHidden)) flag = flag && !item.hidden + if (!(type & FilterType.ShowRead)) flag = flag && !item.hasRead + if (!(type & FilterType.ShowNotStarred)) flag = flag && item.starred + if (!(type & FilterType.ShowHidden)) flag = flag && !item.hidden + if (filter.search !== "") { + let regex = RegExp(filter.search) + if (type & FilterType.FullSearch) { + flag = flag && (regex.test(item.title) || regex.test(item.snippet)) + } else { + flag = flag && regex.test(item.title) + } + } return Boolean(flag) } } @@ -50,13 +81,13 @@ export class RSSFeed { iids: string[] filter: FeedFilter - constructor (id: string = null, sids=[], filter=FeedFilter.Default) { + constructor (id: string = null, sids=[], filter=null) { this._id = id this.sids = sids this.iids = [] this.loaded = false this.allLoaded = false - this.filter = filter + this.filter = filter === null ? new FeedFilter() : filter } static loadFeed(feed: RSSFeed, init = false): Promise { diff --git a/src/scripts/models/item.ts b/src/scripts/models/item.ts index f804d8a..4a6c006 100644 --- a/src/scripts/models/item.ts +++ b/src/scripts/models/item.ts @@ -1,4 +1,5 @@ import * as db from "../db" +import intl = require("react-intl-universal") import { rssParser, domParser, htmlDecode, ActionStatus, AppThunk } from "../utils" import { RSSSource } from "./source" import { FeedActionTypes, INIT_FEED, LOAD_MORE, FeedFilter } from "./feed" @@ -15,14 +16,13 @@ export class RSSItem { content: string snippet: string creator?: string - categories?: string[] hasRead: boolean starred?: true hidden?: true constructor (item: Parser.Item, source: RSSSource) { this.source = source.sid - this.title = item.title || "" + this.title = item.title || intl.get("article.untitled") this.link = item.link || "" this.fetchedDate = new Date() this.date = item.isoDate ? new Date(item.isoDate) : this.fetchedDate @@ -44,7 +44,6 @@ export class RSSItem { this.snippet = htmlDecode(item.contentSnippet || "") } this.creator = item.creator - this.categories = item.categories this.hasRead = false } } diff --git a/src/scripts/models/page.ts b/src/scripts/models/page.ts index 4bb5923..406429e 100644 --- a/src/scripts/models/page.ts +++ b/src/scripts/models/page.ts @@ -1,8 +1,9 @@ -import { ALL, SOURCE, loadMore, FeedFilter, initFeeds } from "./feed" +import { ALL, SOURCE, loadMore, FeedFilter, FilterType, initFeeds } from "./feed" import { getWindowBreakpoint, AppThunk } from "../utils" import { getDefaultView } from "../settings" import { RSSItem, markRead } from "./item" import { SourceActionTypes, DELETE_SOURCE } from "./source" +import { toggleMenu } from "./app" export const SELECT_PAGE = "SELECT_PAGE" export const SWITCH_VIEW = "SWITCH_VIEW" @@ -10,6 +11,7 @@ export const SHOW_ITEM = "SHOW_ITEM" export const SHOW_OFFSET_ITEM = "SHOW_OFFSET_ITEM" export const DISMISS_ITEM = "DISMISS_ITEM" export const APPLY_FILTER = "APPLY_FILTER" +export const TOGGLE_SEARCH = "TOGGLE_SEARCH" export enum PageType { AllArticles, Sources, Page @@ -47,8 +49,10 @@ interface ApplyFilterAction { } interface DismissItemAction { type: typeof DISMISS_ITEM } +interface ToggleSearchAction { type: typeof TOGGLE_SEARCH } -export type PageActionTypes = SelectPageAction | SwitchViewAction | ShowItemAction | DismissItemAction | ApplyFilterAction +export type PageActionTypes = SelectPageAction | SwitchViewAction | ShowItemAction + | DismissItemAction | ApplyFilterAction | ToggleSearchAction export function selectAllArticles(init = false): AppThunk { return (dispatch, getState) => { @@ -96,6 +100,22 @@ export function showItem(feedId: string, item: RSSItem): PageActionTypes { export const dismissItem = (): PageActionTypes => ({ type: DISMISS_ITEM }) +export const toggleSearch = (): AppThunk => { + return (dispatch, getState) => { + let state = getState() + dispatch(({ type: TOGGLE_SEARCH })) + if (!getWindowBreakpoint()) { + dispatch(toggleMenu()) + } + if (state.page.searchOn) { + dispatch(applyFilter({ + ...state.page.filter, + search: "" + })) + } + } +} + export function showOffsetItem(offset: number): AppThunk { return (dispatch, getState) => { let state = getState() @@ -150,28 +170,46 @@ function applyFilter(filter: FeedFilter): AppThunk { } } -export function switchFilter(filter: FeedFilter): AppThunk { +export function switchFilter(filter: FilterType): AppThunk { return (dispatch, getState) => { let oldFilter = getState().page.filter - let newFilter = filter | (oldFilter & FeedFilter.ShowHidden) - if (newFilter != oldFilter) { - dispatch(applyFilter(newFilter)) + let oldType = oldFilter.type + let newType = filter | (oldType & FilterType.Toggles) + if (oldType != newType) { + dispatch(applyFilter({ + ...oldFilter, + type: newType + })) } } } -export function toggleFilter(filter: FeedFilter): AppThunk { +export function toggleFilter(filter: FilterType): AppThunk { return (dispatch, getState) => { - let oldFilter = getState().page.filter - dispatch(applyFilter(oldFilter ^ filter)) + let nextFilter = { ...getState().page.filter } + nextFilter.type ^= filter + dispatch(applyFilter(nextFilter)) + } +} + +export function performSearch(query: string): AppThunk { + return (dispatch, getState) => { + let state = getState() + if (state.page.searchOn) { + dispatch(applyFilter({ + ...state.page.filter, + search: query + })) + } } } export class PageState { viewType = getDefaultView() - filter = FeedFilter.Default + filter = new FeedFilter() feedId = ALL itemId = null as string + searchOn = false } export function pageReducer( @@ -209,6 +247,10 @@ export function pageReducer( ...state, itemId: null } + case TOGGLE_SEARCH: return { + ...state, + searchOn: !state.searchOn + } default: return state } } \ No newline at end of file diff --git a/src/scripts/models/source.ts b/src/scripts/models/source.ts index ef65b60..d680203 100644 --- a/src/scripts/models/source.ts +++ b/src/scripts/models/source.ts @@ -1,9 +1,11 @@ import Parser = require("@yang991178/rss-parser") +import intl = require("react-intl-universal") import * as db from "../db" import { rssParser, faviconPromise, ActionStatus, AppThunk } from "../utils" import { RSSItem, insertItems, ItemActionTypes, FETCH_ITEMS, MARK_READ, MARK_UNREAD, MARK_ALL_READ } from "./item" import { SourceGroup } from "./group" import { saveSettings } from "./app" +import { remote } from "electron" export enum SourceOpenTarget { Local, Webpage, External @@ -14,7 +16,6 @@ export class RSSSource { url: string iconurl: string name: string - description: string openTarget: SourceOpenTarget unreadCount: number @@ -26,8 +27,10 @@ export class RSSSource { async fetchMetaData(parser: Parser) { let feed = await parser.parseURL(this.url) - if (!this.name && feed.title) this.name = feed.title.trim() - this.description = feed.description + if (!this.name) { + if (feed.title) this.name = feed.title.trim() + this.name = this.name || intl.get("sources.untitled") + } let domain = this.url.split("/").slice(0, 3).join("/") let f: string = null try { @@ -232,6 +235,9 @@ export function addSource(url: string, name: string = null, batch = false): AppT .catch(e => { console.log(e) dispatch(addSourceFailure(e, batch)) + if (!batch) { + remote.dialog.showErrorBox(intl.get("sources.errorAdd"), String(e)) + } return new Promise((_, reject) => { reject(e) }) }) }