Merge pull request #6 from yang991178/0.3.2

Version 0.3.2
This commit is contained in:
Haoyuan Liu 2020-06-20 15:59:57 +08:00 committed by GitHub
commit a2d93543e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 380 additions and 96 deletions

View File

@ -23,6 +23,7 @@ If you are using macOS or an older version of Windows, you can also [get Fluent
- Read the full content with the built-in article view or load webpages by default.
- Search for articles with regular expressions or filter by read status.
- Organize your subscriptions with folder-like groupings.
- Single-key [keyboard shortcuts](https://github.com/yang991178/fluent-reader/wiki/Support#keyboard-shortcuts).
## Development
@ -31,7 +32,7 @@ If you are using macOS or an older version of Windows, you can also [get Fluent
Make Fluent Reader better by reporting bugs or opening feature requests through [GitHub issues](https://github.com/yang991178/fluent-reader/issues).
You can also help internationalize the app by providing [translations into additional languages](https://github.com/yang991178/fluent-reader/tree/master/src/scripts/i18n).
Refer to the repo of [react-intl-universal](https://github.com/alibaba/react-intl-universal) for internationalization references.
Refer to the repo of [react-intl-universal](https://github.com/alibaba/react-intl-universal) to get started on internationalization.
If you enjoyed using the app, consider supporting its development by [donating through Paypal](https://www.paypal.me/yang991178) or buying the app from Microsoft Store.

View File

@ -61,3 +61,6 @@ article figure figcaption {
color: var(--gray);
-webkit-user-modify: read-only;
}
article iframe {
width: 100%;
}

60
dist/icons/logo-outline-dark.svg vendored Normal file
View File

@ -0,0 +1,60 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="350" height="350" viewBox="0 0 350 350">
<defs>
<style>
:root {
--white: #1f1f1f;
--primary: #3f3f3f;
}
.cls-1 {
clip-path: url(#clip-Web_1280_5);
}
.cls-2, .cls-3 {
fill: var(--white);
}
.cls-2 {
stroke: var(--primary);
stroke-width: 5px;
}
.cls-4 {
fill: var(--primary);
}
.cls-5 {
fill: var(--white);
font-size: 167px;
font-family: Arial-BoldMT, Arial;
font-weight: 700;
}
.cls-6 {
stroke: none;
}
.cls-7 {
fill: none;
}
</style>
<clipPath id="clip-Web_1280_5">
<rect width="350" height="350"/>
</clipPath>
</defs>
<g class="cls-1">
<rect class="cls-3" width="350" height="350"/>
<g class="cls-2" transform="translate(45 25)">
<rect class="cls-6" width="280" height="280" rx="5"/>
<rect class="cls-7" x="2.5" y="2.5" width="275" height="275" rx="2.5"/>
</g>
<rect class="cls-3" width="235" height="235" rx="5" transform="translate(30 85)"/>
<g class="cls-2" transform="translate(35 90)">
<rect class="cls-6" width="225" height="225" rx="5"/>
<rect class="cls-7" x="2.5" y="2.5" width="220" height="220" rx="2.5"/>
</g>
<rect class="cls-3" width="180" height="180" rx="5" transform="translate(20 150)"/>
<rect class="cls-4" width="170" height="170" rx="5" transform="translate(25 155)"/>
<text id="F" class="cls-5" transform="translate(59 301)"><tspan x="0" y="0">F</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

60
dist/icons/logo-outline.svg vendored Normal file
View File

@ -0,0 +1,60 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="350" height="350" viewBox="0 0 350 350">
<defs>
<style>
:root {
--white: #fff;
--primary: #e1dfdd;
}
.cls-1 {
clip-path: url(#clip-Web_1280_5);
}
.cls-2, .cls-3 {
fill: var(--white);
}
.cls-2 {
stroke: var(--primary);
stroke-width: 5px;
}
.cls-4 {
fill: var(--primary);
}
.cls-5 {
fill: var(--white);
font-size: 167px;
font-family: Arial-BoldMT, Arial;
font-weight: 700;
}
.cls-6 {
stroke: none;
}
.cls-7 {
fill: none;
}
</style>
<clipPath id="clip-Web_1280_5">
<rect width="350" height="350"/>
</clipPath>
</defs>
<g class="cls-1">
<rect class="cls-3" width="350" height="350"/>
<g class="cls-2" transform="translate(45 25)">
<rect class="cls-6" width="280" height="280" rx="5"/>
<rect class="cls-7" x="2.5" y="2.5" width="275" height="275" rx="2.5"/>
</g>
<rect class="cls-3" width="235" height="235" rx="5" transform="translate(30 85)"/>
<g class="cls-2" transform="translate(35 90)">
<rect class="cls-6" width="225" height="225" rx="5"/>
<rect class="cls-7" x="2.5" y="2.5" width="220" height="220" rx="2.5"/>
</g>
<rect class="cls-3" width="180" height="180" rx="5" transform="translate(20 150)"/>
<rect class="cls-4" width="170" height="170" rx="5" transform="translate(25 155)"/>
<text id="F" class="cls-5" transform="translate(59 301)"><tspan x="0" y="0">F</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

44
dist/styles.css vendored
View File

@ -277,6 +277,7 @@ nav.menu-on .btn-group .btn.system, nav.item-on .btn-group .btn.system {
height: calc(100% - 64px);
background-color: var(--white);
box-shadow: 0 6.4px 14.4px 0 rgba(0,0,0,.132), 0 1.2px 3.6px 0 rgba(0,0,0,.108);
overflow: hidden;
}
div[role="toolbar"] {
height: 100%;
@ -469,12 +470,34 @@ img.favicon {
white-space: nowrap;
display: inline-block;
}
.side-article-wrapper {
.side-article-wrapper, .side-logo-wrapper {
flex-grow: 1;
padding-top: 32px;
height: calc(100% - 32px);
background: var(--white);
}
.side-logo-wrapper {
display: flex;
justify-content: center;
align-items: center;
}
.side-logo-wrapper > img {
width: 120px;
height: 120px;
user-select: none;
-webkit-user-drag: none;
}
.side-logo-wrapper > img.dark {
display: none;
}
@media (prefers-color-scheme: dark) {
.side-logo-wrapper > img.light {
display: none;
}
.side-logo-wrapper > img.dark {
display: inline;
}
}
.side-article-wrapper .article {
display: flex;
flex-direction: column-reverse;
@ -631,8 +654,19 @@ img.favicon {
cursor: pointer;
animation-fill-mode: none;
}
.card:hover {
.card:hover, .card:focus {
box-shadow: #0006 0px 5px 40px;
outline: none;
}
.card:focus::after, .list-card:focus::after {
content: "";
position: absolute;
top: 2px;
left: 2px;
width: calc(100% - 6px);
height: calc(100% - 6px);
border: 1px solid var(--white);
outline: 2px solid #0078d4;
}
.card:active {
transform: scale(.97);
@ -663,7 +697,8 @@ img.favicon {
.card img.head, .card p, .card h3 {
transition: transform ease-out .12s;
}
.card.transform:hover img.head, .card.transform:hover p, .card.transform:hover h3 {
.card.transform:hover img.head, .card.transform:hover p, .card.transform:hover h3,
.card.transform:focus img.head, .card.transform:focus p, .card.transform:focus h3 {
transform: translateY(-144px);
}
.card h3.title {
@ -717,8 +752,9 @@ img.favicon {
cursor: pointer;
box-shadow: #0000 0px 5px 15px;
}
.list-card:hover {
.list-card:hover, .list-card:focus {
box-shadow: #0004 0px 5px 15px;
outline: none;
}
.list-card:active {
box-shadow: #0000 0px 5px 15px, inset #0004 0px 0px 15px;

View File

@ -29,7 +29,7 @@
<img class="elevate" src="imgs/opml.png" />
</section>
<section class="elevate">
<h2>Read comfortably.</h2>
<h2>Read fluently.</h2>
<p>
Enjoy your contents like never before with the built-in article view
for RSS full text tailored to maximize focus. Source only comes with
@ -56,7 +56,7 @@
<h3>No personal information collected, ever.</h3>
<h3>Behavior tracking limited.</h3>
<h3>Strict Content Security Policy enforced.</h3>
<h3>Sandboxed webview.</h3>
<h3>Proxy support with PAC.</h3>
<br />
<h3><span>■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■</span></h3>
<h3><span>■ ■ ■ ■ ■ ■</span></h3>
@ -74,7 +74,8 @@
<div><a href="https://github.com/yang991178/fluent-reader/releases">Download for macOS or older Windows</a></div>
<div class="links">
<a href="https://github.com/yang991178/fluent-reader/">GitHub</a>
<a href="https://github.com/yang991178/fluent-reader/blob/master/PRIVACY.md">Privacy</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>
</div>
</body>

View File

@ -1,6 +1,6 @@
{
"name": "fluent-reader",
"version": "0.3.1",
"version": "0.3.2",
"description": "A simplistic, modern desktop RSS reader",
"main": "./dist/electron.js",
"scripts": {

View File

@ -3,10 +3,10 @@ import intl = require("react-intl-universal")
import { renderToString } from "react-dom/server"
import { RSSItem } from "../scripts/models/item"
import { openExternal } from "../scripts/utils"
import { Stack, CommandBarButton, IContextualMenuProps } from "@fluentui/react"
import { Stack, CommandBarButton, IContextualMenuProps, FocusZone } from "@fluentui/react"
import { RSSSource, SourceOpenTarget } from "../scripts/models/source"
import { store } from "../scripts/settings"
import { clipboard } from "electron"
import { clipboard, remote } from "electron"
const FONT_SIZE_STORE_KEY = "fontSize"
const FONT_SIZE_OPTIONS = [12, 13, 14, 15, 16, 17, 18, 19, 20]
@ -15,7 +15,9 @@ type ArticleProps = {
item: RSSItem
source: RSSSource
locale: string
shortcuts: (item: RSSItem, key: string) => void
dismiss: () => void
offsetItem: (offset: number) => void
toggleHasRead: (item: RSSItem) => void
toggleStarred: (item: RSSItem) => void
toggleHidden: (item: RSSItem) => void
@ -28,7 +30,8 @@ type ArticleState = {
}
class Article extends React.Component<ArticleProps, ArticleState> {
webview: HTMLWebViewElement
webview: Electron.WebviewTag
shouldRefocus = false
constructor(props) {
super(props)
@ -100,15 +103,42 @@ class Article extends React.Component<ArticleProps, ArticleState> {
openExternal(event.url)
this.props.dismiss()
}
keyDownHandler = (_, input) => {
if (input.type === "keyDown") {
switch (input.key) {
case "Escape":
this.shouldRefocus = true
this.props.dismiss()
break
case "ArrowLeft":
case "ArrowRight":
this.props.offsetItem(input.key === "ArrowLeft" ? -1 : 1)
break
case "l":
this.toggleWebpage()
break
default:
this.props.shortcuts(this.props.item, input.key)
break
}
}
}
componentDidMount = () => {
let webview = document.getElementById("article")
let webview = document.getElementById("article") as Electron.WebviewTag
if (webview != this.webview) {
if (this.webview) this.componentWillUnmount()
webview.addEventListener("ipc-message", this.ipcHandler)
webview.addEventListener("new-window", this.popUpHandler)
webview.addEventListener("will-navigate", this.navigationHandler)
webview.addEventListener("dom-ready", () => {
let webContents = remote.webContents.fromId(webview.getWebContentsId())
webContents.on("before-input-event", this.keyDownHandler)
})
this.webview = webview
webview.focus()
let card = document.querySelector(`#refocus>div[data-iid="${this.props.item._id}"]`) as HTMLElement
// @ts-ignore
if (card) card.scrollIntoViewIfNeeded()
}
}
componentDidUpdate = (prevProps: ArticleProps) => {
@ -119,9 +149,10 @@ class Article extends React.Component<ArticleProps, ArticleState> {
}
componentWillUnmount = () => {
this.webview.removeEventListener("ipc-message", this.ipcHandler)
this.webview.removeEventListener("new-window", this.popUpHandler)
this.webview.removeEventListener("will-navigate", this.navigationHandler)
if (this.shouldRefocus) {
let refocus = document.querySelector(`#refocus>div[data-iid="${this.props.item._id}"]`) as HTMLElement
if (refocus) refocus.focus()
}
}
openInBrowser = () => {
@ -143,7 +174,7 @@ class Article extends React.Component<ArticleProps, ArticleState> {
</>))) + `&s=${this.state.fontSize}&u=${this.props.item.link}`
render = () => (
<div className="article">
<FocusZone className="article">
<Stack horizontal style={{height: 36}}>
<span style={{width: 96}}></span>
<Stack className="actions" grow horizontal tokens={{childrenGap: 12}}>
@ -193,7 +224,7 @@ class Article extends React.Component<ArticleProps, ArticleState> {
src={this.state.loadWebpage ? this.props.item.link : this.articleView()}
preload={this.state.loadWebpage ? null : "article/preload.js"}
partition="sandbox" />
</div>
</FocusZone>
)
}

View File

@ -7,6 +7,7 @@ export interface CardProps {
feedId: string
item: RSSItem
source: RSSSource
shortcuts: (item: RSSItem, key: string) => void
markRead: (item: RSSItem) => void
contextMenu: (feedId: string, item: RSSItem, e) => void
showItem: (fid: string, item: RSSItem) => void
@ -46,4 +47,8 @@ export class Card extends React.Component<CardProps> {
this.props.contextMenu(this.props.feedId, this.props.item, e)
}
}
onKeyDown = (e: React.KeyboardEvent) => {
this.props.shortcuts(this.props.item, e.key)
}
}

View File

@ -13,8 +13,14 @@ class DefaultCard extends Card {
render() {
return (
<div className={this.className()}
onClick={this.onClick} onMouseUp={this.onMouseUp} >
<div
className={this.className()}
onClick={this.onClick}
onMouseUp={this.onMouseUp}
onMouseDown={event => event.preventDefault()}
onKeyDown={this.onKeyDown}
data-iid={this.props.item._id}
data-is-focusable>
{this.props.item.thumb ? (
<img className="bg" src={this.props.item.thumb} />
) : null}

View File

@ -12,8 +12,14 @@ class ListCard extends Card {
render() {
return (
<div className={this.className()}
onClick={this.onClick} onMouseUp={this.onMouseUp} >
<div
className={this.className()}
onClick={this.onClick}
onMouseUp={this.onMouseUp}
onMouseDown={event => event.preventDefault()}
onKeyDown={this.onKeyDown}
data-iid={this.props.item._id}
data-is-focusable>
{this.props.item.thumb ? (
<div className="head"><img src={this.props.item.thumb} /></div>
) : null}

View File

@ -2,7 +2,7 @@ import * as React from "react"
import intl = require("react-intl-universal")
import { FeedProps } from "./feed"
import DefaultCard from "../cards/default-card"
import { PrimaryButton } from 'office-ui-fabric-react';
import { PrimaryButton, FocusZone } from 'office-ui-fabric-react';
class CardsFeed extends React.Component<FeedProps> {
state = { width: window.innerWidth - 12 }
@ -31,7 +31,7 @@ class CardsFeed extends React.Component<FeedProps> {
render() {
return this.props.feed.loaded && (
<div className="cards-feed-container">
<FocusZone as="div" id="refocus" className="cards-feed-container">
{
this.props.items.map((item) => (
<DefaultCard
@ -39,6 +39,7 @@ class CardsFeed extends React.Component<FeedProps> {
key={item._id}
item={item}
source={this.props.sourceMap[item.source]}
shortcuts={this.props.shortcuts}
markRead={this.props.markRead}
contextMenu={this.props.contextMenu}
showItem={this.props.showItem} />
@ -56,7 +57,7 @@ class CardsFeed extends React.Component<FeedProps> {
{ this.props.items.length === 0 && (
<div className="empty">{intl.get("article.empty")}</div>
)}
</div>
</FocusZone>
)
}
}

View File

@ -11,6 +11,7 @@ export type FeedProps = FeedReduxProps & {
viewType: ViewType
items: RSSItem[]
sourceMap: Object
shortcuts: (item: RSSItem, key: string) => void
markRead: (item: RSSItem) => void
contextMenu: (feedId: string, item: RSSItem, e) => void
loadMore: (feed: RSSFeed) => void

View File

@ -1,13 +1,13 @@
import * as React from "react"
import intl = require("react-intl-universal")
import { FeedProps } from "./feed"
import { DefaultButton } from 'office-ui-fabric-react';
import { DefaultButton, FocusZone, FocusZoneDirection } from 'office-ui-fabric-react';
import ListCard from "../cards/list-card";
class ListFeed extends React.Component<FeedProps> {
render() {
return this.props.feed.loaded && (
<div className="list-feed">
<FocusZone as="div" id="refocus" direction={FocusZoneDirection.vertical} className="list-feed">
{
this.props.items.map((item) => (
<ListCard
@ -15,6 +15,7 @@ class ListFeed extends React.Component<FeedProps> {
key={item._id}
item={item}
source={this.props.sourceMap[item.source]}
shortcuts={this.props.shortcuts}
markRead={this.props.markRead}
contextMenu={this.props.contextMenu}
showItem={this.props.showItem} />
@ -31,7 +32,7 @@ class ListFeed extends React.Component<FeedProps> {
{ this.props.items.length === 0 && (
<div className="empty">{intl.get("article.empty")}</div>
)}
</div>
</FocusZone>
)
}
}

View File

@ -26,7 +26,7 @@ export type MenuProps = {
export class Menu extends React.Component<MenuProps> {
countOverflow = (count: number) => count >= 1000 ? "999+" : String(count)
getItems = (): INavLinkGroup[] => [
getLinkGroups = (): INavLinkGroup[] => [
{
links: [
{
@ -46,10 +46,9 @@ export class Menu extends React.Component<MenuProps> {
url: null
}
]
}
]
getGroups = (): INavLinkGroup[] => [{
},
{
name: intl.get("menu.subscriptions"),
links: this.props.groups.filter(g => g.sids.length > 0).map((g, i) => {
if (g.isMultiple) {
let sources = g.sids.map(sid => this.props.sources[sid])
@ -65,8 +64,9 @@ export class Menu extends React.Component<MenuProps> {
} else {
return this.getSource(this.props.sources[g.sids[0]])
}
})
}
)}]
]
getSource = (s: RSSSource): INavLink => ({
name: s.name,
@ -106,6 +106,10 @@ export class Menu extends React.Component<MenuProps> {
)
};
_onRenderGroupHeader = (group: INavLinkGroup): JSX.Element => {
return <p className={"subs-header " + AnimationClassNames.slideDownIn10}>{group.name}</p>;
}
render() {
return this.props.status && (
<div className="menu-container" onClick={this.props.toggleMenu} style={{display: this.props.display ? "block" : "none"}}>
@ -116,15 +120,11 @@ export class Menu extends React.Component<MenuProps> {
</div>
<div className="nav-wrapper">
<Nav
onRenderGroupHeader={this._onRenderGroupHeader}
onRenderLink={this._onRenderLink}
groups={this.getItems()}
selectedKey={this.props.selected} />
<p className={"subs-header " + AnimationClassNames.slideDownIn10}>{intl.get("menu.subscriptions")}</p>
<Nav
groups={this.getLinkGroups()}
selectedKey={this.props.selected}
onRenderLink={this._onRenderLink}
onLinkExpandClick={(event, item) => this.props.updateGroupExpansion(event, item.key, this.props.selected)}
groups={this.getGroups()} />
onLinkExpandClick={(event, item) => this.props.updateGroupExpansion(event, item.key, this.props.selected)} />
</div>
</div>
</div>

View File

@ -4,6 +4,7 @@ import { remote } from "electron"
import { Icon } from "@fluentui/react/lib/Icon"
import { AppState } from "../scripts/models/app"
import { ProgressIndicator } from "@fluentui/react"
import { getWindowBreakpoint } from "../scripts/utils"
type NavProps = {
state: AppState,
@ -37,6 +38,40 @@ class Nav extends React.Component<NavProps, NavState> {
}
}
navShortcutsHandler = (e: KeyboardEvent) => {
if (!this.props.state.settings.display) {
switch (e.key) {
case "F1":
this.props.menu()
break
case "F5":
this.fetch()
break
case "F6":
this.props.markAllRead()
break
case "F7":
if (!this.props.state.menu || getWindowBreakpoint())
this.props.logs()
break
case "F8":
if (!this.props.state.menu || getWindowBreakpoint())
this.props.views()
break
case "F9":
this.props.settings()
break
}
}
}
componentDidMount() {
document.addEventListener("keydown", this.navShortcutsHandler)
}
componentWillUnmount() {
document.removeEventListener("keydown", this.navShortcutsHandler)
}
minimize = () => {
this.state.window.minimize()
}

View File

@ -1,6 +1,6 @@
import * as React from "react"
import { FeedContainer } from "../containers/feed-container"
import { AnimationClassNames, Icon } from "@fluentui/react"
import { AnimationClassNames, Icon, FocusTrapZone } from "@fluentui/react"
import ArticleContainer from "../containers/article-container"
import { ViewType } from "../scripts/models/page"
import ArticleSearch from "./utils/article-search"
@ -16,50 +16,59 @@ type PageProps = {
}
class Page extends React.Component<PageProps> {
prevItem = (event: React.MouseEvent) => {
offsetItem = (event: React.MouseEvent, offset: number) => {
event.stopPropagation()
this.props.offsetItem(-1)
}
nextItem = (event: React.MouseEvent) => {
event.stopPropagation()
this.props.offsetItem(1)
this.props.offsetItem(offset)
}
prevItem = (event: React.MouseEvent) => this.offsetItem(event, -1)
nextItem = (event: React.MouseEvent) => this.offsetItem(event, 1)
render = () => this.props.viewType == ViewType.Cards
? (
<>
{this.props.settingsOn ? null :
<div className={"main" + (this.props.menuOn ? " menu-on" : "")}>
<div key="card" className={"main" + (this.props.menuOn ? " menu-on" : "")}>
<ArticleSearch />
{this.props.feeds.map(fid => (
<FeedContainer viewType={this.props.viewType} feedId={fid} key={fid} />
))}
</div>}
{this.props.itemId && (
<div className="article-container" onClick={this.props.dismissItem}>
<FocusTrapZone
ignoreExternalFocusing={true}
isClickableOutsideFocusTrap={true}
className="article-container"
onClick={this.props.dismissItem}>
<div className={"article-wrapper " + AnimationClassNames.slideUpIn20} onClick={e => e.stopPropagation()}>
<ArticleContainer itemId={this.props.itemId} />
</div>
<div className="btn-group prev"><a className="btn" onClick={this.prevItem}><Icon iconName="Back" /></a></div>
<div className="btn-group next"><a className="btn" onClick={this.nextItem}><Icon iconName="Forward" /></a></div>
</div>
</FocusTrapZone>
)}
</>
)
: (
<>
{this.props.settingsOn ? null :
<div className={"list-main" + (this.props.menuOn ? " menu-on" : "")}>
<div key="list" 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} />
))}
</div>
{this.props.itemId && (
{this.props.itemId
? (
<div className="side-article-wrapper">
<ArticleContainer itemId={this.props.itemId} />
</div>
)
: (
<div className="side-logo-wrapper">
<img className="light" src="icons/logo-outline.svg" />
<img className="dark" src="icons/logo-outline-dark.svg" />
</div>
)}
</div>}
</>

View File

@ -22,15 +22,15 @@ class Settings extends React.Component<SettingsProps> {
render = () => this.props.display && (
<div className="settings-container">
<div className={"settings " + AnimationClassNames.slideUpIn20}>
{this.props.blocked && <div className="loading">
<Spinner label={intl.get("settings.fetching")} />
</div>}
<div className="btn-group" style={{position: "absolute", top: 6, left: -64}}>
<div className="btn-group" style={{position: "absolute", top: 70, left: "calc(50% - 404px)"}}>
<a className={"btn" + (this.props.exitting ? " disabled" : "")} title={intl.get("settings.exit")} onClick={this.props.close}>
<Icon iconName="Back" />
</a>
</div>
<div className={"settings " + AnimationClassNames.slideUpIn20}>
{this.props.blocked && <div className="loading">
<Spinner label={intl.get("settings.fetching")} />
</div>}
<Pivot>
<PivotItem headerText={intl.get("settings.sources")} itemIcon="Source">
<SourcesTabContainer />

View File

@ -8,11 +8,12 @@ class AboutTab extends React.Component {
render = () => (
<div className="tab-body">
<Stack className="settings-about" horizontalAlign="center">
<img src="logo.svg" style={{width: 120, height: 120}} />
<img src="icons/logo.svg" style={{width: 120, height: 120}} />
<h3>Fluent Reader</h3>
<small>{intl.get("settings.version")} {remote.app.getVersion()}</small>
<p className="settings-hint">Copyright © 2020 Haoyuan Liu. All rights reserved.</p>
<Stack horizontal horizontalAlign="center" tokens={{childrenGap: 12}}>
<small><Link onClick={() => openExternal("https://github.com/yang991178/fluent-reader/wiki/Support#keyboard-shortcuts")}>{intl.get("settings.shortcuts")}</Link></small>
<small><Link onClick={() => openExternal("https://github.com/yang991178/fluent-reader")}>{intl.get("settings.openSource")}</Link></small>
<small><Link onClick={() => openExternal("https://github.com/yang991178/fluent-reader/issues")}>{intl.get("settings.feedback")}</Link></small>
</Stack>

View File

@ -1,9 +1,9 @@
import { connect } from "react-redux"
import { createSelector } from "reselect"
import { RootState } from "../scripts/reducer"
import { RSSItem, markUnread, markRead, toggleStarred, toggleHidden } from "../scripts/models/item"
import { RSSItem, markUnread, markRead, toggleStarred, toggleHidden, itemShortcuts } from "../scripts/models/item"
import { AppDispatch } from "../scripts/utils"
import { dismissItem } from "../scripts/models/page"
import { dismissItem, showOffsetItem } from "../scripts/models/page"
import Article from "../components/article"
import { openTextMenu } from "../scripts/models/app"
@ -28,7 +28,9 @@ const makeMapStateToProps = () => {
const mapDispatchToProps = (dispatch: AppDispatch) => {
return {
shortcuts: (item: RSSItem, key: string) => dispatch(itemShortcuts(item, key)),
dismiss: () => dispatch(dismissItem()),
offsetItem: (offset: number) => dispatch(showOffsetItem(offset)),
toggleHasRead: (item: RSSItem) => dispatch(item.hasRead ? markUnread(item) : markRead(item)),
toggleStarred: (item: RSSItem) => dispatch(toggleStarred(item)),
toggleHidden: (item: RSSItem) => dispatch(toggleHidden(item)),

View File

@ -1,7 +1,7 @@
import { connect } from "react-redux"
import { createSelector } from "reselect"
import { RootState } from "../scripts/reducer"
import { markRead, RSSItem } from "../scripts/models/item"
import { markRead, RSSItem, itemShortcuts } from "../scripts/models/item"
import { openItemMenu } from "../scripts/models/app"
import { loadMore, RSSFeed } from "../scripts/models/feed"
import { showItem, ViewType } from "../scripts/models/page"
@ -30,6 +30,7 @@ const makeMapStateToProps = () => {
}
const mapDispatchToProps = dispatch => {
return {
shortcuts: (item: RSSItem, key: string) => dispatch(itemShortcuts(item, key)),
markRead: (item: RSSItem) => dispatch(markRead(item)),
contextMenu: (feedId: string, item: RSSItem, e) => dispatch(openItemMenu(item, feedId, e)),
loadMore: (feed: RSSFeed) => dispatch(loadMore(feed)),

View File

@ -77,6 +77,7 @@
"app": "Preferences",
"about": "About",
"version": "Version",
"shortcuts": "Shortcuts",
"openSource": "Open source",
"feedback": "Feedback"
},

View File

@ -56,7 +56,7 @@
"copyTitle": "复制标题",
"copyURL": "复制链接",
"copy": "复制",
"search": "使用Google搜索“{text}”",
"search": "使用 Google 搜索“{text}”",
"view": "视图",
"cardView": "卡片视图",
"listView": "列表视图",
@ -77,6 +77,7 @@
"app": "应用偏好",
"about": "关于",
"version": "版本",
"shortcuts": "快捷键",
"openSource": "开源项目",
"feedback": "反馈"
},

View File

@ -1,6 +1,6 @@
import * as db from "../db"
import intl = require("react-intl-universal")
import { rssParser, domParser, htmlDecode, ActionStatus, AppThunk } from "../utils"
import { domParser, htmlDecode, ActionStatus, AppThunk, openExternal } from "../utils"
import { RSSSource } from "./source"
import { FeedActionTypes, INIT_FEED, LOAD_MORE } from "./feed"
import Parser = require("@yang991178/rss-parser")
@ -152,7 +152,7 @@ export function fetchItems(): AppThunk<Promise<void>> {
((s.lastFetched ? s.lastFetched.getTime() : 0) + (s.fetchFrequency || 0) * 60000) <= timenow
)
for (let source of sources) {
let promise = RSSSource.fetchItems(source, rssParser)
let promise = RSSSource.fetchItems(source)
promise.finally(() => dispatch(fetchItemsIntermediate()))
promises.push(promise)
}
@ -263,6 +263,26 @@ export function toggleHidden(item: RSSItem): AppThunk {
}
}
export function itemShortcuts(item: RSSItem, key: string): AppThunk {
return (dispatch) => {
switch (key) {
case "m":
if (item.hasRead) dispatch(markUnread(item))
else dispatch(markRead(item))
break
case "b":
openExternal(item.link)
break
case "s":
dispatch(toggleStarred(item))
break
case "h":
dispatch(toggleHidden(item))
break
}
}
}
export function applyItemReduction(item: RSSItem, type: string) {
let nextItem = { ...item }
switch (type) {

View File

@ -221,18 +221,20 @@ export function pageReducer(
switch (action.pageType) {
case PageType.AllArticles: return {
...state,
feedId: ALL
feedId: ALL,
itemId: null
}
case PageType.Sources: return {
...state,
feedId: SOURCE
feedId: SOURCE,
itemId: null
}
default: return state
}
case SWITCH_VIEW: return {
...state,
viewType: action.viewType,
itemId: action.viewType === ViewType.List ? state.itemId : null
itemId: null
}
case APPLY_FILTER: return {
...state,

View File

@ -1,7 +1,7 @@
import Parser = require("@yang991178/rss-parser")
import intl = require("react-intl-universal")
import * as db from "../db"
import { rssParser, fetchFavicon, ActionStatus, AppThunk, parseRSS } from "../utils"
import { fetchFavicon, ActionStatus, AppThunk, parseRSS } 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"
@ -28,16 +28,16 @@ export class RSSSource {
this.lastFetched = new Date()
}
async fetchMetaData(parser: Parser) {
let feed = await parseRSS(this.url)
if (!this.name) {
if (feed.title) this.name = feed.title.trim()
this.name = this.name || intl.get("sources.untitled")
static async fetchMetaData(source: RSSSource) {
let feed = await parseRSS(source.url)
if (!source.name) {
if (feed.title) source.name = feed.title.trim()
source.name = source.name || intl.get("sources.untitled")
}
let domain = this.url.split("/").slice(0, 3).join("/")
let domain = source.url.split("/").slice(0, 3).join("/")
try {
let f = await fetchFavicon(domain)
if (f !== null) this.iconurl = f
if (f !== null) source.iconurl = f
} finally {
return feed
}
@ -75,7 +75,7 @@ export class RSSSource {
})
}
static async fetchItems(source: RSSSource, parser: Parser) {
static async fetchItems(source: RSSSource) {
let feed = await parseRSS(source.url)
db.sdb.update({ sid: source.sid }, { $set: { lastFetched: new Date() } })
return await this.checkItems(source, feed.items)
@ -236,7 +236,7 @@ export function addSource(url: string, name: string = null, batch = false): AppT
if (app.sourceInit) {
dispatch(addSourceRequest(batch))
let source = new RSSSource(url, name)
return source.fetchMetaData(rssParser)
return RSSSource.fetchMetaData(source)
.then(feed => {
return dispatch(insertSource(source))
.then(inserted => {

View File

@ -1,6 +1,6 @@
import { remote, ipcRenderer } from "electron"
import { ViewType } from "./models/page"
import { IPartialTheme, loadTheme, values } from "@fluentui/react"
import { IPartialTheme, loadTheme } from "@fluentui/react"
import locales from "./i18n/_locales"
import Store = require("electron-store")
import { schemaTypes } from "./config-schema"
@ -51,7 +51,7 @@ export const setDefaultView = (viewType: ViewType) => {
}
const lightTheme: IPartialTheme = {
defaultFontStyle: { fontFamily: '"Source Han Sans SC Regular", "Microsoft YaHei", sans-serif' }
defaultFontStyle: { fontFamily: '"Segoe UI", "Source Han Sans SC Regular", "Microsoft YaHei", sans-serif' }
}
const darkTheme: IPartialTheme = {
...lightTheme,

View File

@ -17,7 +17,7 @@ export type AppThunk<ReturnType = void> = ThunkAction<
export type AppDispatch = ThunkDispatch<RootState, undefined, AnyAction>
import Parser = require("@yang991178/rss-parser")
export const rssParser = new Parser({
const rssParser = new Parser({
customFields: {
item: ["thumb", "image", ["content:encoded", "fullContent"]] as Parser.CustomFieldItem[]
}