Merge pull request #85 from yang991178/0.7.4

Version 0.7.4
This commit is contained in:
Haoyuan Liu 2020-08-25 10:41:38 +08:00 committed by GitHub
commit 687cd8ef1d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 253 additions and 67 deletions

16
dist/styles/cards.css vendored
View File

@ -195,22 +195,34 @@
}
.list-card .data {
flex-grow: 1;
margin: 8px 10px;
overflow: hidden;
}
.list-card .info {
margin: 8px 10px;
margin: 0 0 8px;
height: 16px;
}
.list-card h3.title {
font-size: 14px;
line-height: 18px;
font-weight: 600;
margin: 8px 10px;
margin: 0;
position: relative;
-webkit-line-clamp: 3;
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
}
.list-card p.snippet {
color: var(--neutralSecondary);
font-size: 12px;
line-height: 16px;
margin: 4px 0 0;
-webkit-line-clamp: 2;
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
}
.magazine-card {
width: 700px;

View File

@ -239,7 +239,7 @@ body.darwin .list-main .article-search {
margin-left: 0;
}
@media (min-width: 1441px) {
@media (min-width: 1440px) {
#root > nav.menu-on {
padding-left: 296px;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 86 KiB

View File

@ -78,6 +78,7 @@
<div><a href="https://github.com/yang991178/fluent-reader/releases">Download from GitHub Releases</a></div>
<div class="links">
<a href="https://github.com/yang991178/fluent-reader/">GitHub</a>
<a href="https://github.com/sponsors/yang991178">Sponsor</a>
<a href="https://github.com/yang991178/fluent-reader/wiki/Privacy">Privacy</a>
<a href="https://github.com/yang991178/fluent-reader/wiki/Support">Help</a>
</div>

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "fluent-reader",
"version": "0.7.3",
"version": "0.7.4",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@ -1,6 +1,6 @@
{
"name": "fluent-reader",
"version": "0.7.3",
"version": "0.7.4",
"description": "Modern desktop RSS reader",
"main": "./dist/electron.js",
"scripts": {

View File

@ -1,4 +1,4 @@
import { SourceGroup, ViewType, ThemeSettings, SearchEngines, ServiceConfigs } from "../schema-types"
import { SourceGroup, ViewType, ThemeSettings, SearchEngines, ServiceConfigs, ViewConfigs } from "../schema-types"
import { ipcRenderer } from "electron"
const settingsBridge = {
@ -96,6 +96,13 @@ const settingsBridge = {
ipcRenderer.invoke("set-filter-type", filterType)
},
getViewConfigs: (view: ViewType): ViewConfigs => {
return ipcRenderer.sendSync("get-view-configs", view)
},
setViewConfigs: (view: ViewType, configs: ViewConfigs) => {
ipcRenderer.invoke("set-view-configs", view, configs)
},
getAll: () => {
return ipcRenderer.sendSync("get-all-settings") as Object
},

View File

@ -3,6 +3,7 @@ import { RSSSource, SourceOpenTarget } from "../../scripts/models/source"
import { RSSItem } from "../../scripts/models/item"
import { platformCtrl } from "../../scripts/utils"
import { FeedFilter } from "../../scripts/models/feed"
import { ViewConfigs } from "../../schema-types"
export namespace Card {
export type Props = {
@ -10,6 +11,7 @@ export namespace Card {
item: RSSItem
source: RSSSource
filter: FeedFilter
viewConfigs?: ViewConfigs
shortcuts: (item: RSSItem, e: KeyboardEvent) => void
markRead: (item: RSSItem) => void
contextMenu: (feedId: string, item: RSSItem, e) => void

View File

@ -2,6 +2,7 @@ import * as React from "react"
import { Card } from "./card"
import CardInfo from "./info"
import Highlights from "./highlights"
import { ViewConfigs } from "../../schema-types"
const className = (props: Card.Props) => {
let cn = ["card", "list-card"]
@ -15,12 +16,15 @@ const ListCard: React.FunctionComponent<Card.Props> = (props) => (
{...Card.bindEventsToProps(props)}
data-iid={props.item._id}
data-is-focusable>
{props.item.thumb ? (
{props.item.thumb && (props.viewConfigs & ViewConfigs.ShowCover) ? (
<div className="head"><img src={props.item.thumb} /></div>
) : null}
<div className="data">
<CardInfo source={props.source} item={props.item} />
<h3 className="title"><Highlights text={props.item.title} filter={props.filter} title /></h3>
{Boolean(props.viewConfigs & ViewConfigs.ShowSnippet) && (
<p className="snippet"><Highlights text={props.item.snippet} filter={props.filter} /></p>
)}
</div>
</div>
)

View File

@ -6,7 +6,7 @@ import { ContextualMenu, IContextualMenuItem, ContextualMenuItemType, Directiona
import { ContextMenuType } from "../scripts/models/app"
import { RSSItem } from "../scripts/models/item"
import { ContextReduxProps } from "../containers/context-menu-container"
import { ViewType, ImageCallbackTypes } from "../schema-types"
import { ViewType, ImageCallbackTypes, ViewConfigs } from "../schema-types"
import { FilterType } from "../scripts/models/feed"
export type ContextMenuProps = ContextReduxProps & {
@ -18,6 +18,7 @@ export type ContextMenuProps = ContextReduxProps & {
text?: string
url?: string
viewType?: ViewType
viewConfigs?: ViewConfigs
filter?: FilterType
sids?: number[]
showItem: (feedId: string, item: RSSItem) => void
@ -26,10 +27,12 @@ export type ContextMenuProps = ContextReduxProps & {
toggleStarred: (item: RSSItem) => void
toggleHidden: (item: RSSItem) => void
switchView: (viewType: ViewType) => void
setViewConfigs: (configs: ViewConfigs) => void
switchFilter: (filter: FilterType) => void
toggleFilter: (filter: FilterType) => void
markAllRead: (sids: number[], date?: Date, before?: boolean) => void
settings: () => void
markAllRead: (sids: number[], date?: Date, before?: boolean) => void
fetchItems: (sids: number[]) => void
settings: (sids: number[]) => void
close: () => void
}
@ -142,7 +145,35 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
key: "copyURL",
text: intl.get("context.copyURL"),
onClick: () => { window.utils.writeClipboard(this.props.item.link) }
}
},
...(this.props.viewConfigs !== undefined ? [
{
key: "divider_2",
itemType: ContextualMenuItemType.Divider,
},
{
key: "view",
text: intl.get("context.view"),
subMenuProps: {
items: [
{
key: "showCover",
text: intl.get("context.showCover"),
canCheck: true,
checked: Boolean(this.props.viewConfigs & ViewConfigs.ShowCover),
onClick: () => this.props.setViewConfigs(this.props.viewConfigs ^ ViewConfigs.ShowCover)
},
{
key: "showSnippet",
text: intl.get("context.showSnippet"),
canCheck: true,
checked: Boolean(this.props.viewConfigs & ViewConfigs.ShowSnippet),
onClick: () => this.props.setViewConfigs(this.props.viewConfigs ^ ViewConfigs.ShowSnippet)
},
]
}
},
] : [])
]
case ContextMenuType.Text: {
const items: IContextualMenuItem[] = this.props.text? [
@ -329,11 +360,17 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
iconProps: { iconName: "CheckMark" },
onClick: () => this.props.markAllRead(this.props.sids)
},
{
key: "refresh",
text: intl.get("nav.refresh"),
iconProps: { iconName: "Sync" },
onClick: () => this.props.fetchItems(this.props.sids)
},
{
key: "manage",
text: intl.get("context.manageSources"),
iconProps: { iconName: "Settings" },
onClick: this.props.settings
onClick: () => this.props.settings(this.props.sids)
}
]
default: return []

View File

@ -2,13 +2,14 @@ import * as React from "react"
import { RSSItem } from "../../scripts/models/item"
import { FeedReduxProps } from "../../containers/feed-container"
import { RSSFeed, FeedFilter } from "../../scripts/models/feed"
import { ViewType } from "../../schema-types"
import { ViewType, ViewConfigs } from "../../schema-types"
import CardsFeed from "./cards-feed"
import ListFeed from "./list-feed"
export type FeedProps = FeedReduxProps & {
feed: RSSFeed
viewType: ViewType
viewConfigs?: ViewConfigs
items: RSSItem[]
sourceMap: Object
filter: FeedFilter

View File

@ -17,6 +17,7 @@ class ListFeed extends React.Component<FeedProps> {
item: item,
source: this.props.sourceMap[item.source],
filter: this.props.filter,
viewConfigs: this.props.viewConfigs,
shortcuts: this.props.shortcuts,
markRead: this.props.markRead,
contextMenu: this.props.contextMenu,

View File

@ -23,10 +23,19 @@ class Settings extends React.Component<SettingsProps> {
super(props)
}
onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape" && !this.props.exitting) this.props.close()
}
componentDidUpdate = (prevProps: SettingsProps) => {
if (window.utils.platform === "darwin" && this.props.display !== prevProps.display) {
if (this.props.display) window.utils.destroyTouchBar()
else initTouchBarWithTexts()
if (this.props.display !== prevProps.display) {
if (this.props.display) {
if (window.utils.platform === "darwin") window.utils.destroyTouchBar()
document.body.addEventListener("keydown", this.onKeyDown)
} else {
if (window.utils.platform === "darwin") initTouchBarWithTexts()
document.body.removeEventListener("keydown", this.onKeyDown)
}
}
}

View File

@ -30,11 +30,12 @@ type RulesTabState = {
selectedRules: number[]
editIndex: number
regex: string
fullSearch: boolean
searchType: number
caseSensitive: boolean
match: boolean
actionKeys: string[]
mockTitle: string
mockCreator: string
mockContent: string
mockResult: string
}
@ -52,11 +53,12 @@ class RulesTab extends React.Component<RulesTabProps, RulesTabState> {
selectedRules: [],
editIndex: -1,
regex: "",
fullSearch: false,
searchType: 0,
caseSensitive: false,
match: true,
actionKeys: [],
mockTitle: "",
mockCreator: "",
mockContent: "",
mockResult: ""
}
@ -103,9 +105,14 @@ class RulesTab extends React.Component<RulesTabProps, RulesTabState> {
}
initRuleEdit = (rule: SourceRule = null) => {
let searchType = 0
if (rule) {
if (rule.filter.type & FilterType.FullSearch) searchType = 1
else if (rule.filter.type & FilterType.CreatorSearch) searchType = 2
}
this.setState({
regex: rule ? rule.filter.search : "",
fullSearch: rule ? Boolean(rule.filter.type & FilterType.FullSearch) : false,
searchType: searchType,
caseSensitive: rule ? !(rule.filter.type & FilterType.CaseInsensitive) : false,
match: rule ? rule.match : true,
actionKeys: rule ? RuleActions.toKeys(rule.actions) : []
@ -157,16 +164,17 @@ class RulesTab extends React.Component<RulesTabProps, RulesTabState> {
this.rulesSelection.setAllSelected(false)
this.setState({
sid: item.key as string, selectedRules: [], editIndex: -1,
mockTitle: "", mockContent: "", mockResult: ""
mockTitle: "", mockCreator: "", mockContent: "", mockResult: ""
})
}
searchOptions = (): IDropdownOption[] => [
{ key: 0, text: intl.get("rules.title") },
{ key: 1, text: intl.get("rules.fullSearch") }
{ key: 1, text: intl.get("rules.fullSearch") },
{ key: 2, text: intl.get("rules.creator") },
]
onSearchOptionChange = (_, item: IDropdownOption) => {
this.setState({ fullSearch: Boolean(item.key) })
this.setState({ searchType: item.key as number })
}
matchOptions = (): IDropdownOption[] => [
@ -207,7 +215,11 @@ class RulesTab extends React.Component<RulesTabProps, RulesTabState> {
}
saveRule = () => {
let rule = new SourceRule(this.state.regex, this.state.actionKeys, this.state.fullSearch, this.state.caseSensitive, this.state.match)
let filterType = FilterType.Default | FilterType.ShowHidden
if (!this.state.caseSensitive) filterType |= FilterType.CaseInsensitive
if (this.state.searchType === 1) filterType |= FilterType.FullSearch
else if (this.state.searchType === 2) filterType |= FilterType.CreatorSearch
let rule = new SourceRule(this.state.regex, this.state.actionKeys, filterType, this.state.match)
let source = this.props.sources[parseInt(this.state.sid)]
let rules = source.rules ? [ ...source.rules ] : []
if (this.state.editIndex === -1) {
@ -263,6 +275,7 @@ class RulesTab extends React.Component<RulesTabProps, RulesTabState> {
let source = this.props.sources[parseInt(this.state.sid)]
let item = new RSSItem(parsed as Parser.Item, source)
item.snippet = this.state.mockContent
item.creator = this.state.mockCreator
SourceRule.applyAll(this.getSourceRules(), item)
let result = []
result.push(intl.get(item.hasRead ? "article.markRead" : "article.markUnread"))
@ -319,7 +332,7 @@ class RulesTab extends React.Component<RulesTabProps, RulesTabState> {
<Stack.Item>
<Dropdown
options={this.searchOptions()}
selectedKey={this.state.fullSearch ? 1 : 0}
selectedKey={this.state.searchType}
onChange={this.onSearchOptionChange}
style={{width: 140}} />
</Stack.Item>
@ -393,6 +406,15 @@ class RulesTab extends React.Component<RulesTabProps, RulesTabState> {
value={this.state.mockTitle}
onChange={this.handleInputChange} />
</Stack.Item>
<Stack.Item grow>
<TextField
name="mockCreator"
placeholder={intl.get("rules.creator")}
value={this.state.mockCreator}
onChange={this.handleInputChange} />
</Stack.Item>
</Stack>
<Stack horizontal>
<Stack.Item grow>
<TextField
name="mockContent"

View File

@ -10,6 +10,8 @@ import DangerButton from "../utils/danger-button"
type SourcesTabProps = {
sources: SourceState
serviceOn: boolean
sids: number[]
acknowledgeSIDs: () => void
addSource: (url: string) => void
updateSourceName: (source: RSSSource, name: string) => void
updateSourceIcon: (source: RSSSource, iconUrl: string) => Promise<void>
@ -61,6 +63,15 @@ class SourcesTab extends React.Component<SourcesTabProps, SourcesTabState> {
})
}
componentDidMount = () => {
if (this.props.sids.length > 0) {
for (let sid of this.props.sids) {
this.selection.setKeySelected(String(sid), true, false)
}
this.props.acknowledgeSIDs()
}
}
columns = (): IColumn[] => [
{
key: "favicon",

View File

@ -3,22 +3,24 @@ import { createSelector } from "reselect"
import { RootState } from "../scripts/reducer"
import { ContextMenuType, closeContextMenu, toggleSettings } from "../scripts/models/app"
import { ContextMenu } from "../components/context-menu"
import { RSSItem, markRead, markUnread, toggleStarred, toggleHidden, markAllRead } from "../scripts/models/item"
import { showItem, switchView, switchFilter, toggleFilter } from "../scripts/models/page"
import { ViewType } from "../schema-types"
import { RSSItem, markRead, markUnread, toggleStarred, toggleHidden, markAllRead, fetchItems } from "../scripts/models/item"
import { showItem, switchView, switchFilter, toggleFilter, setViewConfigs } from "../scripts/models/page"
import { ViewType, ViewConfigs } from "../schema-types"
import { FilterType } from "../scripts/models/feed"
const getContext = (state: RootState) => state.app.contextMenu
const getViewType = (state: RootState) => state.page.viewType
const getFilter = (state: RootState) => state.page.filter
const getViewConfigs = (state: RootState) => state.page.viewConfigs
const mapStateToProps = createSelector(
[getContext, getViewType, getFilter],
(context, viewType, filter) => {
[getContext, getViewType, getFilter, getViewConfigs],
(context, viewType, filter, viewConfigs) => {
switch (context.type) {
case ContextMenuType.Item: return {
type: context.type,
event: context.event,
viewConfigs: viewConfigs,
item: context.target[0],
feedId: context.target[1]
}
@ -65,12 +67,14 @@ const mapDispatchToProps = dispatch => {
window.settings.setDefaultView(viewType)
dispatch(switchView(viewType))
},
setViewConfigs: (configs: ViewConfigs) => dispatch(setViewConfigs(configs)),
switchFilter: (filter: FilterType) => dispatch(switchFilter(filter)),
toggleFilter: (filter: FilterType) => dispatch(toggleFilter(filter)),
markAllRead: (sids: number[], date?: Date, before?: boolean) => {
dispatch(markAllRead(sids, date, before))
},
settings: () => dispatch(toggleSettings()),
fetchItems: (sids: number[]) => dispatch(fetchItems(false, sids)),
settings: (sids: number[]) => dispatch(toggleSettings(true, sids)),
close: () => dispatch(closeContextMenu())
}
}

View File

@ -18,16 +18,18 @@ const getItems = (state: RootState) => state.items
const getFeed = (state: RootState, props: FeedContainerProps) => state.feeds[props.feedId]
const getFilter = (state: RootState) => state.page.filter
const getView = (_, props: FeedContainerProps) => props.viewType
const getViewConfigs = (state: RootState) => state.page.viewConfigs
const makeMapStateToProps = () => {
return createSelector(
[getSources, getItems, getFeed, getView, getFilter],
(sources, items, feed, viewType, filter) => ({
[getSources, getItems, getFeed, getView, getFilter, getViewConfigs],
(sources, items, feed, viewType, filter, viewConfigs) => ({
feed: feed,
items: feed.iids.map(iid => items[iid]),
sourceMap: sources,
filter: filter,
viewType: viewType
viewType: viewType,
viewConfigs: viewConfigs,
})
)
}

View File

@ -2,10 +2,11 @@ import { connect } from "react-redux"
import { initIntl, saveSettings, setupAutoFetch } from "../../scripts/models/app"
import * as db from "../../scripts/db"
import AppTab from "../../components/settings/app"
import { initFeeds } from "../../scripts/models/feed"
import { importAll } from "../../scripts/settings"
import { updateUnreadCounts } from "../../scripts/models/source"
import { AppDispatch } from "../../scripts/utils"
const mapDispatchToProps = dispatch => ({
const mapDispatchToProps = (dispatch: AppDispatch) => ({
setLanguage: (option: string) => {
window.settings.setLocaleSettings(option)
dispatch(initIntl())
@ -19,10 +20,8 @@ const mapDispatchToProps = dispatch => ({
let date = new Date()
date.setTime(date.getTime() - days * 86400000)
db.idb.remove({ date: { $lt: date } }, { multi: true }, () => {
dispatch(initFeeds(true)).then(() => dispatch(saveSettings()))
db.idb.prependOnceListener("compaction.done", () => {
resolve()
})
dispatch(updateUnreadCounts()).then(() => dispatch(saveSettings()))
db.idb.prependOnceListener("compaction.done", resolve)
db.idb.persistence.compactDatafile()
})
}),

View File

@ -6,22 +6,25 @@ import SourcesTab from "../../components/settings/sources"
import { addSource, RSSSource, updateSource, deleteSource, SourceOpenTarget, deleteSources } from "../../scripts/models/source"
import { importOPML, exportOPML } from "../../scripts/models/group"
import { AppDispatch, validateFavicon } from "../../scripts/utils"
import { saveSettings } from "../../scripts/models/app"
import { saveSettings, toggleSettings } from "../../scripts/models/app"
import { SyncService } from "../../schema-types"
const getSources = (state: RootState) => state.sources
const getServiceOn = (state: RootState) => state.service.type !== SyncService.None
const getSIDs = (state: RootState) => state.app.settings.sids
const mapStateToProps = createSelector(
[getSources, getServiceOn],
(sources, serviceOn) => ({
[getSources, getServiceOn, getSIDs],
(sources, serviceOn, sids) => ({
sources: sources,
serviceOn: serviceOn
serviceOn: serviceOn,
sids: sids,
})
)
const mapDispatchToProps = (dispatch: AppDispatch) => {
return {
acknowledgeSIDs: () => dispatch(toggleSettings(true)),
addSource: (url: string) => dispatch(addSource(url)),
updateSourceName: (source: RSSSource, name: string) => {
dispatch(updateSource({ ...source, name: name } as RSSSource))

View File

@ -1,6 +1,6 @@
import Store = require("electron-store")
import { SchemaTypes, SourceGroup, ViewType, ThemeSettings, SearchEngines,
SyncService, ServiceConfigs } from "../schema-types"
SyncService, ServiceConfigs, ViewConfigs } from "../schema-types"
import { ipcMain, session, nativeTheme, app } from "electron"
import { WindowManager } from "./window"
@ -153,3 +153,22 @@ ipcMain.on("get-filter-type", (event) => {
ipcMain.handle("set-filter-type", (_, filterType: number) => {
store.set(FILTER_TYPE_STORE_KEY, filterType)
})
const LIST_CONFIGS_STORE_KEY = "listViewConfigs"
ipcMain.on("get-view-configs", (event, view: ViewType) => {
switch (view) {
case ViewType.List:
event.returnValue = store.get(LIST_CONFIGS_STORE_KEY, ViewConfigs.ShowCover)
break
default:
event.returnValue = undefined
break
}
})
ipcMain.handle("set-view-configs", (_, view: ViewType, configs: ViewConfigs) => {
switch (view) {
case ViewType.List:
store.set(LIST_CONFIGS_STORE_KEY, configs)
break
}
})

View File

@ -22,6 +22,11 @@ export const enum ViewType {
Cards, List, Magazine, Compact, Customized
}
export const enum ViewConfigs {
ShowCover = 1 << 0,
ShowSnippet = 1 << 1,
}
export const enum ThemeSettings {
Default = "system",
Light = "light",
@ -70,4 +75,5 @@ export type SchemaTypes = {
searchEngine: SearchEngines
serviceConfigs: ServiceConfigs
filterType: number
listViewConfigs: ViewConfigs
}

View File

@ -89,7 +89,9 @@
"saveImageAs": "Save image as …",
"copyImage": "Copy image",
"copyImageURL": "Copy image link",
"caseSensitive": "Case sensitive"
"caseSensitive": "Case sensitive",
"showCover": "Show cover",
"showSnippet": "Show snippet"
},
"searchEngine": {
"name": "Search engine",
@ -172,6 +174,7 @@
"title": "Title",
"content": "Content",
"fullSearch": "Title or content",
"creator": "Author",
"match": "matches",
"notMatch": "doesn't match",
"regex": "Regular expression",

View File

@ -89,7 +89,9 @@
"saveImageAs": "将图像另存为",
"copyImage": "复制图像",
"copyImageURL": "复制图像链接",
"caseSensitive": "区分大小写"
"caseSensitive": "区分大小写",
"showCover": "显示封面",
"showSnippet": "显示摘要"
},
"searchEngine": {
"name": "搜索引擎",
@ -170,6 +172,7 @@
"title": "标题",
"content": "正文",
"fullSearch": "标题或正文",
"creator": "作者",
"match": "匹配",
"notMatch": "不匹配",
"regex": "正则表达式",

View File

@ -49,6 +49,7 @@ export class AppState {
settings = {
display: false,
changed: false,
sids: new Array<number>(),
saving: false
}
logMenu = {
@ -136,9 +137,15 @@ export interface MenuActionTypes {
export const TOGGLE_SETTINGS = "TOGGLE_SETTINGS"
export const SAVE_SETTINGS = "SAVE_SETTINGS"
export interface SettingsActionTypes {
type: typeof TOGGLE_SETTINGS | typeof SAVE_SETTINGS
interface ToggleSettingsAction {
type: typeof TOGGLE_SETTINGS
open: boolean
sids: number[]
}
interface SaveSettingsAction {
type: typeof SAVE_SETTINGS
}
export type SettingsActionTypes = ToggleSettingsAction | SaveSettingsAction
export function closeContextMenu(): AppThunk {
return (dispatch, getState) => {
@ -190,9 +197,14 @@ export function toggleMenu(): AppThunk {
}
export const toggleLogMenu = () => ({ type: TOGGLE_LOGS })
export const toggleSettings = () => ({ type: TOGGLE_SETTINGS })
export const saveSettings = () => ({ type: SAVE_SETTINGS })
export const toggleSettings = (open = true, sids = new Array<number>()) => ({
type: TOGGLE_SETTINGS,
open: open,
sids: sids,
})
export function exitSettings(): AppThunk {
return (dispatch, getState) => {
if (!getState().app.settings.saving) {
@ -200,10 +212,10 @@ export function exitSettings(): AppThunk {
dispatch(saveSettings())
dispatch(selectAllArticles(true))
dispatch(initFeeds(true)).then(() =>
dispatch(toggleSettings())
dispatch(toggleSettings(false))
)
} else {
dispatch(toggleSettings())
dispatch(toggleSettings(false))
}
}
}
@ -493,8 +505,9 @@ export function appReducer(
case TOGGLE_SETTINGS: return {
...state,
settings: {
display: !state.settings.display,
display: action.open,
changed: false,
sids: action.sids,
saving: false
}
}

View File

@ -1,6 +1,6 @@
import * as db from "../db"
import { SourceActionTypes, INIT_SOURCES, ADD_SOURCE, DELETE_SOURCE } from "./source"
import { ItemActionTypes, FETCH_ITEMS, RSSItem, MARK_READ, MARK_UNREAD, TOGGLE_STARRED, TOGGLE_HIDDEN, applyItemReduction, ItemState, MARK_ALL_READ } from "./item"
import { ItemActionTypes, FETCH_ITEMS, RSSItem, MARK_READ, MARK_UNREAD, TOGGLE_STARRED, TOGGLE_HIDDEN, applyItemReduction } from "./item"
import { ActionStatus, AppThunk, mergeSortedArrays } from "../utils"
import { PageActionTypes, SELECT_PAGE, PageType, APPLY_FILTER } from "./page"
@ -11,6 +11,7 @@ export enum FilterType {
ShowHidden = 1 << 2,
FullSearch = 1 << 3,
CaseInsensitive = 1 << 4,
CreatorSearch = 1 << 5,
Default = ShowRead | ShowNotStarred,
UnreadOnly = ShowNotStarred,
@ -65,6 +66,8 @@ export class FeedFilter {
const regex = RegExp(filter.search, flags)
if (type & FilterType.FullSearch) {
flag = flag && (regex.test(item.title) || regex.test(item.snippet))
} else if (type & FilterType.CreatorSearch) {
flag = flag && (regex.test(item.creator || ""))
} else {
flag = flag && regex.test(item.title)
}

View File

@ -172,17 +172,21 @@ export function insertItems(items: RSSItem[]): Promise<RSSItem[]> {
})
}
export function fetchItems(background = false): AppThunk<Promise<void>> {
export function fetchItems(background = false, sids: number[] = null): AppThunk<Promise<void>> {
return async (dispatch, getState) => {
let promises = new Array<Promise<RSSItem[]>>()
const initState = getState()
if (!initState.app.fetchingItems && !initState.app.syncing) {
await dispatch(syncWithService(background))
if (sids === null || sids.filter(sid => initState.sources[sid].serviceRef !== undefined).length > 0)
await dispatch(syncWithService(background))
let timenow = new Date().getTime()
let sources = <RSSSource[]>Object.values(getState().sources).filter(s => {
let last = s.lastFetched ? s.lastFetched.getTime() : 0
return !s.serviceRef && ((last > timenow) || (last + (s.fetchFrequency || 0) * 60000 <= timenow))
})
const sourcesState = getState().sources
let sources = (sids === null)
? Object.values(sourcesState).filter(s => {
let last = s.lastFetched ? s.lastFetched.getTime() : 0
return !s.serviceRef && ((last > timenow) || (last + (s.fetchFrequency || 0) * 60000 <= timenow))
})
: sids.map(sid => sourcesState[sid]).filter(s => !s.serviceRef)
for (let source of sources) {
let promise = RSSSource.fetchItems(source)
promise.finally(() => dispatch(fetchItemsIntermediate()))

View File

@ -3,10 +3,11 @@ import { getWindowBreakpoint, AppThunk, ActionStatus } from "../utils"
import { RSSItem, markRead } from "./item"
import { SourceActionTypes, DELETE_SOURCE } from "./source"
import { toggleMenu } from "./app"
import { ViewType } from "../../schema-types"
import { ViewType, ViewConfigs } from "../../schema-types"
export const SELECT_PAGE = "SELECT_PAGE"
export const SWITCH_VIEW = "SWITCH_VIEW"
export const SET_VIEW_CONFIGS = "SET_VIEW_CONFIGS"
export const SHOW_ITEM = "SHOW_ITEM"
export const SHOW_OFFSET_ITEM = "SHOW_OFFSET_ITEM"
export const DISMISS_ITEM = "DISMISS_ITEM"
@ -33,6 +34,11 @@ interface SwitchViewAction {
viewType: ViewType
}
interface SetViewConfigsAction {
type: typeof SET_VIEW_CONFIGS
configs: ViewConfigs
}
interface ShowItemAction {
type: typeof SHOW_ITEM
feedId: string
@ -48,7 +54,7 @@ interface DismissItemAction { type: typeof DISMISS_ITEM }
interface ToggleSearchAction { type: typeof TOGGLE_SEARCH }
export type PageActionTypes = SelectPageAction | SwitchViewAction | ShowItemAction
| DismissItemAction | ApplyFilterAction | ToggleSearchAction
| DismissItemAction | ApplyFilterAction | ToggleSearchAction | SetViewConfigsAction
export function selectAllArticles(init = false): AppThunk {
return (dispatch, getState) => {
@ -86,6 +92,16 @@ export function switchView(viewType: ViewType): PageActionTypes {
}
}
export function setViewConfigs(configs: ViewConfigs): AppThunk {
return (dispatch, getState) => {
window.settings.setViewConfigs(getState().page.viewType, configs)
dispatch({
type: "SET_VIEW_CONFIGS",
configs: configs
})
}
}
export function showItem(feedId: string, item: RSSItem): AppThunk {
return (dispatch, getState) => {
const state = getState()
@ -218,6 +234,7 @@ export function performSearch(query: string): AppThunk {
export class PageState {
viewType = window.settings.getDefaultView()
viewConfigs = window.settings.getViewConfigs(window.settings.getDefaultView())
filter = new FeedFilter()
feedId = ALL
itemId = null as string
@ -247,7 +264,12 @@ export function pageReducer(
case SWITCH_VIEW: return {
...state,
viewType: action.viewType,
itemId: null
viewConfigs: window.settings.getViewConfigs(action.viewType),
itemId: null
}
case SET_VIEW_CONFIGS: return {
...state,
viewConfigs: action.configs
}
case APPLY_FILTER: return {
...state,

View File

@ -65,10 +65,8 @@ export class SourceRule {
match: boolean
actions: RuleActions
constructor(regex: string, actions: string[], fullSearch: boolean, caseSensitive: boolean, match: boolean) {
this.filter = new FeedFilter(FilterType.Default | FilterType.ShowHidden, regex)
if (fullSearch) this.filter.type |= FilterType.FullSearch
if (!caseSensitive) this.filter.type |= FilterType.CaseInsensitive
constructor(regex: string, actions: string[], filter: FilterType, match: boolean) {
this.filter = new FeedFilter(filter, regex)
this.match = match
this.actions = RuleActions.fromKeys(actions)
}

View File

@ -124,7 +124,7 @@ export function htmlDecode(input: string) {
export const urlTest = (s: string) =>
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/gi.test(s)
export const getWindowBreakpoint = () => window.outerWidth >= 1441
export const getWindowBreakpoint = () => window.outerWidth >= 1440
export const cutText = (s: string, length: number) => {
return (s.length <= length) ? s : s.slice(0, length) + "…"