Merge pull request #431 from yang991178/1.1.2

Version 1.1.2
This commit is contained in:
Haoyuan Liu 2022-06-19 16:23:04 -07:00 committed by GitHub
commit b827f268ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 765 additions and 381 deletions

View File

@ -14,9 +14,9 @@ jobs:
- name: Build and package the app - name: Build and package the app
run: | run: |
sudo npm install --unsafe-perm=true --allow-root npm install
npm run build npm run build
sudo npm run package-linux npm run package-linux
- name: Get app version - name: Get app version
id: package-version id: package-version

View File

@ -91,6 +91,10 @@
.card span.h { .card span.h {
background: #fce10080; background: #fce10080;
} }
.card.rtl .snippet,
.card.rtl .title {
direction: rtl;
}
.default-card { .default-card {
display: inline-block; display: inline-block;

View File

@ -209,7 +209,7 @@ img.favicon.dropdown {
.main::before { .main::before {
content: ""; content: "";
display: block; display: block;
position: sticky; position: relative;
top: var(--navHeight); top: var(--navHeight);
left: 0; left: 0;
width: calc(100% - 16px); width: calc(100% - 16px);

View File

@ -1,5 +1,5 @@
appId: DevHYLiu.FluentReader appId: DevHYLiu.FluentReader
buildVersion: 26 buildVersion: 27
productName: Fluent Reader productName: Fluent Reader
copyright: Copyright © 2020 Haoyuan Liu copyright: Copyright © 2020 Haoyuan Liu
files: files:

999
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "fluent-reader", "name": "fluent-reader",
"version": "1.1.1", "version": "1.1.2",
"description": "Modern desktop RSS reader", "description": "Modern desktop RSS reader",
"main": "./dist/electron.js", "main": "./dist/electron.js",
"scripts": { "scripts": {
@ -27,7 +27,7 @@
"@types/react-redux": "^7.1.9", "@types/react-redux": "^7.1.9",
"@yang991178/rss-parser": "^3.8.1", "@yang991178/rss-parser": "^3.8.1",
"electron": "^19.0.0", "electron": "^19.0.0",
"electron-builder": "^22.11.3", "electron-builder": "^23.0.3",
"electron-react-devtools": "^0.5.3", "electron-react-devtools": "^0.5.3",
"electron-store": "^5.2.0", "electron-store": "^5.2.0",
"electron-window-state": "^5.0.3", "electron-window-state": "^5.0.3",

View File

@ -3,10 +3,12 @@ import { Card } from "./card"
import CardInfo from "./info" import CardInfo from "./info"
import Time from "../utils/time" import Time from "../utils/time"
import Highlights from "./highlights" import Highlights from "./highlights"
import { SourceTextDirection } from "../../scripts/models/source"
const className = (props: Card.Props) => { const className = (props: Card.Props) => {
let cn = ["card", "compact-card"] let cn = ["card", "compact-card"]
if (props.item.hidden) cn.push("hidden") if (props.item.hidden) cn.push("hidden")
if (props.source.textDir === SourceTextDirection.RTL) cn.push("rtl")
return cn.join(" ") return cn.join(" ")
} }
@ -23,15 +25,10 @@ const CompactCard: React.FunctionComponent<Card.Props> = props => (
text={props.item.title} text={props.item.title}
filter={props.filter} filter={props.filter}
title title
dir={props.source.textDir}
/> />
</span> </span>
<span className="snippet"> <span className="snippet">
<Highlights <Highlights text={props.item.snippet} filter={props.filter} />
text={props.item.snippet}
filter={props.filter}
dir={props.source.textDir}
/>
</span> </span>
</div> </div>
<Time date={props.item.date} /> <Time date={props.item.date} />

View File

@ -2,11 +2,13 @@ import * as React from "react"
import { Card } from "./card" import { Card } from "./card"
import CardInfo from "./info" import CardInfo from "./info"
import Highlights from "./highlights" import Highlights from "./highlights"
import { SourceTextDirection } from "../../scripts/models/source"
const className = (props: Card.Props) => { const className = (props: Card.Props) => {
let cn = ["card", "default-card"] let cn = ["card", "default-card"]
if (props.item.snippet && props.item.thumb) cn.push("transform") if (props.item.snippet && props.item.thumb) cn.push("transform")
if (props.item.hidden) cn.push("hidden") if (props.item.hidden) cn.push("hidden")
if (props.source.textDir === SourceTextDirection.RTL) cn.push("rtl")
return cn.join(" ") return cn.join(" ")
} }
@ -25,19 +27,10 @@ const DefaultCard: React.FunctionComponent<Card.Props> = props => (
) : null} ) : null}
<CardInfo source={props.source} item={props.item} /> <CardInfo source={props.source} item={props.item} />
<h3 className="title"> <h3 className="title">
<Highlights <Highlights text={props.item.title} filter={props.filter} title />
text={props.item.title}
filter={props.filter}
title
dir={props.source.textDir}
/>
</h3> </h3>
<p className={"snippet" + (props.item.thumb ? "" : " show")}> <p className={"snippet" + (props.item.thumb ? "" : " show")}>
<Highlights <Highlights text={props.item.snippet} filter={props.filter} />
text={props.item.snippet}
filter={props.filter}
dir={props.source.textDir}
/>
</p> </p>
</div> </div>
) )

View File

@ -7,7 +7,6 @@ type HighlightsProps = {
text: string text: string
filter: FeedFilter filter: FeedFilter
title?: boolean title?: boolean
dir?: SourceTextDirection
} }
const Highlights: React.FunctionComponent<HighlightsProps> = props => { const Highlights: React.FunctionComponent<HighlightsProps> = props => {
@ -59,22 +58,10 @@ const Highlights: React.FunctionComponent<HighlightsProps> = props => {
} }
} }
const testStyle = {
direction: "inherit",
} as React.CSSProperties
if (props.dir === SourceTextDirection.RTL) {
testStyle.direction = "rtl"
}
return ( return (
<> <>
{spans.map(([text, flag]) => {spans.map(([text, flag]) =>
flag ? ( flag ? <span className="h">{text}</span> : text
<div className="h" style={testStyle}>
{text}
</div>
) : (
<div style={testStyle}>{text}</div>
)
)} )}
</> </>
) )

View File

@ -3,6 +3,7 @@ import { Card } from "./card"
import CardInfo from "./info" import CardInfo from "./info"
import Highlights from "./highlights" import Highlights from "./highlights"
import { ViewConfigs } from "../../schema-types" import { ViewConfigs } from "../../schema-types"
import { SourceTextDirection } from "../../scripts/models/source"
const className = (props: Card.Props) => { const className = (props: Card.Props) => {
let cn = ["card", "list-card"] let cn = ["card", "list-card"]
@ -10,6 +11,7 @@ const className = (props: Card.Props) => {
if (props.selected) cn.push("selected") if (props.selected) cn.push("selected")
if (props.viewConfigs & ViewConfigs.FadeRead && props.item.hasRead) if (props.viewConfigs & ViewConfigs.FadeRead && props.item.hasRead)
cn.push("read") cn.push("read")
if (props.source.textDir === SourceTextDirection.RTL) cn.push("rtl")
return cn.join(" ") return cn.join(" ")
} }
@ -31,7 +33,6 @@ const ListCard: React.FunctionComponent<Card.Props> = props => (
text={props.item.title} text={props.item.title}
filter={props.filter} filter={props.filter}
title title
dir={props.source.textDir}
/> />
</h3> </h3>
{Boolean(props.viewConfigs & ViewConfigs.ShowSnippet) && ( {Boolean(props.viewConfigs & ViewConfigs.ShowSnippet) && (
@ -39,7 +40,6 @@ const ListCard: React.FunctionComponent<Card.Props> = props => (
<Highlights <Highlights
text={props.item.snippet} text={props.item.snippet}
filter={props.filter} filter={props.filter}
dir={props.source.textDir}
/> />
</p> </p>
)} )}

View File

@ -2,11 +2,13 @@ import * as React from "react"
import { Card } from "./card" import { Card } from "./card"
import CardInfo from "./info" import CardInfo from "./info"
import Highlights from "./highlights" import Highlights from "./highlights"
import { SourceTextDirection } from "../../scripts/models/source"
const className = (props: Card.Props) => { const className = (props: Card.Props) => {
let cn = ["card", "magazine-card"] let cn = ["card", "magazine-card"]
if (props.item.hasRead) cn.push("read") if (props.item.hasRead) cn.push("read")
if (props.item.hidden) cn.push("hidden") if (props.item.hidden) cn.push("hidden")
if (props.source.textDir === SourceTextDirection.RTL) cn.push("rtl")
return cn.join(" ") return cn.join(" ")
} }
@ -28,14 +30,12 @@ const MagazineCard: React.FunctionComponent<Card.Props> = props => (
text={props.item.title} text={props.item.title}
filter={props.filter} filter={props.filter}
title title
dir={props.source.textDir}
/> />
</h3> </h3>
<p className="snippet"> <p className="snippet">
<Highlights <Highlights
text={props.item.snippet} text={props.item.snippet}
filter={props.filter} filter={props.filter}
dir={props.source.textDir}
/> />
</p> </p>
</div> </div>

View File

@ -49,6 +49,7 @@ export class Menu extends React.Component<MenuProps> {
intl.get("allArticles") + intl.get("allArticles") +
this.countOverflow( this.countOverflow(
Object.values(this.props.sources) Object.values(this.props.sources)
.filter(s => !s.hidden)
.map(s => s.unreadCount) .map(s => s.unreadCount)
.reduce((a, b) => a + b, 0) .reduce((a, b) => a + b, 0)
), ),

View File

@ -16,6 +16,7 @@ import {
Dropdown, Dropdown,
MessageBar, MessageBar,
MessageBarType, MessageBarType,
Toggle,
} from "@fluentui/react" } from "@fluentui/react"
import { import {
SourceState, SourceState,
@ -42,6 +43,7 @@ type SourcesTabProps = {
deleteSources: (sources: RSSSource[]) => void deleteSources: (sources: RSSSource[]) => void
importOPML: () => void importOPML: () => void
exportOPML: () => void exportOPML: () => void
toggleSourceHidden: (source: RSSSource) => void
} }
type SourcesTabState = { type SourcesTabState = {
@ -217,6 +219,16 @@ class SourcesTab extends React.Component<SourcesTabProps, SourcesTabState> {
}) })
} }
onToggleHidden = () => {
this.props.toggleSourceHidden(this.state.selectedSource)
this.setState({
selectedSource: {
...this.state.selectedSource,
hidden: !this.state.selectedSource.hidden,
} as RSSSource,
})
}
render = () => ( render = () => (
<div className="tab-body"> <div className="tab-body">
{this.props.serviceOn && ( {this.props.serviceOn && (
@ -409,6 +421,17 @@ class SourcesTab extends React.Component<SourcesTabProps, SourcesTabState> {
)} )}
onChange={this.onOpenTargetChange} onChange={this.onOpenTargetChange}
/> />
<Stack horizontal verticalAlign="baseline">
<Stack.Item grow>
<Label>{intl.get("sources.hidden")}</Label>
</Stack.Item>
<Stack.Item>
<Toggle
checked={this.state.selectedSource.hidden}
onChange={this.onToggleHidden}
/>
</Stack.Item>
</Stack>
{!this.state.selectedSource.serviceRef && ( {!this.state.selectedSource.serviceRef && (
<Stack horizontal> <Stack horizontal>
<Stack.Item> <Stack.Item>

View File

@ -10,6 +10,7 @@ import {
deleteSource, deleteSource,
SourceOpenTarget, SourceOpenTarget,
deleteSources, deleteSources,
toggleSourceHidden,
} from "../../scripts/models/source" } from "../../scripts/models/source"
import { importOPML, exportOPML } from "../../scripts/models/group" import { importOPML, exportOPML } from "../../scripts/models/group"
import { AppDispatch, validateFavicon } from "../../scripts/utils" import { AppDispatch, validateFavicon } from "../../scripts/utils"
@ -67,6 +68,8 @@ const mapDispatchToProps = (dispatch: AppDispatch) => {
dispatch(deleteSources(sources)), dispatch(deleteSources(sources)),
importOPML: () => dispatch(importOPML()), importOPML: () => dispatch(importOPML()),
exportOPML: () => dispatch(exportOPML()), exportOPML: () => dispatch(exportOPML()),
toggleSourceHidden: (source: RSSSource) =>
dispatch(toggleSourceHidden(source)),
} }
} }

View File

@ -4,7 +4,7 @@ import lf from "lovefield"
import { RSSSource } from "./models/source" import { RSSSource } from "./models/source"
import { RSSItem } from "./models/item" import { RSSItem } from "./models/item"
const sdbSchema = lf.schema.create("sourcesDB", 2) const sdbSchema = lf.schema.create("sourcesDB", 3)
sdbSchema sdbSchema
.createTable("sources") .createTable("sources")
.addColumn("sid", lf.Type.INTEGER) .addColumn("sid", lf.Type.INTEGER)
@ -18,6 +18,7 @@ sdbSchema
.addColumn("fetchFrequency", lf.Type.NUMBER) .addColumn("fetchFrequency", lf.Type.NUMBER)
.addColumn("rules", lf.Type.OBJECT) .addColumn("rules", lf.Type.OBJECT)
.addColumn("textDir", lf.Type.NUMBER) .addColumn("textDir", lf.Type.NUMBER)
.addColumn("hidden", lf.Type.BOOLEAN)
.addNullable(["iconurl", "serviceRef", "rules"]) .addNullable(["iconurl", "serviceRef", "rules"])
.addIndex("idxURL", ["url"], true) .addIndex("idxURL", ["url"], true)
@ -54,6 +55,9 @@ async function onUpgradeSourceDB(rawDb: lf.raw.BackStore) {
if (version < 2) { if (version < 2) {
await rawDb.addTableColumn("sources", "textDir", 0) await rawDb.addTableColumn("sources", "textDir", 0)
} }
if (version < 3) {
await rawDb.addTableColumn("sources", "hidden", false)
}
} }
export async function init() { export async function init() {
@ -99,6 +103,7 @@ async function migrateNeDB() {
delete doc._id delete doc._id
if (!doc.fetchFrequency) doc.fetchFrequency = 0 if (!doc.fetchFrequency) doc.fetchFrequency = 0
doc.textDir = 0 doc.textDir = 0
doc.hidden = false
return sources.createRow(doc) return sources.createRow(doc)
}) })
const iRows = itemDocs.map(doc => { const iRows = itemDocs.map(doc => {

View File

@ -7,7 +7,7 @@
"openExternal": "Extern öffnen", "openExternal": "Extern öffnen",
"emptyName": "Dieses Feld darf nicht leer sein.", "emptyName": "Dieses Feld darf nicht leer sein.",
"emptyField": "Dieses Feld darf nicht leer sein.", "emptyField": "Dieses Feld darf nicht leer sein.",
"edit": "Berarbeiten", "edit": "Bearbeiten",
"delete": "Entfernen", "delete": "Entfernen",
"followSystem": "Systemstandard verwenden", "followSystem": "Systemstandard verwenden",
"more": "Mehr", "more": "Mehr",
@ -62,7 +62,7 @@
"markBelow": "Darunter als gelesen markieren", "markBelow": "Darunter als gelesen markieren",
"star": "Favorisieren", "star": "Favorisieren",
"unstar": "Favorit entfernen", "unstar": "Favorit entfernen",
"fontSize": "Schritgröße", "fontSize": "Schriftgröße",
"loadWebpage": "Internetseite laden", "loadWebpage": "Internetseite laden",
"loadFull": "Kompletten Inhalt laden", "loadFull": "Kompletten Inhalt laden",
"notify": "Benachrichtigen, wenn im Hintergrund geladen", "notify": "Benachrichtigen, wenn im Hintergrund geladen",
@ -201,7 +201,7 @@
"fetchLimitNum": "{count} Artikel", "fetchLimitNum": "{count} Artikel",
"importGroups": "Gruppen importieren", "importGroups": "Gruppen importieren",
"failure": "Fehler beim Verbinden zum Server", "failure": "Fehler beim Verbinden zum Server",
"failureHint": "Bite überprüfe die Server-Konfiguration oder deinen Netzwerk-Status.", "failureHint": "Bitte überprüfe die Server-Konfiguration oder deinen Netzwerk-Status.",
"fetchUnlimited": "Unbegrenzt (nicht empfohlen)", "fetchUnlimited": "Unbegrenzt (nicht empfohlen)",
"exportToLite": "Für Fluent Reader Lite exportieren" "exportToLite": "Für Fluent Reader Lite exportieren"
}, },

View File

@ -149,7 +149,8 @@
"badUrl": "Invalid URL", "badUrl": "Invalid URL",
"deleteWarning": "The source and all saved articles will be removed.", "deleteWarning": "The source and all saved articles will be removed.",
"selected": "Selected source", "selected": "Selected source",
"selectedMulti": "Selected multiple sources" "selectedMulti": "Selected multiple sources",
"hidden": "Hide in \"all articles\""
}, },
"groups": { "groups": {
"exist": "This group already exists.", "exist": "This group already exists.",

View File

@ -147,7 +147,8 @@
"badUrl": "请正确输入URL", "badUrl": "请正确输入URL",
"deleteWarning": "这将移除订阅源与所有已保存的文章", "deleteWarning": "这将移除订阅源与所有已保存的文章",
"selected": "选中订阅源", "selected": "选中订阅源",
"selectedMulti": "选中多个订阅源" "selectedMulti": "选中多个订阅源",
"hidden": "从“全部文章”中隐藏"
}, },
"groups": { "groups": {
"exist": "该分组已存在", "exist": "该分组已存在",

View File

@ -147,7 +147,8 @@
"badUrl": "請正確輸入URL", "badUrl": "請正確輸入URL",
"deleteWarning": "這將移除訂閱源與所有已儲存的文章", "deleteWarning": "這將移除訂閱源與所有已儲存的文章",
"selected": "選中訂閱源", "selected": "選中訂閱源",
"selectedMulti": "選中多個訂閱源" "selectedMulti": "選中多個訂閱源",
"hidden": "從“全部文章”中隱藏"
}, },
"groups": { "groups": {
"exist": "該分組已存在", "exist": "該分組已存在",

View File

@ -5,6 +5,8 @@ import {
INIT_SOURCES, INIT_SOURCES,
ADD_SOURCE, ADD_SOURCE,
DELETE_SOURCE, DELETE_SOURCE,
UNHIDE_SOURCE,
HIDE_SOURCE,
} from "./source" } from "./source"
import { import {
ItemActionTypes, ItemActionTypes,
@ -316,13 +318,16 @@ export function feedReducer(
...state, ...state,
[ALL]: new RSSFeed( [ALL]: new RSSFeed(
ALL, ALL,
Object.values(action.sources).map(s => s.sid) Object.values(action.sources)
.filter(s => !s.hidden)
.map(s => s.sid)
), ),
} }
default: default:
return state return state
} }
case ADD_SOURCE: case ADD_SOURCE:
case UNHIDE_SOURCE:
switch (action.status) { switch (action.status) {
case ActionStatus.Success: case ActionStatus.Success:
return { return {
@ -336,7 +341,8 @@ export function feedReducer(
default: default:
return state return state
} }
case DELETE_SOURCE: { case DELETE_SOURCE:
case HIDE_SOURCE: {
let nextState = {} let nextState = {}
for (let [id, feed] of Object.entries(state)) { for (let [id, feed] of Object.entries(state)) {
nextState[id] = new RSSFeed( nextState[id] = new RSSFeed(

View File

@ -41,6 +41,7 @@ export class RSSSource {
fetchFrequency: number // in minutes fetchFrequency: number // in minutes
rules?: SourceRule[] rules?: SourceRule[]
textDir: SourceTextDirection textDir: SourceTextDirection
hidden: boolean
constructor(url: string, name: string = null) { constructor(url: string, name: string = null) {
this.url = url this.url = url
@ -49,6 +50,7 @@ export class RSSSource {
this.lastFetched = new Date() this.lastFetched = new Date()
this.fetchFrequency = 0 this.fetchFrequency = 0
this.textDir = SourceTextDirection.LTR this.textDir = SourceTextDirection.LTR
this.hidden = false
} }
static async fetchMetaData(source: RSSSource) { static async fetchMetaData(source: RSSSource) {
@ -120,6 +122,8 @@ export const ADD_SOURCE = "ADD_SOURCE"
export const UPDATE_SOURCE = "UPDATE_SOURCE" export const UPDATE_SOURCE = "UPDATE_SOURCE"
export const UPDATE_UNREAD_COUNTS = "UPDATE_UNREAD_COUNTS" export const UPDATE_UNREAD_COUNTS = "UPDATE_UNREAD_COUNTS"
export const DELETE_SOURCE = "DELETE_SOURCE" export const DELETE_SOURCE = "DELETE_SOURCE"
export const HIDE_SOURCE = "HIDE_SOURCE"
export const UNHIDE_SOURCE = "UNHIDE_SOURCE"
interface InitSourcesAction { interface InitSourcesAction {
type: typeof INIT_SOURCES type: typeof INIT_SOURCES
@ -151,12 +155,19 @@ interface DeleteSourceAction {
source: RSSSource source: RSSSource
} }
interface ToggleSourceHiddenAction {
type: typeof HIDE_SOURCE | typeof UNHIDE_SOURCE
status: ActionStatus
source: RSSSource
}
export type SourceActionTypes = export type SourceActionTypes =
| InitSourcesAction | InitSourcesAction
| AddSourceAction | AddSourceAction
| UpdateSourceAction | UpdateSourceAction
| UpdateUnreadCountsAction | UpdateUnreadCountsAction
| DeleteSourceAction | DeleteSourceAction
| ToggleSourceHiddenAction
export function initSourcesRequest(): SourceActionTypes { export function initSourcesRequest(): SourceActionTypes {
return { return {
@ -382,6 +393,19 @@ export function deleteSources(sources: RSSSource[]): AppThunk<Promise<void>> {
} }
} }
export function toggleSourceHidden(source: RSSSource): AppThunk<Promise<void>> {
return async (dispatch, getState) => {
const sourceCopy: RSSSource = { ...getState().sources[source.sid] }
sourceCopy.hidden = !sourceCopy.hidden
dispatch({
type: sourceCopy.hidden ? HIDE_SOURCE : UNHIDE_SOURCE,
status: ActionStatus.Success,
source: sourceCopy,
})
await dispatch(updateSource(sourceCopy))
}
}
export function updateFavicon( export function updateFavicon(
sids?: number[], sids?: number[],
force = false force = false

View File

@ -146,6 +146,7 @@ export async function importAll() {
const sRows = configs.lovefield.sources.map(s => { const sRows = configs.lovefield.sources.map(s => {
s.lastFetched = new Date(s.lastFetched) s.lastFetched = new Date(s.lastFetched)
if (!s.textDir) s.textDir = SourceTextDirection.LTR if (!s.textDir) s.textDir = SourceTextDirection.LTR
if (!s.hidden) s.hidden = false
return db.sources.createRow(s) return db.sources.createRow(s)
}) })
const iRows = configs.lovefield.items.map(i => { const iRows = configs.lovefield.items.map(i => {