mirror of
https://github.com/yang991178/fluent-reader.git
synced 2025-03-10 16:30:11 +01:00
commit
687cd8ef1d
16
dist/styles/cards.css
vendored
16
dist/styles/cards.css
vendored
@ -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;
|
||||
|
2
dist/styles/main.css
vendored
2
dist/styles/main.css
vendored
@ -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 |
@ -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
2
package-lock.json
generated
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "fluent-reader",
|
||||
"version": "0.7.3",
|
||||
"version": "0.7.4",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@ -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": {
|
||||
|
@ -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
|
||||
},
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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 []
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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",
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
@ -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()
|
||||
})
|
||||
}),
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -89,7 +89,9 @@
|
||||
"saveImageAs": "将图像另存为",
|
||||
"copyImage": "复制图像",
|
||||
"copyImageURL": "复制图像链接",
|
||||
"caseSensitive": "区分大小写"
|
||||
"caseSensitive": "区分大小写",
|
||||
"showCover": "显示封面",
|
||||
"showSnippet": "显示摘要"
|
||||
},
|
||||
"searchEngine": {
|
||||
"name": "搜索引擎",
|
||||
@ -170,6 +172,7 @@
|
||||
"title": "标题",
|
||||
"content": "正文",
|
||||
"fullSearch": "标题或正文",
|
||||
"creator": "作者",
|
||||
"match": "匹配",
|
||||
"notMatch": "不匹配",
|
||||
"regex": "正则表达式",
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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()))
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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) + "…"
|
||||
|
Loading…
x
Reference in New Issue
Block a user