mirror of
https://github.com/yang991178/fluent-reader.git
synced 2025-04-13 18:02:14 +02:00
search
This commit is contained in:
parent
d649bde776
commit
7385592ba7
25
dist/styles.css
vendored
25
dist/styles.css
vendored
@ -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);
|
||||
|
13
package.json
13
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",
|
||||
|
@ -24,7 +24,7 @@ class DefaultCard extends Card {
|
||||
) : null}
|
||||
<CardInfo source={this.props.source} item={this.props.item} />
|
||||
<h3 className="title">{this.props.item.title}</h3>
|
||||
<p className={"snippet"+(this.props.item.thumb?"":" show")}>{this.props.item.snippet}</p>
|
||||
<p className={"snippet"+(this.props.item.thumb?"":" show")}>{this.props.item.snippet.slice(0, 325)}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -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<ContextMenuProps> {
|
||||
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 [
|
||||
|
@ -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<HTMLElement>, key: string, selected: string) => void,
|
||||
toggleSearch: () => void,
|
||||
}
|
||||
|
||||
export class Menu extends React.Component<MenuProps> {
|
||||
@ -29,8 +31,10 @@ export class Menu extends React.Component<MenuProps> {
|
||||
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<MenuProps> {
|
||||
{link.ariaLabel !== "0" && <div className="unread-count">{link.ariaLabel}</div>}
|
||||
</Stack>
|
||||
)
|
||||
return ;
|
||||
};
|
||||
|
||||
render() {
|
||||
|
@ -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<PageProps> {
|
||||
<>
|
||||
{this.props.settingsOn ? null :
|
||||
<div className={"main" + (this.props.menuOn ? " menu-on" : "")}>
|
||||
<ArticleSearch />
|
||||
{this.props.feeds.map(fid => (
|
||||
<FeedContainer viewType={this.props.viewType} feedId={fid} key={fid} />
|
||||
))}
|
||||
@ -48,6 +50,7 @@ class Page extends React.Component<PageProps> {
|
||||
<>
|
||||
{this.props.settingsOn ? null :
|
||||
<div className={"list-main" + (this.props.menuOn ? " menu-on" : "")}>
|
||||
<ArticleSearch />
|
||||
<div className="list-feed-container">
|
||||
{this.props.feeds.map(fid => (
|
||||
<FeedContainer viewType={this.props.viewType} feedId={fid} key={fid} />
|
||||
|
63
src/components/utils/article-search.tsx
Normal file
63
src/components/utils/article-search.tsx
Normal file
@ -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<SearchProps> {
|
||||
debouncedSearch: (query: string) => void
|
||||
inputRef: React.RefObject<ISearchBox>
|
||||
|
||||
constructor(props: SearchProps) {
|
||||
super(props)
|
||||
this.debouncedSearch = new Debounced().use((query: string) => props.dispatch(performSearch(query)), 750)
|
||||
this.inputRef = React.createRef<ISearchBox>()
|
||||
}
|
||||
|
||||
onSearchChange = (_, newValue: string) => {
|
||||
this.debouncedSearch(newValue)
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: SearchProps) {
|
||||
if (this.props.searchOn && !prevProps.searchOn) {
|
||||
this.inputRef.current.focus()
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.props.searchOn && (
|
||||
<SearchBox
|
||||
componentRef={this.inputRef}
|
||||
className="article-search"
|
||||
placeholder={intl.get("search")}
|
||||
defaultValue={this.props.initQuery}
|
||||
onChange={this.onSearchChange} />
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const getSearchProps = (state: RootState) => ({
|
||||
searchOn: state.page.searchOn,
|
||||
initQuery: state.page.filter.search
|
||||
})
|
||||
export default connect(getSearchProps)(ArticleSearch)
|
@ -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())
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -26,7 +26,8 @@ function createWindow() {
|
||||
show: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
webviewTag: true
|
||||
webviewTag: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
})
|
||||
mainWindowState.manage(mainWindow)
|
||||
|
@ -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",
|
||||
|
@ -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": "订阅源名称",
|
||||
|
@ -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<RSSItem[]> {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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) })
|
||||
})
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user